diff --git a/CLAUDE.md b/CLAUDE.md index 0f6d148eb..3a085a9c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -240,6 +240,7 @@ Workflow Contracts define the structure and requirements for CI/CD attestations. - **Actions**: Implement business logic in `internal/action/` - **Policy Development**: Use `internal/policydevel/` for local policy testing - **Default Endpoints**: Override with `-ldflags` at build time using `defaultCASAPI` and `defaultCPAPI` variables +- **Documentation**: After adding or modifying CLI commands, regenerate docs with `make generate` (runs `go generate ./...` which includes CLI docs) ### Artifact CAS Development - **Storage Backends**: Implement new backends in `pkg/blobmanager/` with Provider interface diff --git a/app/cli/cmd/organization_update.go b/app/cli/cmd/organization_update.go index 0b7dec506..9c916fbae 100644 --- a/app/cli/cmd/organization_update.go +++ b/app/cli/cmd/organization_update.go @@ -31,6 +31,7 @@ func newOrganizationUpdateCmd() *cobra.Command { preventImplicitWorkflowCreation bool restrictContractCreation bool apiTokenMaxDaysInactive string + enableAIAgentCollector bool ) cmd := &cobra.Command{ @@ -54,6 +55,10 @@ func newOrganizationUpdateCmd() *cobra.Command { opts.RestrictContractCreation = &restrictContractCreation } + if cmd.Flags().Changed("enable-ai-agent-collector") { + opts.EnableAIAgentCollector = &enableAIAgentCollector + } + if cmd.Flags().Changed("api-token-max-days-inactive") { days, err := strconv.Atoi(apiTokenMaxDaysInactive) if err != nil { @@ -84,5 +89,6 @@ func newOrganizationUpdateCmd() *cobra.Command { cmd.Flags().BoolVar(&preventImplicitWorkflowCreation, "prevent-implicit-workflow-creation", false, "prevent workflows and projects from being created implicitly during attestation init") cmd.Flags().BoolVar(&restrictContractCreation, "restrict-contract-creation", false, "restrict contract creation (org-level and project-level) to only organization admins (owner/admin roles)") cmd.Flags().StringVar(&apiTokenMaxDaysInactive, "api-token-max-days-inactive", "", "maximum days of inactivity before API tokens are auto-revoked (e.g. '90', '0' to disable)") + cmd.Flags().BoolVar(&enableAIAgentCollector, "enable-ai-agent-collector", false, "enable automatic AI agent config collection during attestation init") return cmd } diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index c7e2da07d..bab1537db 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2824,6 +2824,7 @@ Options ``` --api-token-max-days-inactive string maximum days of inactivity before API tokens are auto-revoked (e.g. '90', '0' to disable) --block set the default policy violation blocking strategy +--enable-ai-agent-collector enable automatic AI agent config collection during attestation init -h, --help help for update --name string organization name --policies-allowed-hostnames strings set the allowed hostnames for the policy engine diff --git a/app/cli/pkg/action/attestation_init.go b/app/cli/pkg/action/attestation_init.go index c5124461c..6595014e8 100644 --- a/app/cli/pkg/action/attestation_init.go +++ b/app/cli/pkg/action/attestation_init.go @@ -19,6 +19,7 @@ import ( "context" "errors" "fmt" + "slices" "strconv" "github.com/chainloop-dev/chainloop/app/cli/internal/token" @@ -196,6 +197,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun attestationID string blockOnPolicyViolation bool policiesAllowedHostnames []string + enableAIAgentCollector bool // Timestamp Authority URL for new attestations timestampAuthorityURL, signingCAName string uiDashboardURL string @@ -226,6 +228,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun workflowMeta.Organization = result.GetOrganization() blockOnPolicyViolation = result.GetBlockOnPolicyViolation() policiesAllowedHostnames = result.GetPoliciesAllowedHostnames() + enableAIAgentCollector = result.GetEnableAiAgentCollector() signingOpts := result.GetSigningOptions() timestampAuthorityURL = signingOpts.GetTimestampAuthorityUrl() signingCAName = signingOpts.GetSigningCa() @@ -308,8 +311,9 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun } // Register and run auto-discovery collectors - // PR metadata is always collected; other collectors are opt-in via --collectors flag + // PR metadata is always collected; other collectors are opt-in via --collectors flag or org settings collectors := []crafter.Collector{crafter.NewPRMetadataCollector(discoveredRunner)} + aiConfigRequested := slices.Contains(opts.Collectors, aiConfigCollectorName) for _, name := range opts.Collectors { switch name { case aiConfigCollectorName: @@ -318,6 +322,11 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun action.Logger.Warn().Str("collector", name).Msg("unknown collector, skipping") } } + // Auto-enable AI agent config collector if the org setting is on and not already requested via CLI flag + if enableAIAgentCollector && !aiConfigRequested { + action.Logger.Debug().Msg("auto-enabling AI agent config collector from org setting") + collectors = append(collectors, crafter.NewAIAgentConfigCollector()) + } action.c.RegisterCollectors(collectors...) action.c.RunCollectors(ctx, attestationID, casBackend) diff --git a/app/cli/pkg/action/membership_list.go b/app/cli/pkg/action/membership_list.go index 36f82b042..1d27964c6 100644 --- a/app/cli/pkg/action/membership_list.go +++ b/app/cli/pkg/action/membership_list.go @@ -36,6 +36,7 @@ type OrgItem struct { PolicyAllowedHostnames []string `json:"policyAllowedHostnames,omitempty"` PreventImplicitWorkflowCreation bool `json:"preventImplicitWorkflowCreation"` APITokenMaxDaysInactive *string `json:"apiTokenMaxDaysInactive,omitempty"` + EnableAIAgentCollector bool `json:"enableAiAgentCollector"` } type MembershipItem struct { @@ -138,6 +139,7 @@ func pbOrgItemToAction(in *pb.OrgItem) *OrgItem { CreatedAt: toTimePtr(in.CreatedAt.AsTime()), PolicyAllowedHostnames: in.PolicyAllowedHostnames, PreventImplicitWorkflowCreation: in.PreventImplicitWorkflowCreation, + EnableAIAgentCollector: in.EnableAiAgentCollector, } if in.DefaultPolicyViolationStrategy == pb.OrgItem_POLICY_VIOLATION_BLOCKING_STRATEGY_BLOCK { diff --git a/app/cli/pkg/action/org_update.go b/app/cli/pkg/action/org_update.go index 3436b333a..8ec2f15bf 100644 --- a/app/cli/pkg/action/org_update.go +++ b/app/cli/pkg/action/org_update.go @@ -38,6 +38,8 @@ type NewOrgUpdateOpts struct { // APITokenMaxDaysInactive is the maximum number of days a token can be inactive before auto-revocation. // 0 means disabled. APITokenMaxDaysInactive *int + // EnableAIAgentCollector enables automatic AI agent config collection during attestation init + EnableAIAgentCollector *bool } func (action *OrgUpdate) Run(ctx context.Context, name string, opts *NewOrgUpdateOpts) (*OrgItem, error) { @@ -48,6 +50,7 @@ func (action *OrgUpdate) Run(ctx context.Context, name string, opts *NewOrgUpdat BlockOnPolicyViolation: opts.BlockOnPolicyViolation, PreventImplicitWorkflowCreation: opts.PreventImplicitWorkflowCreation, RestrictContractCreationToOrgAdmins: opts.RestrictContractCreation, + EnableAiAgentCollector: opts.EnableAIAgentCollector, } if opts.PoliciesAllowedHostnames != nil { diff --git a/app/controlplane/api/controlplane/v1/organization.pb.go b/app/controlplane/api/controlplane/v1/organization.pb.go index f26641613..7dcb83ffd 100644 --- a/app/controlplane/api/controlplane/v1/organization.pb.go +++ b/app/controlplane/api/controlplane/v1/organization.pb.go @@ -450,8 +450,10 @@ type OrganizationServiceUpdateRequest struct { RestrictContractCreationToOrgAdmins *bool `protobuf:"varint,6,opt,name=restrict_contract_creation_to_org_admins,json=restrictContractCreationToOrgAdmins,proto3,oneof" json:"restrict_contract_creation_to_org_admins,omitempty"` // Maximum days of inactivity before API tokens are auto-revoked. Set to 0 to disable. ApiTokenMaxDaysInactive *int32 `protobuf:"varint,7,opt,name=api_token_max_days_inactive,json=apiTokenMaxDaysInactive,proto3,oneof" json:"api_token_max_days_inactive,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Enable automatic AI agent config collection during attestation init + EnableAiAgentCollector *bool `protobuf:"varint,8,opt,name=enable_ai_agent_collector,json=enableAiAgentCollector,proto3,oneof" json:"enable_ai_agent_collector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *OrganizationServiceUpdateRequest) Reset() { @@ -533,6 +535,13 @@ func (x *OrganizationServiceUpdateRequest) GetApiTokenMaxDaysInactive() int32 { return 0 } +func (x *OrganizationServiceUpdateRequest) GetEnableAiAgentCollector() bool { + if x != nil && x.EnableAiAgentCollector != nil { + return *x.EnableAiAgentCollector + } + return false +} + type OrganizationServiceUpdateResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Result *OrgItem `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` @@ -691,7 +700,7 @@ const file_controlplane_v1_organization_proto_rawDesc = "" + " OrganizationServiceCreateRequest\x12\x1b\n" + "\x04name\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x04name\"U\n" + "!OrganizationServiceCreateResponse\x120\n" + - "\x06result\x18\x01 \x01(\v2\x18.controlplane.v1.OrgItemR\x06result\"\x8b\x05\n" + + "\x06result\x18\x01 \x01(\v2\x18.controlplane.v1.OrgItemR\x06result\"\xe9\x05\n" + " OrganizationServiceUpdateRequest\x12\x1b\n" + "\x04name\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x04name\x12>\n" + "\x19block_on_policy_violation\x18\x02 \x01(\bH\x00R\x16blockOnPolicyViolation\x88\x01\x01\x12<\n" + @@ -699,11 +708,13 @@ const file_controlplane_v1_organization_proto_rawDesc = "" + "!update_policies_allowed_hostnames\x18\x04 \x01(\bR\x1eupdatePoliciesAllowedHostnames\x12P\n" + "\"prevent_implicit_workflow_creation\x18\x05 \x01(\bH\x01R\x1fpreventImplicitWorkflowCreation\x88\x01\x01\x12Z\n" + "(restrict_contract_creation_to_org_admins\x18\x06 \x01(\bH\x02R#restrictContractCreationToOrgAdmins\x88\x01\x01\x12A\n" + - "\x1bapi_token_max_days_inactive\x18\a \x01(\x05H\x03R\x17apiTokenMaxDaysInactive\x88\x01\x01B\x1c\n" + + "\x1bapi_token_max_days_inactive\x18\a \x01(\x05H\x03R\x17apiTokenMaxDaysInactive\x88\x01\x01\x12>\n" + + "\x19enable_ai_agent_collector\x18\b \x01(\bH\x04R\x16enableAiAgentCollector\x88\x01\x01B\x1c\n" + "\x1a_block_on_policy_violationB%\n" + "#_prevent_implicit_workflow_creationB+\n" + ")_restrict_contract_creation_to_org_adminsB\x1e\n" + - "\x1c_api_token_max_days_inactive\"U\n" + + "\x1c_api_token_max_days_inactiveB\x1c\n" + + "\x1a_enable_ai_agent_collector\"U\n" + "!OrganizationServiceUpdateResponse\x120\n" + "\x06result\x18\x01 \x01(\v2\x18.controlplane.v1.OrgItemR\x06result\"?\n" + " OrganizationServiceDeleteRequest\x12\x1b\n" + diff --git a/app/controlplane/api/controlplane/v1/organization.proto b/app/controlplane/api/controlplane/v1/organization.proto index 1cb3d2443..24bbee8ea 100644 --- a/app/controlplane/api/controlplane/v1/organization.proto +++ b/app/controlplane/api/controlplane/v1/organization.proto @@ -102,6 +102,9 @@ message OrganizationServiceUpdateRequest { // Maximum days of inactivity before API tokens are auto-revoked. Set to 0 to disable. optional int32 api_token_max_days_inactive = 7; + + // Enable automatic AI agent config collection during attestation init + optional bool enable_ai_agent_collector = 8; } message OrganizationServiceUpdateResponse { diff --git a/app/controlplane/api/controlplane/v1/response_messages.pb.go b/app/controlplane/api/controlplane/v1/response_messages.pb.go index a8f908449..939318e2d 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.pb.go +++ b/app/controlplane/api/controlplane/v1/response_messages.pb.go @@ -1906,8 +1906,10 @@ type OrgItem struct { RestrictContractCreationToOrgAdmins bool `protobuf:"varint,8,opt,name=restrict_contract_creation_to_org_admins,json=restrictContractCreationToOrgAdmins,proto3" json:"restrict_contract_creation_to_org_admins,omitempty"` // Maximum days of inactivity before API tokens are auto-revoked. Absent if disabled. ApiTokenMaxDaysInactive *int32 `protobuf:"varint,9,opt,name=api_token_max_days_inactive,json=apiTokenMaxDaysInactive,proto3,oneof" json:"api_token_max_days_inactive,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Whether AI agent config collection is automatically enabled during attestation init + EnableAiAgentCollector bool `protobuf:"varint,10,opt,name=enable_ai_agent_collector,json=enableAiAgentCollector,proto3" json:"enable_ai_agent_collector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *OrgItem) Reset() { @@ -2003,6 +2005,13 @@ func (x *OrgItem) GetApiTokenMaxDaysInactive() int32 { return 0 } +func (x *OrgItem) GetEnableAiAgentCollector() bool { + if x != nil { + return x.EnableAiAgentCollector + } + return false +} + type CASBackendItem struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -2807,7 +2816,7 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + "\n" + "updated_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x123\n" + - "\x04role\x18\x06 \x01(\x0e2\x1f.controlplane.v1.MembershipRoleR\x04role\"\xa1\x06\n" + + "\x04role\x18\x06 \x01(\x0e2\x1f.controlplane.v1.MembershipRoleR\x04role\"\xdc\x06\n" + "\aOrgItem\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x129\n" + @@ -2819,7 +2828,9 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "\x18policy_allowed_hostnames\x18\x05 \x03(\tR\x16policyAllowedHostnames\x12K\n" + "\"prevent_implicit_workflow_creation\x18\a \x01(\bR\x1fpreventImplicitWorkflowCreation\x12U\n" + "(restrict_contract_creation_to_org_admins\x18\b \x01(\bR#restrictContractCreationToOrgAdmins\x12A\n" + - "\x1bapi_token_max_days_inactive\x18\t \x01(\x05H\x00R\x17apiTokenMaxDaysInactive\x88\x01\x01\"\xb4\x01\n" + + "\x1bapi_token_max_days_inactive\x18\t \x01(\x05H\x00R\x17apiTokenMaxDaysInactive\x88\x01\x01\x129\n" + + "\x19enable_ai_agent_collector\x18\n" + + " \x01(\bR\x16enableAiAgentCollector\"\xb4\x01\n" + "\x1fPolicyViolationBlockingStrategy\x122\n" + ".POLICY_VIOLATION_BLOCKING_STRATEGY_UNSPECIFIED\x10\x00\x12,\n" + "(POLICY_VIOLATION_BLOCKING_STRATEGY_BLOCK\x10\x01\x12/\n" + diff --git a/app/controlplane/api/controlplane/v1/response_messages.proto b/app/controlplane/api/controlplane/v1/response_messages.proto index 9400a68c8..d31b62472 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.proto +++ b/app/controlplane/api/controlplane/v1/response_messages.proto @@ -288,6 +288,8 @@ message OrgItem { bool restrict_contract_creation_to_org_admins = 8; // Maximum days of inactivity before API tokens are auto-revoked. Absent if disabled. optional int32 api_token_max_days_inactive = 9; + // Whether AI agent config collection is automatically enabled during attestation init + bool enable_ai_agent_collector = 10; enum PolicyViolationBlockingStrategy { POLICY_VIOLATION_BLOCKING_STRATEGY_UNSPECIFIED = 0; diff --git a/app/controlplane/api/controlplane/v1/workflow_run.pb.go b/app/controlplane/api/controlplane/v1/workflow_run.pb.go index 48e1cd759..38f48d79d 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_run.pb.go @@ -1386,8 +1386,10 @@ type AttestationServiceInitResponse_Result struct { PoliciesAllowedHostnames []string `protobuf:"bytes,6,rep,name=policies_allowed_hostnames,json=policiesAllowedHostnames,proto3" json:"policies_allowed_hostnames,omitempty"` // URL pointing to the web dashboard. It might be empty if not available UiDashboardUrl string `protobuf:"bytes,7,opt,name=ui_dashboard_url,json=uiDashboardUrl,proto3" json:"ui_dashboard_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Whether AI agent config collection is enabled at the org level + EnableAiAgentCollector bool `protobuf:"varint,8,opt,name=enable_ai_agent_collector,json=enableAiAgentCollector,proto3" json:"enable_ai_agent_collector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttestationServiceInitResponse_Result) Reset() { @@ -1462,6 +1464,13 @@ func (x *AttestationServiceInitResponse_Result) GetUiDashboardUrl() string { return "" } +func (x *AttestationServiceInitResponse_Result) GetEnableAiAgentCollector() bool { + if x != nil { + return x.EnableAiAgentCollector + } + return false +} + type AttestationServiceInitResponse_SigningOptions struct { state protoimpl.MessageState `protogen:"open.v1"` // TSA service to be used for signing @@ -1783,16 +1792,17 @@ const file_controlplane_v1_workflow_run_proto_rawDesc = "" + "\rworkflow_name\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\fworkflowName\x12*\n" + "\fproject_name\x18\x05 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\vprojectName\x12'\n" + "\x0fproject_version\x18\x06 \x01(\tR\x0eprojectVersion\x128\n" + - "\x18require_existing_version\x18\a \x01(\bR\x16requireExistingVersion\"\xd9\x04\n" + + "\x18require_existing_version\x18\a \x01(\bR\x16requireExistingVersion\"\x94\x05\n" + "\x1eAttestationServiceInitResponse\x12N\n" + - "\x06result\x18\x01 \x01(\v26.controlplane.v1.AttestationServiceInitResponse.ResultR\x06result\x1a\xfd\x02\n" + + "\x06result\x18\x01 \x01(\v26.controlplane.v1.AttestationServiceInitResponse.ResultR\x06result\x1a\xb8\x03\n" + "\x06Result\x12C\n" + "\fworkflow_run\x18\x02 \x01(\v2 .controlplane.v1.WorkflowRunItemR\vworkflowRun\x12\"\n" + "\forganization\x18\x03 \x01(\tR\forganization\x129\n" + "\x19block_on_policy_violation\x18\x04 \x01(\bR\x16blockOnPolicyViolation\x12g\n" + "\x0fsigning_options\x18\x05 \x01(\v2>.controlplane.v1.AttestationServiceInitResponse.SigningOptionsR\x0esigningOptions\x12<\n" + "\x1apolicies_allowed_hostnames\x18\x06 \x03(\tR\x18policiesAllowedHostnames\x12(\n" + - "\x10ui_dashboard_url\x18\a \x01(\tR\x0euiDashboardUrl\x1ag\n" + + "\x10ui_dashboard_url\x18\a \x01(\tR\x0euiDashboardUrl\x129\n" + + "\x19enable_ai_agent_collector\x18\b \x01(\bR\x16enableAiAgentCollector\x1ag\n" + "\x0eSigningOptions\x126\n" + "\x17timestamp_authority_url\x18\x01 \x01(\tR\x15timestampAuthorityUrl\x12\x1d\n" + "\n" + diff --git a/app/controlplane/api/controlplane/v1/workflow_run.proto b/app/controlplane/api/controlplane/v1/workflow_run.proto index f4f30fcb1..7b1decfe3 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run.proto +++ b/app/controlplane/api/controlplane/v1/workflow_run.proto @@ -141,6 +141,8 @@ message AttestationServiceInitResponse { repeated string policies_allowed_hostnames = 6; // URL pointing to the web dashboard. It might be empty if not available string ui_dashboard_url = 7; + // Whether AI agent config collection is enabled at the org level + bool enable_ai_agent_collector = 8; } message SigningOptions { diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/organization.ts b/app/controlplane/api/gen/frontend/controlplane/v1/organization.ts index 3cd42d9f5..f08e7f155 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/organization.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/organization.ts @@ -85,7 +85,11 @@ export interface OrganizationServiceUpdateRequest { | boolean | undefined; /** Maximum days of inactivity before API tokens are auto-revoked. Set to 0 to disable. */ - apiTokenMaxDaysInactive?: number | undefined; + apiTokenMaxDaysInactive?: + | number + | undefined; + /** Enable automatic AI agent config collection during attestation init */ + enableAiAgentCollector?: boolean | undefined; } export interface OrganizationServiceUpdateResponse { @@ -676,6 +680,7 @@ function createBaseOrganizationServiceUpdateRequest(): OrganizationServiceUpdate preventImplicitWorkflowCreation: undefined, restrictContractCreationToOrgAdmins: undefined, apiTokenMaxDaysInactive: undefined, + enableAiAgentCollector: undefined, }; } @@ -702,6 +707,9 @@ export const OrganizationServiceUpdateRequest = { if (message.apiTokenMaxDaysInactive !== undefined) { writer.uint32(56).int32(message.apiTokenMaxDaysInactive); } + if (message.enableAiAgentCollector !== undefined) { + writer.uint32(64).bool(message.enableAiAgentCollector); + } return writer; }, @@ -761,6 +769,13 @@ export const OrganizationServiceUpdateRequest = { message.apiTokenMaxDaysInactive = reader.int32(); continue; + case 8: + if (tag !== 64) { + break; + } + + message.enableAiAgentCollector = reader.bool(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -789,6 +804,7 @@ export const OrganizationServiceUpdateRequest = { apiTokenMaxDaysInactive: isSet(object.apiTokenMaxDaysInactive) ? Number(object.apiTokenMaxDaysInactive) : undefined, + enableAiAgentCollector: isSet(object.enableAiAgentCollector) ? Boolean(object.enableAiAgentCollector) : undefined, }; }, @@ -809,6 +825,7 @@ export const OrganizationServiceUpdateRequest = { (obj.restrictContractCreationToOrgAdmins = message.restrictContractCreationToOrgAdmins); message.apiTokenMaxDaysInactive !== undefined && (obj.apiTokenMaxDaysInactive = Math.round(message.apiTokenMaxDaysInactive)); + message.enableAiAgentCollector !== undefined && (obj.enableAiAgentCollector = message.enableAiAgentCollector); return obj; }, @@ -829,6 +846,7 @@ export const OrganizationServiceUpdateRequest = { message.preventImplicitWorkflowCreation = object.preventImplicitWorkflowCreation ?? undefined; message.restrictContractCreationToOrgAdmins = object.restrictContractCreationToOrgAdmins ?? undefined; message.apiTokenMaxDaysInactive = object.apiTokenMaxDaysInactive ?? undefined; + message.enableAiAgentCollector = object.enableAiAgentCollector ?? undefined; return message; }, }; diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts index 7ce129b9c..f906d87fb 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts @@ -614,7 +614,11 @@ export interface OrgItem { /** restrict_contract_creation_to_org_admins restricts contract creation (org-level and project-level) to only organization admins (owner/admin roles) */ restrictContractCreationToOrgAdmins: boolean; /** Maximum days of inactivity before API tokens are auto-revoked. Absent if disabled. */ - apiTokenMaxDaysInactive?: number | undefined; + apiTokenMaxDaysInactive?: + | number + | undefined; + /** Whether AI agent config collection is automatically enabled during attestation init */ + enableAiAgentCollector: boolean; } export enum OrgItem_PolicyViolationBlockingStrategy { @@ -3815,6 +3819,7 @@ function createBaseOrgItem(): OrgItem { preventImplicitWorkflowCreation: false, restrictContractCreationToOrgAdmins: false, apiTokenMaxDaysInactive: undefined, + enableAiAgentCollector: false, }; } @@ -3847,6 +3852,9 @@ export const OrgItem = { if (message.apiTokenMaxDaysInactive !== undefined) { writer.uint32(72).int32(message.apiTokenMaxDaysInactive); } + if (message.enableAiAgentCollector === true) { + writer.uint32(80).bool(message.enableAiAgentCollector); + } return writer; }, @@ -3920,6 +3928,13 @@ export const OrgItem = { message.apiTokenMaxDaysInactive = reader.int32(); continue; + case 10: + if (tag !== 80) { + break; + } + + message.enableAiAgentCollector = reader.bool(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -3950,6 +3965,7 @@ export const OrgItem = { apiTokenMaxDaysInactive: isSet(object.apiTokenMaxDaysInactive) ? Number(object.apiTokenMaxDaysInactive) : undefined, + enableAiAgentCollector: isSet(object.enableAiAgentCollector) ? Boolean(object.enableAiAgentCollector) : false, }; }, @@ -3974,6 +3990,7 @@ export const OrgItem = { (obj.restrictContractCreationToOrgAdmins = message.restrictContractCreationToOrgAdmins); message.apiTokenMaxDaysInactive !== undefined && (obj.apiTokenMaxDaysInactive = Math.round(message.apiTokenMaxDaysInactive)); + message.enableAiAgentCollector !== undefined && (obj.enableAiAgentCollector = message.enableAiAgentCollector); return obj; }, @@ -3992,6 +4009,7 @@ export const OrgItem = { message.preventImplicitWorkflowCreation = object.preventImplicitWorkflowCreation ?? false; message.restrictContractCreationToOrgAdmins = object.restrictContractCreationToOrgAdmins ?? false; message.apiTokenMaxDaysInactive = object.apiTokenMaxDaysInactive ?? undefined; + message.enableAiAgentCollector = object.enableAiAgentCollector ?? false; return message; }, }; diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts index 89f3740ae..aabc5cacc 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts @@ -117,6 +117,8 @@ export interface AttestationServiceInitResponse_Result { policiesAllowedHostnames: string[]; /** URL pointing to the web dashboard. It might be empty if not available */ uiDashboardUrl: string; + /** Whether AI agent config collection is enabled at the org level */ + enableAiAgentCollector: boolean; } export interface AttestationServiceInitResponse_SigningOptions { @@ -1285,6 +1287,7 @@ function createBaseAttestationServiceInitResponse_Result(): AttestationServiceIn signingOptions: undefined, policiesAllowedHostnames: [], uiDashboardUrl: "", + enableAiAgentCollector: false, }; } @@ -1308,6 +1311,9 @@ export const AttestationServiceInitResponse_Result = { if (message.uiDashboardUrl !== "") { writer.uint32(58).string(message.uiDashboardUrl); } + if (message.enableAiAgentCollector === true) { + writer.uint32(64).bool(message.enableAiAgentCollector); + } return writer; }, @@ -1360,6 +1366,13 @@ export const AttestationServiceInitResponse_Result = { message.uiDashboardUrl = reader.string(); continue; + case 8: + if (tag !== 64) { + break; + } + + message.enableAiAgentCollector = reader.bool(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1381,6 +1394,7 @@ export const AttestationServiceInitResponse_Result = { ? object.policiesAllowedHostnames.map((e: any) => String(e)) : [], uiDashboardUrl: isSet(object.uiDashboardUrl) ? String(object.uiDashboardUrl) : "", + enableAiAgentCollector: isSet(object.enableAiAgentCollector) ? Boolean(object.enableAiAgentCollector) : false, }; }, @@ -1399,6 +1413,7 @@ export const AttestationServiceInitResponse_Result = { obj.policiesAllowedHostnames = []; } message.uiDashboardUrl !== undefined && (obj.uiDashboardUrl = message.uiDashboardUrl); + message.enableAiAgentCollector !== undefined && (obj.enableAiAgentCollector = message.enableAiAgentCollector); return obj; }, @@ -1422,6 +1437,7 @@ export const AttestationServiceInitResponse_Result = { : undefined; message.policiesAllowedHostnames = object.policiesAllowedHostnames?.map((e) => e) || []; message.uiDashboardUrl = object.uiDashboardUrl ?? ""; + message.enableAiAgentCollector = object.enableAiAgentCollector ?? false; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitResponse.Result.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitResponse.Result.jsonschema.json index d6421f5c6..c8b111c40 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitResponse.Result.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitResponse.Result.jsonschema.json @@ -7,6 +7,10 @@ "description": "fail the attestation if there is a violation in any policy", "type": "boolean" }, + "^(enable_ai_agent_collector)$": { + "description": "Whether AI agent config collection is enabled at the org level", + "type": "boolean" + }, "^(policies_allowed_hostnames)$": { "description": "array of hostnames that are allowed to be used in the policies", "items": { @@ -31,6 +35,10 @@ "description": "fail the attestation if there is a violation in any policy", "type": "boolean" }, + "enableAiAgentCollector": { + "description": "Whether AI agent config collection is enabled at the org level", + "type": "boolean" + }, "organization": { "description": "organization name", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitResponse.Result.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitResponse.Result.schema.json index 1a86b4c67..76901c13d 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitResponse.Result.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitResponse.Result.schema.json @@ -7,6 +7,10 @@ "description": "fail the attestation if there is a violation in any policy", "type": "boolean" }, + "^(enableAiAgentCollector)$": { + "description": "Whether AI agent config collection is enabled at the org level", + "type": "boolean" + }, "^(policiesAllowedHostnames)$": { "description": "array of hostnames that are allowed to be used in the policies", "items": { @@ -31,6 +35,10 @@ "description": "fail the attestation if there is a violation in any policy", "type": "boolean" }, + "enable_ai_agent_collector": { + "description": "Whether AI agent config collection is enabled at the org level", + "type": "boolean" + }, "organization": { "description": "organization name", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgItem.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgItem.jsonschema.json index c05952695..13d7cc327 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgItem.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgItem.jsonschema.json @@ -30,6 +30,10 @@ } ] }, + "^(enable_ai_agent_collector)$": { + "description": "Whether AI agent config collection is automatically enabled during attestation init", + "type": "boolean" + }, "^(policy_allowed_hostnames)$": { "items": { "type": "string" @@ -76,6 +80,10 @@ } ] }, + "enableAiAgentCollector": { + "description": "Whether AI agent config collection is automatically enabled during attestation init", + "type": "boolean" + }, "id": { "type": "string" }, diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgItem.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgItem.schema.json index fcd07cd3e..49ae180b9 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgItem.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgItem.schema.json @@ -30,6 +30,10 @@ } ] }, + "^(enableAiAgentCollector)$": { + "description": "Whether AI agent config collection is automatically enabled during attestation init", + "type": "boolean" + }, "^(policyAllowedHostnames)$": { "items": { "type": "string" @@ -76,6 +80,10 @@ } ] }, + "enable_ai_agent_collector": { + "description": "Whether AI agent config collection is automatically enabled during attestation init", + "type": "boolean" + }, "id": { "type": "string" }, diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateRequest.jsonschema.json index fbe8d2ad9..836ead451 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateRequest.jsonschema.json @@ -13,6 +13,10 @@ "description": "\"optional\" allow us to detect if the value is explicitly set", "type": "boolean" }, + "^(enable_ai_agent_collector)$": { + "description": "Enable automatic AI agent config collection during attestation init", + "type": "boolean" + }, "^(policies_allowed_hostnames)$": { "description": "array of hostnames that are allowed to be used in the policies", "items": { @@ -44,6 +48,10 @@ "description": "\"optional\" allow us to detect if the value is explicitly set", "type": "boolean" }, + "enableAiAgentCollector": { + "description": "Enable automatic AI agent config collection during attestation init", + "type": "boolean" + }, "name": { "minLength": 1, "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateRequest.schema.json index c8709352c..f7e8e9f4f 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateRequest.schema.json @@ -13,6 +13,10 @@ "description": "\"optional\" allow us to detect if the value is explicitly set", "type": "boolean" }, + "^(enableAiAgentCollector)$": { + "description": "Enable automatic AI agent config collection during attestation init", + "type": "boolean" + }, "^(policiesAllowedHostnames)$": { "description": "array of hostnames that are allowed to be used in the policies", "items": { @@ -44,6 +48,10 @@ "description": "\"optional\" allow us to detect if the value is explicitly set", "type": "boolean" }, + "enable_ai_agent_collector": { + "description": "Enable automatic AI agent config collection during attestation init", + "type": "boolean" + }, "name": { "minLength": 1, "type": "string" diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index c713ebbc3..d56037d5c 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -213,6 +213,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer BlockOnPolicyViolation: org.BlockOnPolicyViolation, PoliciesAllowedHostnames: org.PoliciesAllowedHostnames, UiDashboardUrl: s.bootstrapConfig.UiDashboardUrl, + EnableAiAgentCollector: org.EnableAIAgentCollector, } resp.SigningOptions = &cpAPI.AttestationServiceInitResponse_SigningOptions{} diff --git a/app/controlplane/internal/service/context.go b/app/controlplane/internal/service/context.go index cd8e56849..b24c91b8c 100644 --- a/app/controlplane/internal/service/context.go +++ b/app/controlplane/internal/service/context.go @@ -125,6 +125,7 @@ func bizOrgToPb(m *biz.Organization) *pb.OrgItem { PolicyAllowedHostnames: m.PoliciesAllowedHostnames, PreventImplicitWorkflowCreation: m.PreventImplicitWorkflowCreation, RestrictContractCreationToOrgAdmins: m.RestrictContractCreationToOrgAdmins, + EnableAiAgentCollector: m.EnableAIAgentCollector, } if m.APITokenInactivityThresholdDays != nil { diff --git a/app/controlplane/internal/service/organization.go b/app/controlplane/internal/service/organization.go index 7242de52e..84020eeff 100644 --- a/app/controlplane/internal/service/organization.go +++ b/app/controlplane/internal/service/organization.go @@ -102,7 +102,14 @@ func (s *OrganizationService) Update(ctx context.Context, req *pb.OrganizationSe apiTokenMaxDaysInactive = &days } - org, err := s.orgUC.Update(ctx, currentUser.ID, req.Name, req.BlockOnPolicyViolation, policiesAllowedHostnames, req.PreventImplicitWorkflowCreation, req.RestrictContractCreationToOrgAdmins, apiTokenMaxDaysInactive) + org, err := s.orgUC.Update(ctx, currentUser.ID, req.Name, &biz.OrganizationUpdateOpts{ + BlockOnPolicyViolation: req.BlockOnPolicyViolation, + PoliciesAllowedHostnames: policiesAllowedHostnames, + PreventImplicitWorkflowCreation: req.PreventImplicitWorkflowCreation, + RestrictContractCreationToOrgAdmins: req.RestrictContractCreationToOrgAdmins, + APITokenInactivityThresholdDays: apiTokenMaxDaysInactive, + EnableAIAgentCollector: req.EnableAiAgentCollector, + }) if err != nil { return nil, handleUseCaseErr(err, s.log) } diff --git a/app/controlplane/pkg/biz/apitoken_stale_revoker_integration_test.go b/app/controlplane/pkg/biz/apitoken_stale_revoker_integration_test.go index d7604df19..fef122a94 100644 --- a/app/controlplane/pkg/biz/apitoken_stale_revoker_integration_test.go +++ b/app/controlplane/pkg/biz/apitoken_stale_revoker_integration_test.go @@ -199,7 +199,9 @@ func (s *staleRevokerTestSuite) createOrgWithThreshold(ctx context.Context, days _, err = s.Membership.Create(ctx, org.ID, s.user.ID, biz.WithCurrentMembership()) require.NoError(s.T(), err) - org, err = s.Organization.Update(ctx, s.user.ID, org.Name, nil, nil, nil, nil, &days) + org, err = s.Organization.Update(ctx, s.user.ID, org.Name, &biz.OrganizationUpdateOpts{ + APITokenInactivityThresholdDays: &days, + }) require.NoError(s.T(), err) require.NotNil(s.T(), org.APITokenInactivityThresholdDays) assert.Equal(s.T(), days, *org.APITokenInactivityThresholdDays) diff --git a/app/controlplane/pkg/biz/mocks/OrganizationRepo.go b/app/controlplane/pkg/biz/mocks/OrganizationRepo.go index 895534853..1f1f2b24e 100644 --- a/app/controlplane/pkg/biz/mocks/OrganizationRepo.go +++ b/app/controlplane/pkg/biz/mocks/OrganizationRepo.go @@ -363,8 +363,8 @@ func (_c *OrganizationRepo_FindWithTokenInactivityThreshold_Call) RunAndReturn(r } // Update provides a mock function for the type OrganizationRepo -func (_mock *OrganizationRepo) Update(ctx context.Context, id uuid.UUID, blockOnPolicyViolation *bool, policiesAllowedHostnames []string, preventImplicitWorkflowCreation *bool, restrictContractCreationToOrgAdmins *bool, apiTokenInactivityThresholdDays *int) (*biz.Organization, error) { - ret := _mock.Called(ctx, id, blockOnPolicyViolation, policiesAllowedHostnames, preventImplicitWorkflowCreation, restrictContractCreationToOrgAdmins, apiTokenInactivityThresholdDays) +func (_mock *OrganizationRepo) Update(ctx context.Context, id uuid.UUID, opts *biz.OrganizationUpdateOpts) (*biz.Organization, error) { + ret := _mock.Called(ctx, id, opts) if len(ret) == 0 { panic("no return value specified for Update") @@ -372,18 +372,18 @@ func (_mock *OrganizationRepo) Update(ctx context.Context, id uuid.UUID, blockOn var r0 *biz.Organization var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, *bool, []string, *bool, *bool, *int) (*biz.Organization, error)); ok { - return returnFunc(ctx, id, blockOnPolicyViolation, policiesAllowedHostnames, preventImplicitWorkflowCreation, restrictContractCreationToOrgAdmins, apiTokenInactivityThresholdDays) + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, *biz.OrganizationUpdateOpts) (*biz.Organization, error)); ok { + return returnFunc(ctx, id, opts) } - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, *bool, []string, *bool, *bool, *int) *biz.Organization); ok { - r0 = returnFunc(ctx, id, blockOnPolicyViolation, policiesAllowedHostnames, preventImplicitWorkflowCreation, restrictContractCreationToOrgAdmins, apiTokenInactivityThresholdDays) + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, *biz.OrganizationUpdateOpts) *biz.Organization); ok { + r0 = returnFunc(ctx, id, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*biz.Organization) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID, *bool, []string, *bool, *bool, *int) error); ok { - r1 = returnFunc(ctx, id, blockOnPolicyViolation, policiesAllowedHostnames, preventImplicitWorkflowCreation, restrictContractCreationToOrgAdmins, apiTokenInactivityThresholdDays) + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID, *biz.OrganizationUpdateOpts) error); ok { + r1 = returnFunc(ctx, id, opts) } else { r1 = ret.Error(1) } @@ -398,16 +398,12 @@ type OrganizationRepo_Update_Call struct { // Update is a helper method to define mock.On call // - ctx context.Context // - id uuid.UUID -// - blockOnPolicyViolation *bool -// - policiesAllowedHostnames []string -// - preventImplicitWorkflowCreation *bool -// - restrictContractCreationToOrgAdmins *bool -// - apiTokenInactivityThresholdDays *int -func (_e *OrganizationRepo_Expecter) Update(ctx interface{}, id interface{}, blockOnPolicyViolation interface{}, policiesAllowedHostnames interface{}, preventImplicitWorkflowCreation interface{}, restrictContractCreationToOrgAdmins interface{}, apiTokenInactivityThresholdDays interface{}) *OrganizationRepo_Update_Call { - return &OrganizationRepo_Update_Call{Call: _e.mock.On("Update", ctx, id, blockOnPolicyViolation, policiesAllowedHostnames, preventImplicitWorkflowCreation, restrictContractCreationToOrgAdmins, apiTokenInactivityThresholdDays)} +// - opts *biz.OrganizationUpdateOpts +func (_e *OrganizationRepo_Expecter) Update(ctx interface{}, id interface{}, opts interface{}) *OrganizationRepo_Update_Call { + return &OrganizationRepo_Update_Call{Call: _e.mock.On("Update", ctx, id, opts)} } -func (_c *OrganizationRepo_Update_Call) Run(run func(ctx context.Context, id uuid.UUID, blockOnPolicyViolation *bool, policiesAllowedHostnames []string, preventImplicitWorkflowCreation *bool, restrictContractCreationToOrgAdmins *bool, apiTokenInactivityThresholdDays *int)) *OrganizationRepo_Update_Call { +func (_c *OrganizationRepo_Update_Call) Run(run func(ctx context.Context, id uuid.UUID, opts *biz.OrganizationUpdateOpts)) *OrganizationRepo_Update_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -417,34 +413,14 @@ func (_c *OrganizationRepo_Update_Call) Run(run func(ctx context.Context, id uui if args[1] != nil { arg1 = args[1].(uuid.UUID) } - var arg2 *bool + var arg2 *biz.OrganizationUpdateOpts if args[2] != nil { - arg2 = args[2].(*bool) - } - var arg3 []string - if args[3] != nil { - arg3 = args[3].([]string) - } - var arg4 *bool - if args[4] != nil { - arg4 = args[4].(*bool) - } - var arg5 *bool - if args[5] != nil { - arg5 = args[5].(*bool) - } - var arg6 *int - if args[6] != nil { - arg6 = args[6].(*int) + arg2 = args[2].(*biz.OrganizationUpdateOpts) } run( arg0, arg1, arg2, - arg3, - arg4, - arg5, - arg6, ) }) return _c @@ -455,7 +431,7 @@ func (_c *OrganizationRepo_Update_Call) Return(organization *biz.Organization, e return _c } -func (_c *OrganizationRepo_Update_Call) RunAndReturn(run func(ctx context.Context, id uuid.UUID, blockOnPolicyViolation *bool, policiesAllowedHostnames []string, preventImplicitWorkflowCreation *bool, restrictContractCreationToOrgAdmins *bool, apiTokenInactivityThresholdDays *int) (*biz.Organization, error)) *OrganizationRepo_Update_Call { +func (_c *OrganizationRepo_Update_Call) RunAndReturn(run func(ctx context.Context, id uuid.UUID, opts *biz.OrganizationUpdateOpts) (*biz.Organization, error)) *OrganizationRepo_Update_Call { _c.Call.Return(run) return _c } diff --git a/app/controlplane/pkg/biz/organization.go b/app/controlplane/pkg/biz/organization.go index cc673a74a..fb1a06cb9 100644 --- a/app/controlplane/pkg/biz/organization.go +++ b/app/controlplane/pkg/biz/organization.go @@ -46,13 +46,27 @@ type Organization struct { // APITokenInactivityThresholdDays is the number of days after which inactive API tokens are auto-revoked. // nil means the feature is disabled. APITokenInactivityThresholdDays *int + // EnableAIAgentCollector enables automatic AI agent config collection during attestation init + EnableAIAgentCollector bool +} + +// OrganizationUpdateOpts holds optional fields for updating an organization. +// Pointer fields use nil to indicate "no change". For PoliciesAllowedHostnames, +// nil means "no change" while an empty slice means "clear the list". +type OrganizationUpdateOpts struct { + BlockOnPolicyViolation *bool + PoliciesAllowedHostnames []string + PreventImplicitWorkflowCreation *bool + RestrictContractCreationToOrgAdmins *bool + APITokenInactivityThresholdDays *int + EnableAIAgentCollector *bool } type OrganizationRepo interface { FindByID(ctx context.Context, orgID uuid.UUID) (*Organization, error) FindByName(ctx context.Context, name string) (*Organization, error) Create(ctx context.Context, name string) (*Organization, error) - Update(ctx context.Context, id uuid.UUID, blockOnPolicyViolation *bool, policiesAllowedHostnames []string, preventImplicitWorkflowCreation *bool, restrictContractCreationToOrgAdmins *bool, apiTokenInactivityThresholdDays *int) (*Organization, error) + Update(ctx context.Context, id uuid.UUID, opts *OrganizationUpdateOpts) (*Organization, error) Delete(ctx context.Context, ID uuid.UUID) error // FindWithTokenInactivityThreshold returns orgs that have api_token_inactivity_threshold_days set (non-nil). FindWithTokenInactivityThreshold(ctx context.Context) ([]*Organization, error) @@ -194,7 +208,11 @@ func (uc *OrganizationUseCase) doCreate(ctx context.Context, name string, opts . return org, nil } -func (uc *OrganizationUseCase) Update(ctx context.Context, userID, orgName string, blockOnPolicyViolation *bool, policiesAllowedHostnames []string, preventImplicitWorkflowCreation *bool, restrictContractCreationToOrgAdmins *bool, apiTokenInactivityThresholdDays *int) (*Organization, error) { +func (uc *OrganizationUseCase) Update(ctx context.Context, userID, orgName string, opts *OrganizationUpdateOpts) (*Organization, error) { + if opts == nil { + opts = &OrganizationUpdateOpts{} + } + userUUID, err := uuid.Parse(userID) if err != nil { return nil, NewErrInvalidUUID(err) @@ -214,7 +232,7 @@ func (uc *OrganizationUseCase) Update(ctx context.Context, userID, orgName strin } // Perform the update - org, err := uc.orgRepo.Update(ctx, orgUUID, blockOnPolicyViolation, policiesAllowedHostnames, preventImplicitWorkflowCreation, restrictContractCreationToOrgAdmins, apiTokenInactivityThresholdDays) + org, err := uc.orgRepo.Update(ctx, orgUUID, opts) if err != nil { return nil, fmt.Errorf("failed to update organization: %w", err) } else if org == nil { diff --git a/app/controlplane/pkg/biz/organization_integration_test.go b/app/controlplane/pkg/biz/organization_integration_test.go index 2f3ae0659..0cc0dc370 100644 --- a/app/controlplane/pkg/biz/organization_integration_test.go +++ b/app/controlplane/pkg/biz/organization_integration_test.go @@ -118,7 +118,7 @@ func (s *OrgIntegrationTestSuite) TestUpdate() { s.Run("org non existent", func() { // org not found - _, err := s.Organization.Update(ctx, s.user.ID, uuid.NewString(), nil, nil, nil, nil, nil) + _, err := s.Organization.Update(ctx, s.user.ID, uuid.NewString(), &biz.OrganizationUpdateOpts{}) s.Error(err) s.True(biz.IsNotFound(err)) }) @@ -126,35 +126,57 @@ func (s *OrgIntegrationTestSuite) TestUpdate() { s.Run("org not accessible to user", func() { org2, err := s.Organization.CreateWithRandomName(ctx) require.NoError(s.T(), err) - _, err = s.Organization.Update(ctx, s.user.ID, org2.Name, nil, nil, nil, nil, nil) + _, err = s.Organization.Update(ctx, s.user.ID, org2.Name, &biz.OrganizationUpdateOpts{}) s.Error(err) s.True(biz.IsNotFound(err)) }) s.Run("valid block on policy violation update", func() { - got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, toPtrBool(true), nil, nil, nil, nil) + got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, &biz.OrganizationUpdateOpts{ + BlockOnPolicyViolation: toPtrBool(true), + }) s.NoError(err) s.True(got.BlockOnPolicyViolation) }) s.Run("valid policy allowed hostnames update", func() { - got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, nil, []string{"foo.com", "bar.com"}, nil, nil, nil) + got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, &biz.OrganizationUpdateOpts{ + PoliciesAllowedHostnames: []string{"foo.com", "bar.com"}, + }) s.NoError(err) s.Equal([]string{"foo.com", "bar.com"}, got.PoliciesAllowedHostnames) }) s.Run("clear policy allowed hostnames", func() { - got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, nil, []string{}, nil, nil, nil) + got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, &biz.OrganizationUpdateOpts{ + PoliciesAllowedHostnames: []string{}, + }) s.NoError(err) s.Equal([]string{}, got.PoliciesAllowedHostnames) }) + s.Run("valid enable AI agent collector update", func() { + got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, &biz.OrganizationUpdateOpts{ + EnableAIAgentCollector: toPtrBool(true), + }) + s.NoError(err) + s.True(got.EnableAIAgentCollector) + + got, err = s.Organization.Update(ctx, s.user.ID, s.org.Name, &biz.OrganizationUpdateOpts{ + EnableAIAgentCollector: toPtrBool(false), + }) + s.NoError(err) + s.False(got.EnableAIAgentCollector) + }) + s.Run("but not passing a value doesn't clear the hostnames value", func() { - got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, nil, []string{"foo.com", "bar.com"}, nil, nil, nil) + got, err := s.Organization.Update(ctx, s.user.ID, s.org.Name, &biz.OrganizationUpdateOpts{ + PoliciesAllowedHostnames: []string{"foo.com", "bar.com"}, + }) s.NoError(err) s.Equal([]string{"foo.com", "bar.com"}, got.PoliciesAllowedHostnames) - got, err = s.Organization.Update(ctx, s.user.ID, s.org.Name, nil, nil, nil, nil, nil) + got, err = s.Organization.Update(ctx, s.user.ID, s.org.Name, &biz.OrganizationUpdateOpts{}) s.NoError(err) s.Equal([]string{"foo.com", "bar.com"}, got.PoliciesAllowedHostnames) }) diff --git a/app/controlplane/pkg/biz/workflow_integration_test.go b/app/controlplane/pkg/biz/workflow_integration_test.go index 467da0187..fdc3b7581 100644 --- a/app/controlplane/pkg/biz/workflow_integration_test.go +++ b/app/controlplane/pkg/biz/workflow_integration_test.go @@ -187,7 +187,9 @@ func (s *workflowIntegrationTestSuite) TestCreate() { // Enable implicit workflow creation prevention for testing orgID, _ := uuid.Parse(s.org.ID) - _, err = s.Repos.OrganizationRepo.Update(ctx, orgID, nil, nil, toPtrBool(true), nil, nil) + _, err = s.Repos.OrganizationRepo.Update(ctx, orgID, &biz.OrganizationUpdateOpts{ + PreventImplicitWorkflowCreation: toPtrBool(true), + }) s.Require().NoError(err) for _, tc := range testCases { diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20260318160301.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20260318160301.sql new file mode 100644 index 000000000..a6cfb3fbb --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20260318160301.sql @@ -0,0 +1,2 @@ +-- Modify "organizations" table +ALTER TABLE "organizations" ADD COLUMN "enable_ai_agent_collector" boolean NOT NULL DEFAULT false; diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 0285ca20a..7a9b904e4 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:SDVo/094SOGNvK1KK35O4KpUY78MuSTZljVN58UGkO0= +h1:POeLVZ5wn0luHTIuCxBdUZBNVPEq0gfgfoaNFgQzR4g= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -126,3 +126,4 @@ h1:SDVo/094SOGNvK1KK35O4KpUY78MuSTZljVN58UGkO0= 20260204113827.sql h1:rlJNf8QRfqOfDHf2GUi+59Rgv2BkSbMTPuMalPsMkZg= 20260211225609.sql h1:DTkyg3oZSV99uPGl+vOuK9FSlEumXwoYCgchUhsg/P4= 20260303120000.sql h1:msXy2MRkzMOGxWbG1NOHh+PN5qjaBZcRzVT+7SFIwaA= +20260318160301.sql h1:kH88s6pOi7Vprydb7xrzgY55JhMxfzY32txpQ8a1wEE= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index 849c7ac70..3e59365a6 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -433,6 +433,7 @@ var ( {Name: "prevent_implicit_workflow_creation", Type: field.TypeBool, Default: false}, {Name: "restrict_contract_creation_to_org_admins", Type: field.TypeBool, Default: false}, {Name: "api_token_inactivity_threshold_days", Type: field.TypeInt, Nullable: true}, + {Name: "enable_ai_agent_collector", Type: field.TypeBool, Default: false}, } // OrganizationsTable holds the schema information for the "organizations" table. OrganizationsTable = &schema.Table{ diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index ffff2a12c..6bb19f4d0 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -8748,6 +8748,7 @@ type OrganizationMutation struct { restrict_contract_creation_to_org_admins *bool api_token_inactivity_threshold_days *int addapi_token_inactivity_threshold_days *int + enable_ai_agent_collector *bool clearedFields map[string]struct{} memberships map[uuid.UUID]struct{} removedmemberships map[uuid.UUID]struct{} @@ -9282,6 +9283,42 @@ func (m *OrganizationMutation) ResetAPITokenInactivityThresholdDays() { delete(m.clearedFields, organization.FieldAPITokenInactivityThresholdDays) } +// SetEnableAiAgentCollector sets the "enable_ai_agent_collector" field. +func (m *OrganizationMutation) SetEnableAiAgentCollector(b bool) { + m.enable_ai_agent_collector = &b +} + +// EnableAiAgentCollector returns the value of the "enable_ai_agent_collector" field in the mutation. +func (m *OrganizationMutation) EnableAiAgentCollector() (r bool, exists bool) { + v := m.enable_ai_agent_collector + if v == nil { + return + } + return *v, true +} + +// OldEnableAiAgentCollector returns the old "enable_ai_agent_collector" field's value of the Organization entity. +// If the Organization object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *OrganizationMutation) OldEnableAiAgentCollector(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEnableAiAgentCollector is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEnableAiAgentCollector requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEnableAiAgentCollector: %w", err) + } + return oldValue.EnableAiAgentCollector, nil +} + +// ResetEnableAiAgentCollector resets all changes to the "enable_ai_agent_collector" field. +func (m *OrganizationMutation) ResetEnableAiAgentCollector() { + m.enable_ai_agent_collector = nil +} + // AddMembershipIDs adds the "memberships" edge to the Membership entity by ids. func (m *OrganizationMutation) AddMembershipIDs(ids ...uuid.UUID) { if m.memberships == nil { @@ -9748,7 +9785,7 @@ func (m *OrganizationMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *OrganizationMutation) Fields() []string { - fields := make([]string, 0, 9) + fields := make([]string, 0, 10) if m.name != nil { fields = append(fields, organization.FieldName) } @@ -9776,6 +9813,9 @@ func (m *OrganizationMutation) Fields() []string { if m.api_token_inactivity_threshold_days != nil { fields = append(fields, organization.FieldAPITokenInactivityThresholdDays) } + if m.enable_ai_agent_collector != nil { + fields = append(fields, organization.FieldEnableAiAgentCollector) + } return fields } @@ -9802,6 +9842,8 @@ func (m *OrganizationMutation) Field(name string) (ent.Value, bool) { return m.RestrictContractCreationToOrgAdmins() case organization.FieldAPITokenInactivityThresholdDays: return m.APITokenInactivityThresholdDays() + case organization.FieldEnableAiAgentCollector: + return m.EnableAiAgentCollector() } return nil, false } @@ -9829,6 +9871,8 @@ func (m *OrganizationMutation) OldField(ctx context.Context, name string) (ent.V return m.OldRestrictContractCreationToOrgAdmins(ctx) case organization.FieldAPITokenInactivityThresholdDays: return m.OldAPITokenInactivityThresholdDays(ctx) + case organization.FieldEnableAiAgentCollector: + return m.OldEnableAiAgentCollector(ctx) } return nil, fmt.Errorf("unknown Organization field %s", name) } @@ -9901,6 +9945,13 @@ func (m *OrganizationMutation) SetField(name string, value ent.Value) error { } m.SetAPITokenInactivityThresholdDays(v) return nil + case organization.FieldEnableAiAgentCollector: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEnableAiAgentCollector(v) + return nil } return fmt.Errorf("unknown Organization field %s", name) } @@ -10013,6 +10064,9 @@ func (m *OrganizationMutation) ResetField(name string) error { case organization.FieldAPITokenInactivityThresholdDays: m.ResetAPITokenInactivityThresholdDays() return nil + case organization.FieldEnableAiAgentCollector: + m.ResetEnableAiAgentCollector() + return nil } return fmt.Errorf("unknown Organization field %s", name) } diff --git a/app/controlplane/pkg/data/ent/organization.go b/app/controlplane/pkg/data/ent/organization.go index 908059c3f..83936fc97 100644 --- a/app/controlplane/pkg/data/ent/organization.go +++ b/app/controlplane/pkg/data/ent/organization.go @@ -37,6 +37,8 @@ type Organization struct { RestrictContractCreationToOrgAdmins bool `json:"restrict_contract_creation_to_org_admins,omitempty"` // APITokenInactivityThresholdDays holds the value of the "api_token_inactivity_threshold_days" field. APITokenInactivityThresholdDays *int `json:"api_token_inactivity_threshold_days,omitempty"` + // EnableAiAgentCollector holds the value of the "enable_ai_agent_collector" field. + EnableAiAgentCollector bool `json:"enable_ai_agent_collector,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the OrganizationQuery when eager-loading is set. Edges OrganizationEdges `json:"edges"` @@ -145,7 +147,7 @@ func (*Organization) scanValues(columns []string) ([]any, error) { switch columns[i] { case organization.FieldPoliciesAllowedHostnames: values[i] = new([]byte) - case organization.FieldBlockOnPolicyViolation, organization.FieldPreventImplicitWorkflowCreation, organization.FieldRestrictContractCreationToOrgAdmins: + case organization.FieldBlockOnPolicyViolation, organization.FieldPreventImplicitWorkflowCreation, organization.FieldRestrictContractCreationToOrgAdmins, organization.FieldEnableAiAgentCollector: values[i] = new(sql.NullBool) case organization.FieldAPITokenInactivityThresholdDays: values[i] = new(sql.NullInt64) @@ -233,6 +235,12 @@ func (_m *Organization) assignValues(columns []string, values []any) error { _m.APITokenInactivityThresholdDays = new(int) *_m.APITokenInactivityThresholdDays = int(value.Int64) } + case organization.FieldEnableAiAgentCollector: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field enable_ai_agent_collector", values[i]) + } else if value.Valid { + _m.EnableAiAgentCollector = value.Bool + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -337,6 +345,9 @@ func (_m *Organization) String() string { builder.WriteString("api_token_inactivity_threshold_days=") builder.WriteString(fmt.Sprintf("%v", *v)) } + builder.WriteString(", ") + builder.WriteString("enable_ai_agent_collector=") + builder.WriteString(fmt.Sprintf("%v", _m.EnableAiAgentCollector)) builder.WriteByte(')') return builder.String() } diff --git a/app/controlplane/pkg/data/ent/organization/organization.go b/app/controlplane/pkg/data/ent/organization/organization.go index 07407aec3..a51c4bd11 100644 --- a/app/controlplane/pkg/data/ent/organization/organization.go +++ b/app/controlplane/pkg/data/ent/organization/organization.go @@ -33,6 +33,8 @@ const ( FieldRestrictContractCreationToOrgAdmins = "restrict_contract_creation_to_org_admins" // FieldAPITokenInactivityThresholdDays holds the string denoting the api_token_inactivity_threshold_days field in the database. FieldAPITokenInactivityThresholdDays = "api_token_inactivity_threshold_days" + // FieldEnableAiAgentCollector holds the string denoting the enable_ai_agent_collector field in the database. + FieldEnableAiAgentCollector = "enable_ai_agent_collector" // EdgeMemberships holds the string denoting the memberships edge name in mutations. EdgeMemberships = "memberships" // EdgeWorkflowContracts holds the string denoting the workflow_contracts edge name in mutations. @@ -121,6 +123,7 @@ var Columns = []string{ FieldPreventImplicitWorkflowCreation, FieldRestrictContractCreationToOrgAdmins, FieldAPITokenInactivityThresholdDays, + FieldEnableAiAgentCollector, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -144,6 +147,8 @@ var ( DefaultPreventImplicitWorkflowCreation bool // DefaultRestrictContractCreationToOrgAdmins holds the default value on creation for the "restrict_contract_creation_to_org_admins" field. DefaultRestrictContractCreationToOrgAdmins bool + // DefaultEnableAiAgentCollector holds the default value on creation for the "enable_ai_agent_collector" field. + DefaultEnableAiAgentCollector bool // DefaultID holds the default value on creation for the "id" field. DefaultID func() uuid.UUID ) @@ -196,6 +201,11 @@ func ByAPITokenInactivityThresholdDays(opts ...sql.OrderTermOption) OrderOption return sql.OrderByField(FieldAPITokenInactivityThresholdDays, opts...).ToFunc() } +// ByEnableAiAgentCollector orders the results by the enable_ai_agent_collector field. +func ByEnableAiAgentCollector(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEnableAiAgentCollector, opts...).ToFunc() +} + // ByMembershipsCount orders the results by memberships count. func ByMembershipsCount(opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/organization/where.go b/app/controlplane/pkg/data/ent/organization/where.go index f2823a5d4..36a1abb09 100644 --- a/app/controlplane/pkg/data/ent/organization/where.go +++ b/app/controlplane/pkg/data/ent/organization/where.go @@ -96,6 +96,11 @@ func APITokenInactivityThresholdDays(v int) predicate.Organization { return predicate.Organization(sql.FieldEQ(FieldAPITokenInactivityThresholdDays, v)) } +// EnableAiAgentCollector applies equality check predicate on the "enable_ai_agent_collector" field. It's identical to EnableAiAgentCollectorEQ. +func EnableAiAgentCollector(v bool) predicate.Organization { + return predicate.Organization(sql.FieldEQ(FieldEnableAiAgentCollector, v)) +} + // NameEQ applies the EQ predicate on the "name" field. func NameEQ(v string) predicate.Organization { return predicate.Organization(sql.FieldEQ(FieldName, v)) @@ -381,6 +386,16 @@ func APITokenInactivityThresholdDaysNotNil() predicate.Organization { return predicate.Organization(sql.FieldNotNull(FieldAPITokenInactivityThresholdDays)) } +// EnableAiAgentCollectorEQ applies the EQ predicate on the "enable_ai_agent_collector" field. +func EnableAiAgentCollectorEQ(v bool) predicate.Organization { + return predicate.Organization(sql.FieldEQ(FieldEnableAiAgentCollector, v)) +} + +// EnableAiAgentCollectorNEQ applies the NEQ predicate on the "enable_ai_agent_collector" field. +func EnableAiAgentCollectorNEQ(v bool) predicate.Organization { + return predicate.Organization(sql.FieldNEQ(FieldEnableAiAgentCollector, v)) +} + // HasMemberships applies the HasEdge predicate on the "memberships" edge. func HasMemberships() predicate.Organization { return predicate.Organization(func(s *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/organization_create.go b/app/controlplane/pkg/data/ent/organization_create.go index 3552b9791..bbe8ca725 100644 --- a/app/controlplane/pkg/data/ent/organization_create.go +++ b/app/controlplane/pkg/data/ent/organization_create.go @@ -142,6 +142,20 @@ func (_c *OrganizationCreate) SetNillableAPITokenInactivityThresholdDays(v *int) return _c } +// SetEnableAiAgentCollector sets the "enable_ai_agent_collector" field. +func (_c *OrganizationCreate) SetEnableAiAgentCollector(v bool) *OrganizationCreate { + _c.mutation.SetEnableAiAgentCollector(v) + return _c +} + +// SetNillableEnableAiAgentCollector sets the "enable_ai_agent_collector" field if the given value is not nil. +func (_c *OrganizationCreate) SetNillableEnableAiAgentCollector(v *bool) *OrganizationCreate { + if v != nil { + _c.SetEnableAiAgentCollector(*v) + } + return _c +} + // SetID sets the "id" field. func (_c *OrganizationCreate) SetID(v uuid.UUID) *OrganizationCreate { _c.mutation.SetID(v) @@ -331,6 +345,10 @@ func (_c *OrganizationCreate) defaults() { v := organization.DefaultRestrictContractCreationToOrgAdmins _c.mutation.SetRestrictContractCreationToOrgAdmins(v) } + if _, ok := _c.mutation.EnableAiAgentCollector(); !ok { + v := organization.DefaultEnableAiAgentCollector + _c.mutation.SetEnableAiAgentCollector(v) + } if _, ok := _c.mutation.ID(); !ok { v := organization.DefaultID() _c.mutation.SetID(v) @@ -357,6 +375,9 @@ func (_c *OrganizationCreate) check() error { if _, ok := _c.mutation.RestrictContractCreationToOrgAdmins(); !ok { return &ValidationError{Name: "restrict_contract_creation_to_org_admins", err: errors.New(`ent: missing required field "Organization.restrict_contract_creation_to_org_admins"`)} } + if _, ok := _c.mutation.EnableAiAgentCollector(); !ok { + return &ValidationError{Name: "enable_ai_agent_collector", err: errors.New(`ent: missing required field "Organization.enable_ai_agent_collector"`)} + } return nil } @@ -429,6 +450,10 @@ func (_c *OrganizationCreate) createSpec() (*Organization, *sqlgraph.CreateSpec) _spec.SetField(organization.FieldAPITokenInactivityThresholdDays, field.TypeInt, value) _node.APITokenInactivityThresholdDays = &value } + if value, ok := _c.mutation.EnableAiAgentCollector(); ok { + _spec.SetField(organization.FieldEnableAiAgentCollector, field.TypeBool, value) + _node.EnableAiAgentCollector = value + } if nodes := _c.mutation.MembershipsIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -729,6 +754,18 @@ func (u *OrganizationUpsert) ClearAPITokenInactivityThresholdDays() *Organizatio return u } +// SetEnableAiAgentCollector sets the "enable_ai_agent_collector" field. +func (u *OrganizationUpsert) SetEnableAiAgentCollector(v bool) *OrganizationUpsert { + u.Set(organization.FieldEnableAiAgentCollector, v) + return u +} + +// UpdateEnableAiAgentCollector sets the "enable_ai_agent_collector" field to the value that was provided on create. +func (u *OrganizationUpsert) UpdateEnableAiAgentCollector() *OrganizationUpsert { + u.SetExcluded(organization.FieldEnableAiAgentCollector) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. // Using this option is equivalent to using: // @@ -920,6 +957,20 @@ func (u *OrganizationUpsertOne) ClearAPITokenInactivityThresholdDays() *Organiza }) } +// SetEnableAiAgentCollector sets the "enable_ai_agent_collector" field. +func (u *OrganizationUpsertOne) SetEnableAiAgentCollector(v bool) *OrganizationUpsertOne { + return u.Update(func(s *OrganizationUpsert) { + s.SetEnableAiAgentCollector(v) + }) +} + +// UpdateEnableAiAgentCollector sets the "enable_ai_agent_collector" field to the value that was provided on create. +func (u *OrganizationUpsertOne) UpdateEnableAiAgentCollector() *OrganizationUpsertOne { + return u.Update(func(s *OrganizationUpsert) { + s.UpdateEnableAiAgentCollector() + }) +} + // Exec executes the query. func (u *OrganizationUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -1278,6 +1329,20 @@ func (u *OrganizationUpsertBulk) ClearAPITokenInactivityThresholdDays() *Organiz }) } +// SetEnableAiAgentCollector sets the "enable_ai_agent_collector" field. +func (u *OrganizationUpsertBulk) SetEnableAiAgentCollector(v bool) *OrganizationUpsertBulk { + return u.Update(func(s *OrganizationUpsert) { + s.SetEnableAiAgentCollector(v) + }) +} + +// UpdateEnableAiAgentCollector sets the "enable_ai_agent_collector" field to the value that was provided on create. +func (u *OrganizationUpsertBulk) UpdateEnableAiAgentCollector() *OrganizationUpsertBulk { + return u.Update(func(s *OrganizationUpsert) { + s.UpdateEnableAiAgentCollector() + }) +} + // Exec executes the query. func (u *OrganizationUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/app/controlplane/pkg/data/ent/organization_update.go b/app/controlplane/pkg/data/ent/organization_update.go index c0451eb84..39d0b51c6 100644 --- a/app/controlplane/pkg/data/ent/organization_update.go +++ b/app/controlplane/pkg/data/ent/organization_update.go @@ -174,6 +174,20 @@ func (_u *OrganizationUpdate) ClearAPITokenInactivityThresholdDays() *Organizati return _u } +// SetEnableAiAgentCollector sets the "enable_ai_agent_collector" field. +func (_u *OrganizationUpdate) SetEnableAiAgentCollector(v bool) *OrganizationUpdate { + _u.mutation.SetEnableAiAgentCollector(v) + return _u +} + +// SetNillableEnableAiAgentCollector sets the "enable_ai_agent_collector" field if the given value is not nil. +func (_u *OrganizationUpdate) SetNillableEnableAiAgentCollector(v *bool) *OrganizationUpdate { + if v != nil { + _u.SetEnableAiAgentCollector(*v) + } + return _u +} + // AddMembershipIDs adds the "memberships" edge to the Membership entity by IDs. func (_u *OrganizationUpdate) AddMembershipIDs(ids ...uuid.UUID) *OrganizationUpdate { _u.mutation.AddMembershipIDs(ids...) @@ -550,6 +564,9 @@ func (_u *OrganizationUpdate) sqlSave(ctx context.Context) (_node int, err error if _u.mutation.APITokenInactivityThresholdDaysCleared() { _spec.ClearField(organization.FieldAPITokenInactivityThresholdDays, field.TypeInt) } + if value, ok := _u.mutation.EnableAiAgentCollector(); ok { + _spec.SetField(organization.FieldEnableAiAgentCollector, field.TypeBool, value) + } if _u.mutation.MembershipsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1067,6 +1084,20 @@ func (_u *OrganizationUpdateOne) ClearAPITokenInactivityThresholdDays() *Organiz return _u } +// SetEnableAiAgentCollector sets the "enable_ai_agent_collector" field. +func (_u *OrganizationUpdateOne) SetEnableAiAgentCollector(v bool) *OrganizationUpdateOne { + _u.mutation.SetEnableAiAgentCollector(v) + return _u +} + +// SetNillableEnableAiAgentCollector sets the "enable_ai_agent_collector" field if the given value is not nil. +func (_u *OrganizationUpdateOne) SetNillableEnableAiAgentCollector(v *bool) *OrganizationUpdateOne { + if v != nil { + _u.SetEnableAiAgentCollector(*v) + } + return _u +} + // AddMembershipIDs adds the "memberships" edge to the Membership entity by IDs. func (_u *OrganizationUpdateOne) AddMembershipIDs(ids ...uuid.UUID) *OrganizationUpdateOne { _u.mutation.AddMembershipIDs(ids...) @@ -1473,6 +1504,9 @@ func (_u *OrganizationUpdateOne) sqlSave(ctx context.Context) (_node *Organizati if _u.mutation.APITokenInactivityThresholdDaysCleared() { _spec.ClearField(organization.FieldAPITokenInactivityThresholdDays, field.TypeInt) } + if value, ok := _u.mutation.EnableAiAgentCollector(); ok { + _spec.SetField(organization.FieldEnableAiAgentCollector, field.TypeBool, value) + } if _u.mutation.MembershipsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/app/controlplane/pkg/data/ent/runtime.go b/app/controlplane/pkg/data/ent/runtime.go index d0cae38ce..adb88e48d 100644 --- a/app/controlplane/pkg/data/ent/runtime.go +++ b/app/controlplane/pkg/data/ent/runtime.go @@ -213,6 +213,10 @@ func init() { organizationDescRestrictContractCreationToOrgAdmins := organizationFields[8].Descriptor() // organization.DefaultRestrictContractCreationToOrgAdmins holds the default value on creation for the restrict_contract_creation_to_org_admins field. organization.DefaultRestrictContractCreationToOrgAdmins = organizationDescRestrictContractCreationToOrgAdmins.Default.(bool) + // organizationDescEnableAiAgentCollector is the schema descriptor for enable_ai_agent_collector field. + organizationDescEnableAiAgentCollector := organizationFields[10].Descriptor() + // organization.DefaultEnableAiAgentCollector holds the default value on creation for the enable_ai_agent_collector field. + organization.DefaultEnableAiAgentCollector = organizationDescEnableAiAgentCollector.Default.(bool) // organizationDescID is the schema descriptor for id field. organizationDescID := organizationFields[0].Descriptor() // organization.DefaultID holds the default value on creation for the id field. diff --git a/app/controlplane/pkg/data/ent/schema/organization.go b/app/controlplane/pkg/data/ent/schema/organization.go index a6f5cead5..f360eb015 100644 --- a/app/controlplane/pkg/data/ent/schema/organization.go +++ b/app/controlplane/pkg/data/ent/schema/organization.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-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. @@ -58,6 +58,8 @@ func (Organization) Fields() []ent.Field { // api_token_inactivity_threshold_days is the number of days after which inactive API tokens are auto-revoked. // nil = disabled, any positive int = threshold in days field.Int("api_token_inactivity_threshold_days").Optional().Nillable(), + // enable_ai_agent_collector enables automatic AI agent config collection during attestation init + field.Bool("enable_ai_agent_collector").Default(false), } } diff --git a/app/controlplane/pkg/data/organization.go b/app/controlplane/pkg/data/organization.go index 7f658af78..b47671023 100644 --- a/app/controlplane/pkg/data/organization.go +++ b/app/controlplane/pkg/data/organization.go @@ -77,27 +77,32 @@ func (r *OrganizationRepo) FindByName(ctx context.Context, name string) (*biz.Or return entOrgToBizOrg(org), nil } -func (r *OrganizationRepo) Update(ctx context.Context, id uuid.UUID, blockOnPolicyViolation *bool, policiesAllowedHostnames []string, preventImplicitWorkflowCreation *bool, restrictContractCreationToOrgAdmins *bool, apiTokenInactivityThresholdDays *int) (*biz.Organization, error) { - opts := r.data.DB.Organization.UpdateOneID(id). +func (r *OrganizationRepo) Update(ctx context.Context, id uuid.UUID, updateOpts *biz.OrganizationUpdateOpts) (*biz.Organization, error) { + if updateOpts == nil { + updateOpts = &biz.OrganizationUpdateOpts{} + } + + query := r.data.DB.Organization.UpdateOneID(id). Where(organization.DeletedAtIsNil()). - SetNillableBlockOnPolicyViolation(blockOnPolicyViolation). - SetNillablePreventImplicitWorkflowCreation(preventImplicitWorkflowCreation). - SetNillableRestrictContractCreationToOrgAdmins(restrictContractCreationToOrgAdmins). + SetNillableBlockOnPolicyViolation(updateOpts.BlockOnPolicyViolation). + SetNillablePreventImplicitWorkflowCreation(updateOpts.PreventImplicitWorkflowCreation). + SetNillableRestrictContractCreationToOrgAdmins(updateOpts.RestrictContractCreationToOrgAdmins). + SetNillableEnableAiAgentCollector(updateOpts.EnableAIAgentCollector). SetUpdatedAt(time.Now()) - if policiesAllowedHostnames != nil { - opts.SetPoliciesAllowedHostnames(policiesAllowedHostnames) + if updateOpts.PoliciesAllowedHostnames != nil { + query.SetPoliciesAllowedHostnames(updateOpts.PoliciesAllowedHostnames) } - if apiTokenInactivityThresholdDays != nil { - if *apiTokenInactivityThresholdDays == 0 { - opts.ClearAPITokenInactivityThresholdDays() + if updateOpts.APITokenInactivityThresholdDays != nil { + if *updateOpts.APITokenInactivityThresholdDays == 0 { + query.ClearAPITokenInactivityThresholdDays() } else { - opts.SetAPITokenInactivityThresholdDays(*apiTokenInactivityThresholdDays) + query.SetAPITokenInactivityThresholdDays(*updateOpts.APITokenInactivityThresholdDays) } } - org, err := opts.Save(ctx) + org, err := query.Save(ctx) if err != nil { return nil, fmt.Errorf("failed to update organization: %w", err) } @@ -143,5 +148,6 @@ func entOrgToBizOrg(eu *ent.Organization) *biz.Organization { PreventImplicitWorkflowCreation: eu.PreventImplicitWorkflowCreation, RestrictContractCreationToOrgAdmins: eu.RestrictContractCreationToOrgAdmins, APITokenInactivityThresholdDays: eu.APITokenInactivityThresholdDays, + EnableAIAgentCollector: eu.EnableAiAgentCollector, } }