diff --git a/data-machine-code.php b/data-machine-code.php index 0c216d12..169d1466 100644 --- a/data-machine-code.php +++ b/data-machine-code.php @@ -131,6 +131,7 @@ function datamachine_code_bootstrap() { new \DataMachineCode\Abilities\WordPressRuntimeAbilities(); \DataMachineCode\SourceInventory\WorkspaceSourceInventory::register(); \DataMachineCode\AgentsApi\WorkspaceExecutorAdapter::register(); + \DataMachineCode\Runtime\MountedSandboxBootstrap::register(); datamachine_code_register_datamachine_integrations(); // Register ability categories on the correct hook (must happen during wp_abilities_api_categories_init). diff --git a/inc/Runtime/MountedSandboxBootstrap.php b/inc/Runtime/MountedSandboxBootstrap.php new file mode 100644 index 00000000..4ba71b3e --- /dev/null +++ b/inc/Runtime/MountedSandboxBootstrap.php @@ -0,0 +1,183 @@ + */ + private static array $context = array(); + + public static function register(): void { + add_action('mounted_runtime_bootstrap', array( self::class, 'configure' ), 10, 1); + add_action('wordpress_runtime_bootstrap', array( self::class, 'configure' ), 10, 1); + + $context = self::discover_context(); + if ( self::should_configure($context) ) { + self::configure($context); + } + } + + /** + * Configure DMC from a generic mounted-runtime context. + * + * @param array $context Runtime context supplied by the sandbox substrate. + */ + public static function configure( array $context = array() ): void { + self::$context = $context; + self::configure_workspace_root($context); + self::force_mounted_workspace_backend(); + + add_action('wp_abilities_api_init', array( self::class, 'adopt_workspace_mounts' ), 100); + } + + /** @return array */ + public static function context(): array { + return self::$context; + } + + /** @return array */ + private static function discover_context(): array { + foreach ( array( 'mounted_runtime_context', 'wordpress_runtime_context' ) as $global_key ) { + $context = $GLOBALS[ $global_key ] ?? null; + if ( is_array($context) ) { + return $context; + } + } + + $encoded = getenv('MOUNTED_RUNTIME_CONTEXT'); + if ( is_string($encoded) && '' !== $encoded ) { + $decoded = json_decode($encoded, true); + if ( is_array($decoded) ) { + return $decoded; + } + } + + $workspace_root = getenv('MOUNTED_RUNTIME_WORKSPACE_ROOT'); + if ( is_string($workspace_root) && '' !== $workspace_root ) { + return array( 'workspace_root' => $workspace_root ); + } + + return is_dir(self::DEFAULT_WORKSPACE_ROOT) + ? array( 'workspace_root' => self::DEFAULT_WORKSPACE_ROOT ) + : array(); + } + + /** @param array $context */ + private static function should_configure( array $context ): bool { + return '' !== self::workspace_root_from_context($context); + } + + /** @param array $context */ + private static function configure_workspace_root( array $context ): void { + if ( defined('DATAMACHINE_WORKSPACE_PATH') ) { + return; + } + + $root = self::workspace_root_from_context($context); + if ( '' === $root ) { + return; + } + + if ( ! is_dir($root) ) { + if ( function_exists('wp_mkdir_p') ) { + wp_mkdir_p($root); + } else { + @mkdir($root, 0775, true); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_mkdir + } + } + + if ( is_dir($root) ) { + define('DATAMACHINE_WORKSPACE_PATH', rtrim($root, '/')); + } + } + + private static function force_mounted_workspace_backend(): void { + add_filter('datamachine_code_remote_workspace_backend_should_handle', '__return_false', 100); + } + + public static function adopt_workspace_mounts(): void { + if ( ! defined('DATAMACHINE_WORKSPACE_PATH') || ! class_exists(WorkspaceAbilities::class) ) { + return; + } + + $workspace_root = rtrim( (string) DATAMACHINE_WORKSPACE_PATH, '/'); + $mounts = self::workspace_mounts(self::$context, $workspace_root); + foreach ( $mounts as $mount ) { + if ( ! empty($mount['repo_backed']) ) { + continue; + } + + $path = (string) $mount['path']; + if ( '' === $path || ! is_dir($path) || ! is_dir($path . '/.git') ) { + continue; + } + + WorkspaceAbilities::adoptRepo( + array( + 'path' => $path, + 'name' => basename($path), + ) + ); + } + } + + /** @param array $context */ + private static function workspace_root_from_context( array $context ): string { + $root = isset($context['workspace_root']) ? (string) $context['workspace_root'] : ''; + if ( '' === $root ) { + $workspace = is_array($context['sandbox_workspace'] ?? null) ? $context['sandbox_workspace'] : array(); + $root = isset($workspace['root']) ? (string) $workspace['root'] : ''; + } + + return '' !== $root ? rtrim($root, '/') : ''; + } + + /** + * @param array $context + * @return array + */ + private static function workspace_mounts( array $context, string $workspace_root ): array { + $workspace = is_array($context['sandbox_workspace'] ?? null) ? $context['sandbox_workspace'] : array(); + $mounts = array(); + if ( is_array($workspace['mounts'] ?? null) ) { + foreach ( $workspace['mounts'] as $mount ) { + if ( ! is_array($mount) ) { + continue; + } + $target = rtrim( (string) ( $mount['target'] ?? '' ), '/'); + if ( '' === $target || 0 !== strpos($target . '/', $workspace_root . '/') ) { + continue; + } + $mounts[] = array( + 'path' => $target, + 'repo_backed' => 'repo-backed' === (string) ( $mount['sourceMode'] ?? '' ), + ); + } + } + + if ( ! empty($mounts) ) { + return $mounts; + } + + $paths = glob($workspace_root . '/*', GLOB_ONLYDIR); + foreach ( false !== $paths ? $paths : array() as $path ) { + $mounts[] = array( + 'path' => $path, + 'repo_backed' => is_file($path . '/.git'), + ); + } + + return $mounts; + } +} diff --git a/tests/smoke-mounted-sandbox-bootstrap.php b/tests/smoke-mounted-sandbox-bootstrap.php new file mode 100644 index 00000000..38f715be --- /dev/null +++ b/tests/smoke-mounted-sandbox-bootstrap.php @@ -0,0 +1,127 @@ + true ) + $input; + } + } +} + +namespace { + if ( ! defined('ABSPATH') ) { + define('ABSPATH', __DIR__ . '/'); + } + + $GLOBALS['dmc_mounted_sandbox_actions'] = array(); + $GLOBALS['dmc_mounted_sandbox_filters'] = array(); + + function add_action( string $tag, callable $callback, int $priority = 10 ): void { + $GLOBALS['dmc_mounted_sandbox_actions'][ $tag ][ $priority ][] = $callback; + } + + function do_action( string $tag, ...$args ): void { + if ( empty($GLOBALS['dmc_mounted_sandbox_actions'][ $tag ]) ) { + return; + } + ksort($GLOBALS['dmc_mounted_sandbox_actions'][ $tag ]); + foreach ( $GLOBALS['dmc_mounted_sandbox_actions'][ $tag ] as $callbacks ) { + foreach ( $callbacks as $callback ) { + $callback(...$args); + } + } + } + + function add_filter( string $tag, callable|string $callback, int $priority = 10 ): void { + $GLOBALS['dmc_mounted_sandbox_filters'][ $tag ][ $priority ][] = $callback; + } + + function apply_filters( string $tag, $value ) { + if ( empty($GLOBALS['dmc_mounted_sandbox_filters'][ $tag ]) ) { + return $value; + } + ksort($GLOBALS['dmc_mounted_sandbox_filters'][ $tag ]); + foreach ( $GLOBALS['dmc_mounted_sandbox_filters'][ $tag ] as $callbacks ) { + foreach ( $callbacks as $callback ) { + $value = is_string($callback) && '__return_false' === $callback ? false : $callback($value); + } + } + return $value; + } + + function wp_mkdir_p( string $path ): bool { + return is_dir($path) || mkdir($path, 0775, true); + } + + require __DIR__ . '/../inc/Runtime/MountedSandboxBootstrap.php'; + + $failures = array(); + $passes = 0; + $assert = function ( string $label, bool $condition ) use ( &$failures, &$passes ): void { + if ( $condition ) { + ++$passes; + echo " ok {$label}\n"; + return; + } + + $failures[] = $label; + echo " fail {$label}\n"; + }; + + echo "Mounted sandbox bootstrap - smoke\n"; + + $root = sys_get_temp_dir() . '/dmc-mounted-sandbox-' . bin2hex(random_bytes(4)); + $repo_backed = $root . '/repo-backed'; + $local_repo = $root . '/local-repo'; + mkdir($repo_backed . '/.git', 0775, true); + mkdir($local_repo . '/.git', 0775, true); + register_shutdown_function( + static function () use ( $root ): void { + if ( is_dir($root) ) { + exec('rm -rf ' . escapeshellarg($root)); + } + } + ); + + $GLOBALS['mounted_runtime_context'] = array( + 'workspace_root' => $root, + 'sandbox_workspace' => array( + 'root' => $root, + 'mounts' => array( + array( 'target' => $repo_backed, 'sourceMode' => 'repo-backed' ), + array( 'target' => $local_repo, 'sourceMode' => 'local' ), + ), + ), + ); + \DataMachineCode\Runtime\MountedSandboxBootstrap::register(); + + $assert('defines workspace path from generic sandbox context', defined('DATAMACHINE_WORKSPACE_PATH') && DATAMACHINE_WORKSPACE_PATH === $root); + $assert('forces mounted local workspace backend', false === apply_filters('datamachine_code_remote_workspace_backend_should_handle', true)); + $registered_actions = array_keys($GLOBALS['dmc_mounted_sandbox_actions']); + $assert('registers only neutral mounted-runtime actions', $registered_actions === array( 'mounted_runtime_bootstrap', 'wordpress_runtime_bootstrap', 'wp_abilities_api_init' )); + + do_action('wp_abilities_api_init'); + $adopted = \DataMachineCode\Abilities\WorkspaceAbilities::$adopted; + $assert('adopts exactly one non repo-backed workspace mount', 1 === count($adopted)); + $assert('adopts the local workspace mount', isset($adopted[0]['path']) && $adopted[0]['path'] === $local_repo); + + if ( $failures ) { + echo "\nFailures:\n"; + foreach ( $failures as $failure ) { + echo " - {$failure}\n"; + } + exit(1); + } + + echo "\nOK ({$passes} assertions)\n"; +}