Automated PR: merge branch 248-private-ai-policy-foundation into dev (created by Copilot) Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #288
181 lines
6.9 KiB
PHP
181 lines
6.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Ai;
|
|
|
|
use App\Services\Audit\WorkspaceAuditLogger;
|
|
use App\Services\Settings\SettingsResolver;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\OperationalControls\OperationalControlEvaluator;
|
|
|
|
final class GovernedAiExecutionBoundary
|
|
{
|
|
public function __construct(
|
|
private readonly AiUseCaseCatalog $useCaseCatalog,
|
|
private readonly SettingsResolver $settingsResolver,
|
|
private readonly OperationalControlEvaluator $operationalControls,
|
|
private readonly AiDecisionAuditMetadataFactory $auditMetadataFactory,
|
|
private readonly WorkspaceAuditLogger $workspaceAuditLogger,
|
|
) {}
|
|
|
|
public function evaluate(AiExecutionRequest $request): AiExecutionDecision
|
|
{
|
|
$decision = $this->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: [],
|
|
);
|
|
}
|
|
} |