decisionFor($request); $metadata = $this->auditMetadataFactory->make($request, $decision); $decision = new AiExecutionDecision( outcome: $decision->outcome, reasonCode: $decision->reasonCode, workspaceAiPolicyMode: $decision->workspaceAiPolicyMode, matchedOperationalControlScope: $decision->matchedOperationalControlScope, useCaseKey: $decision->useCaseKey, requestedProviderClass: $decision->requestedProviderClass, dataClassifications: $decision->dataClassifications, sourceFamily: $decision->sourceFamily, auditAction: $decision->auditAction, auditMetadata: $metadata, ); if ($request->workspace !== null) { $definition = $this->useCaseCatalog->find($request->useCaseKey); $this->workspaceAuditLogger->log( workspace: $request->workspace, action: $decision->auditAction, context: ['metadata' => $decision->auditMetadata], actor: $request->actor, status: $decision->isAllowed() ? 'success' : 'blocked', resourceType: 'ai_use_case', resourceId: $request->useCaseKey, targetLabel: $definition['label'] ?? $request->useCaseKey, summary: 'AI execution decision evaluated', tenant: $request->tenant, ); } return $decision; } private function decisionFor(AiExecutionRequest $request): AiExecutionDecision { if ($request->workspace === null) { return $this->blockedDecision( request: $request, reasonCode: AiDecisionReasonCode::MissingWorkspaceContext, workspaceAiPolicyMode: AiPolicyMode::Disabled->value, ); } if ($request->tenant !== null && (int) $request->tenant->workspace_id !== (int) $request->workspace->getKey()) { return $this->blockedDecision( request: $request, reasonCode: AiDecisionReasonCode::TenantOutsideWorkspace, workspaceAiPolicyMode: AiPolicyMode::Disabled->value, ); } $controlDecision = $this->operationalControls->evaluate('ai.execution', $request->workspace); if ($controlDecision->isPaused()) { return $this->blockedDecision( request: $request, reasonCode: AiDecisionReasonCode::OperationalControlPaused, workspaceAiPolicyMode: $this->resolvedPolicyMode($request), matchedOperationalControlScope: $controlDecision->matchedScopeType, ); } $policyMode = $this->resolvedPolicyMode($request); if ($policyMode === AiPolicyMode::Disabled->value) { return $this->blockedDecision( request: $request, reasonCode: AiDecisionReasonCode::WorkspacePolicyDisabled, workspaceAiPolicyMode: $policyMode, ); } $definition = $this->useCaseCatalog->find($request->useCaseKey); if ($definition === null) { return $this->blockedDecision( request: $request, reasonCode: AiDecisionReasonCode::UnregisteredUseCase, workspaceAiPolicyMode: $policyMode, ); } if ($definition['source_family'] !== $request->sourceFamily) { return $this->blockedDecision( request: $request, reasonCode: AiDecisionReasonCode::SourceFamilyMismatch, workspaceAiPolicyMode: $policyMode, ); } if (! in_array($request->requestedProviderClass, $definition['allowed_provider_classes'], true)) { return $this->blockedDecision( request: $request, reasonCode: AiDecisionReasonCode::ProviderClassBlocked, workspaceAiPolicyMode: $policyMode, ); } foreach ($request->dataClassifications as $classification) { if (! in_array($classification, $definition['allowed_data_classifications'], true)) { return $this->blockedDecision( request: $request, reasonCode: AiDecisionReasonCode::DataClassificationBlocked, workspaceAiPolicyMode: $policyMode, ); } } return new AiExecutionDecision( outcome: 'allowed', reasonCode: AiDecisionReasonCode::Allowed, workspaceAiPolicyMode: $policyMode, matchedOperationalControlScope: null, useCaseKey: $request->useCaseKey, requestedProviderClass: $request->requestedProviderClass, dataClassifications: $request->dataClassifications, sourceFamily: $request->sourceFamily, auditAction: AuditActionId::AiExecutionDecisionEvaluated, auditMetadata: [], ); } private function resolvedPolicyMode(AiExecutionRequest $request): string { if ($request->workspace === null) { return AiPolicyMode::Disabled->value; } $resolved = $this->settingsResolver->resolveValue($request->workspace, 'ai', 'policy_mode'); return is_string($resolved) && $resolved !== '' ? $resolved : AiPolicyMode::Disabled->value; } private function blockedDecision( AiExecutionRequest $request, AiDecisionReasonCode $reasonCode, string $workspaceAiPolicyMode, ?string $matchedOperationalControlScope = null, ): AiExecutionDecision { return new AiExecutionDecision( outcome: 'blocked', reasonCode: $reasonCode, workspaceAiPolicyMode: $workspaceAiPolicyMode, matchedOperationalControlScope: $matchedOperationalControlScope, useCaseKey: $request->useCaseKey, requestedProviderClass: $request->requestedProviderClass, dataClassifications: $request->dataClassifications, sourceFamily: $request->sourceFamily, auditAction: AuditActionId::AiExecutionDecisionEvaluated, auditMetadata: [], ); } }