$allowedEnvironments */ public function __construct( public bool $invocationEnabled, public bool $productionRunnerEnabled, public array $allowedEnvironments, public string $currentEnvironment, public string $powershellBinary, public bool $moduleCheckEnabled, public int $checkTimeoutSeconds, public int $invocationTimeoutSeconds, public int $stdoutMaxBytes, public int $stderrMaxBytes, public int $itemLimit, public int $lockTtlSeconds, public bool $tempFilesEnabled, public ?string $tempDirectory, ) {} public static function fromConfig(): self { $runtime = config('tenantpilot.exchange_powershell.runtime', []); $runtime = is_array($runtime) ? $runtime : []; return new self( invocationEnabled: (bool) config(ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH, false), productionRunnerEnabled: (bool) config(ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH, false), allowedEnvironments: self::stringList($runtime['allowed_environments'] ?? ['local', 'testing']), currentEnvironment: app()->environment(), powershellBinary: self::nonEmptyString($runtime['powershell_binary'] ?? null, 'pwsh'), moduleCheckEnabled: (bool) ($runtime['module_check_enabled'] ?? true), checkTimeoutSeconds: max(0, (int) ($runtime['check_timeout_seconds'] ?? 5)), invocationTimeoutSeconds: max(0, (int) ($runtime['invocation_timeout_seconds'] ?? 60)), stdoutMaxBytes: max(0, (int) ($runtime['stdout_max_bytes'] ?? 524288)), stderrMaxBytes: max(0, (int) ($runtime['stderr_max_bytes'] ?? 65536)), itemLimit: max(0, (int) ($runtime['item_limit'] ?? 1000)), lockTtlSeconds: max(0, (int) ($runtime['lock_ttl_seconds'] ?? 300)), tempFilesEnabled: (bool) ($runtime['temp_files_enabled'] ?? false), tempDirectory: self::nullableString($runtime['temp_directory'] ?? null), ); } public function environmentAllowed(): bool { return in_array($this->currentEnvironment, $this->allowedEnvironments, true); } public function timeoutPolicyValid(): bool { return $this->checkTimeoutSeconds > 0 && $this->invocationTimeoutSeconds > 0 && $this->stdoutMaxBytes > 0 && $this->stderrMaxBytes > 0 && $this->itemLimit > 0 && $this->lockTtlSeconds > 0; } public function tempPolicySafe(): bool { if (! $this->tempFilesEnabled) { return true; } if ($this->tempDirectory === null || $this->tempDirectory === '') { return false; } $allowedPrefix = rtrim(storage_path('app/tmp'), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; $directory = rtrim($this->tempDirectory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; return str_starts_with($directory, $allowedPrefix); } /** * @return list */ private static function stringList(mixed $values): array { if (is_string($values)) { $values = explode(',', $values); } if (! is_array($values)) { return []; } return array_values(array_filter( array_map(static fn (mixed $value): string => trim((string) $value), $values), static fn (string $value): bool => $value !== '', )); } private static function nonEmptyString(mixed $value, string $fallback): string { $value = is_string($value) ? trim($value) : ''; return $value !== '' ? $value : $fallback; } private static function nullableString(mixed $value): ?string { $value = is_string($value) ? trim($value) : ''; return $value !== '' ? $value : null; } }