Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data-machine-code.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
183 changes: 183 additions & 0 deletions inc/Runtime/MountedSandboxBootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php
/**
* Self-configuration for mounted sandbox runtimes.
*
* @package DataMachineCode\Runtime
*/

namespace DataMachineCode\Runtime;

use DataMachineCode\Abilities\WorkspaceAbilities;

defined('ABSPATH') || exit;

final class MountedSandboxBootstrap {

private const DEFAULT_WORKSPACE_ROOT = '/workspace';

/** @var array<string,mixed> */
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<string,mixed> $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<string,mixed> */
public static function context(): array {
return self::$context;
}

/** @return array<string,mixed> */
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<string,mixed> $context */
private static function should_configure( array $context ): bool {
return '' !== self::workspace_root_from_context($context);
}

/** @param array<string,mixed> $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<string,mixed> $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<string,mixed> $context
* @return array<int,array{path:string,repo_backed:bool}>
*/
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;
}
}
127 changes: 127 additions & 0 deletions tests/smoke-mounted-sandbox-bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php
/**
* Smoke test for mounted sandbox self-configuration.
*
* Run: php tests/smoke-mounted-sandbox-bootstrap.php
*/

declare( strict_types=1 );

namespace DataMachineCode\Abilities {
class WorkspaceAbilities {
public static array $adopted = array();

public static function adoptRepo( array $input ): array {
self::$adopted[] = $input;
return array( 'success' => 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";
}
Loading