*/ public function reconcile(OperationRun $run, User $actor): array { $run->loadMissing(['workspace', 'tenant']); $authorization = Gate::forUser($actor)->inspect('reconcile', $run); if ($authorization->denied()) { $this->recordAction( run: $run, actor: $actor, action: 'operation.reconcile_denied', status: 'denied', reasonCode: 'policy_denied', before: $this->state($run), after: $this->state($run), ); abort($authorization->status() ?: 403); } $decision = $this->eligibility->forRun($run, $actor); if (! $this->hasEnabledAction($decision, 'reconcile')) { $this->recordAction( run: $run, actor: $actor, action: 'operation.reconcile_denied', status: 'denied', reasonCode: array_key_exists('reconcile', $decision['disabled_reasons']) ? 'reconcile_unavailable' : 'reconcile_not_primary', before: $this->state($run), after: $this->state($run), ); abort(403); } $before = $this->state($run); $change = $this->reconciler->reconcileOperationRun($run, false); $run->refresh(); $after = $this->state($run); if (! is_array($change) || ($change['applied'] ?? false) !== true) { $this->recordAction( run: $run, actor: $actor, action: 'operation.reconcile_noop', status: 'warning', reasonCode: is_string($change['reason_code'] ?? null) ? (string) $change['reason_code'] : 'no_reconciliation_applied', before: $before, after: $after, ); return [ 'applied' => false, 'reason_code' => is_string($change['reason_code'] ?? null) ? (string) $change['reason_code'] : 'no_reconciliation_applied', 'change' => $change, ]; } $this->recordAction( run: $run, actor: $actor, action: 'operation.reconciled_by_operator', status: 'success', reasonCode: is_string($change['reason_code'] ?? null) ? (string) $change['reason_code'] : 'operator_reconcile_applied', before: $before, after: $after, ); return [ 'applied' => true, 'reason_code' => is_string($change['reason_code'] ?? null) ? (string) $change['reason_code'] : 'operator_reconcile_applied', 'change' => $change, ]; } /** * @param array $decision */ private function hasEnabledAction(array $decision, string $key): bool { $primary = $decision['primary_action'] ?? null; if (is_array($primary) && ($primary['key'] ?? null) === $key) { return true; } foreach ($decision['secondary_actions'] ?? [] as $action) { if (is_array($action) && ($action['key'] ?? null) === $key) { return true; } } return false; } /** * @return array{status:string,outcome:string} */ private function state(OperationRun $run): array { return [ 'status' => (string) $run->status, 'outcome' => (string) $run->outcome, ]; } /** * @param array{status:string,outcome:string} $before * @param array{status:string,outcome:string} $after */ private function recordAction( OperationRun $run, User $actor, string $action, string $status, string $reasonCode, array $before, array $after, ): void { $workspace = $run->workspace instanceof Workspace ? $run->workspace : null; $tenant = $run->tenant instanceof ManagedEnvironment ? $run->tenant : null; if (! $workspace instanceof Workspace) { return; } $this->auditRecorder->record( action: $action, context: [ 'metadata' => [ 'operator_action' => 'reconcile', 'operation_run_id' => (int) $run->getKey(), 'workspace_id' => (int) $workspace->getKey(), 'managed_environment_id' => $tenant instanceof ManagedEnvironment ? (int) $tenant->getKey() : null, 'actor_user_id' => (int) $actor->getKey(), 'operation_type' => OperationCatalog::canonicalCode((string) $run->type), 'previous_status' => $before['status'], 'previous_outcome' => $before['outcome'], 'resulting_status' => $after['status'], 'resulting_outcome' => $after['outcome'], 'reason_code' => $reasonCode, 'requested_at' => now()->toIso8601String(), 'mutation_scope' => 'tenantpilot_operation_metadata_only', ], ], workspace: $workspace, tenant: $tenant, actor: AuditActorSnapshot::human($actor), target: new AuditTargetSnapshot( type: 'operation_run', id: (int) $run->getKey(), label: OperationCatalog::label((string) $run->type).' #'.$run->getKey(), ), outcome: $status, summary: match ($status) { 'success' => 'Operation run reconciled by operator', 'warning' => 'Operation run reconcile action had no effect', default => 'Operation run reconcile action denied', }, operationRunId: (int) $run->getKey(), ); } }