Skip to content

Commit bf1ed84

Browse files
Add mounted sandbox self-configuration
1 parent 352a978 commit bf1ed84

3 files changed

Lines changed: 273 additions & 0 deletions

File tree

data-machine-code.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ function datamachine_code_bootstrap() {
131131
new \DataMachineCode\Abilities\WordPressRuntimeAbilities();
132132
\DataMachineCode\SourceInventory\WorkspaceSourceInventory::register();
133133
\DataMachineCode\AgentsApi\WorkspaceExecutorAdapter::register();
134+
\DataMachineCode\Runtime\MountedSandboxBootstrap::register();
134135
datamachine_code_register_datamachine_integrations();
135136

136137
// Register ability categories on the correct hook (must happen during wp_abilities_api_categories_init).
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php
2+
/**
3+
* Self-configuration for mounted sandbox runtimes.
4+
*
5+
* @package DataMachineCode\Runtime
6+
*/
7+
8+
namespace DataMachineCode\Runtime;
9+
10+
use DataMachineCode\Abilities\WorkspaceAbilities;
11+
12+
defined('ABSPATH') || exit;
13+
14+
final class MountedSandboxBootstrap {
15+
16+
private const DEFAULT_WORKSPACE_ROOT = '/workspace';
17+
18+
/** @var array<string,mixed> */
19+
private static array $context = array();
20+
21+
public static function register(): void {
22+
add_action('wp_codebox_sandbox_runtime_bootstrap', array( self::class, 'configure' ), 10, 1);
23+
}
24+
25+
/**
26+
* Configure DMC from a generic mounted-runtime context.
27+
*
28+
* @param array<string,mixed> $context Runtime context supplied by the sandbox substrate.
29+
*/
30+
public static function configure( array $context = array() ): void {
31+
self::$context = $context;
32+
self::configure_workspace_root($context);
33+
self::force_mounted_workspace_backend();
34+
35+
add_action('wp_abilities_api_init', array( self::class, 'adopt_workspace_mounts' ), 100);
36+
}
37+
38+
/** @return array<string,mixed> */
39+
public static function context(): array {
40+
return self::$context;
41+
}
42+
43+
/** @param array<string,mixed> $context */
44+
private static function configure_workspace_root( array $context ): void {
45+
if ( defined('DATAMACHINE_WORKSPACE_PATH') ) {
46+
return;
47+
}
48+
49+
$root = self::workspace_root_from_context($context);
50+
if ( '' === $root ) {
51+
return;
52+
}
53+
54+
if ( ! is_dir($root) ) {
55+
if ( function_exists('wp_mkdir_p') ) {
56+
wp_mkdir_p($root);
57+
} else {
58+
@mkdir($root, 0775, true); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
59+
}
60+
}
61+
62+
if ( is_dir($root) ) {
63+
define('DATAMACHINE_WORKSPACE_PATH', rtrim($root, '/'));
64+
}
65+
}
66+
67+
private static function force_mounted_workspace_backend(): void {
68+
add_filter('datamachine_code_remote_workspace_backend_should_handle', '__return_false', 100);
69+
}
70+
71+
public static function adopt_workspace_mounts(): void {
72+
if ( ! defined('DATAMACHINE_WORKSPACE_PATH') || ! class_exists(WorkspaceAbilities::class) ) {
73+
return;
74+
}
75+
76+
$workspace_root = rtrim((string) DATAMACHINE_WORKSPACE_PATH, '/');
77+
$mounts = self::workspace_mounts(self::$context, $workspace_root);
78+
foreach ( $mounts as $mount ) {
79+
if ( ! empty($mount['repo_backed']) ) {
80+
continue;
81+
}
82+
83+
$path = (string) ( $mount['path'] ?? '' );
84+
if ( '' === $path || ! is_dir($path) || ! is_dir($path . '/.git') ) {
85+
continue;
86+
}
87+
88+
WorkspaceAbilities::adoptRepo(
89+
array(
90+
'path' => $path,
91+
'name' => basename($path),
92+
)
93+
);
94+
}
95+
}
96+
97+
/** @param array<string,mixed> $context */
98+
private static function workspace_root_from_context( array $context ): string {
99+
$root = isset($context['workspace_root']) ? (string) $context['workspace_root'] : '';
100+
if ( '' === $root ) {
101+
$workspace = is_array($context['sandbox_workspace'] ?? null) ? $context['sandbox_workspace'] : array();
102+
$root = isset($workspace['root']) ? (string) $workspace['root'] : '';
103+
}
104+
105+
return '' !== $root ? rtrim($root, '/') : self::DEFAULT_WORKSPACE_ROOT;
106+
}
107+
108+
/**
109+
* @param array<string,mixed> $context
110+
* @return array<int,array{path:string,repo_backed:bool}>
111+
*/
112+
private static function workspace_mounts( array $context, string $workspace_root ): array {
113+
$workspace = is_array($context['sandbox_workspace'] ?? null) ? $context['sandbox_workspace'] : array();
114+
$mounts = array();
115+
if ( is_array($workspace['mounts'] ?? null) ) {
116+
foreach ( $workspace['mounts'] as $mount ) {
117+
if ( ! is_array($mount) ) {
118+
continue;
119+
}
120+
$target = rtrim((string) ( $mount['target'] ?? '' ), '/');
121+
if ( '' === $target || 0 !== strpos($target . '/', $workspace_root . '/') ) {
122+
continue;
123+
}
124+
$mounts[] = array(
125+
'path' => $target,
126+
'repo_backed' => 'repo-backed' === (string) ( $mount['sourceMode'] ?? '' ),
127+
);
128+
}
129+
}
130+
131+
if ( ! empty($mounts) ) {
132+
return $mounts;
133+
}
134+
135+
foreach ( glob($workspace_root . '/*', GLOB_ONLYDIR) ?: array() as $path ) {
136+
$mounts[] = array(
137+
'path' => $path,
138+
'repo_backed' => is_file($path . '/.git'),
139+
);
140+
}
141+
142+
return $mounts;
143+
}
144+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
/**
3+
* Smoke test for mounted sandbox self-configuration.
4+
*
5+
* Run: php tests/smoke-mounted-sandbox-bootstrap.php
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace DataMachineCode\Abilities {
11+
class WorkspaceAbilities {
12+
public static array $adopted = array();
13+
14+
public static function adoptRepo( array $input ): array {
15+
self::$adopted[] = $input;
16+
return array( 'success' => true ) + $input;
17+
}
18+
}
19+
}
20+
21+
namespace {
22+
if ( ! defined('ABSPATH') ) {
23+
define('ABSPATH', __DIR__ . '/');
24+
}
25+
26+
$GLOBALS['dmc_mounted_sandbox_actions'] = array();
27+
$GLOBALS['dmc_mounted_sandbox_filters'] = array();
28+
29+
function add_action( string $tag, callable $callback, int $priority = 10 ): void {
30+
$GLOBALS['dmc_mounted_sandbox_actions'][ $tag ][ $priority ][] = $callback;
31+
}
32+
33+
function do_action( string $tag, ...$args ): void {
34+
if ( empty($GLOBALS['dmc_mounted_sandbox_actions'][ $tag ]) ) {
35+
return;
36+
}
37+
ksort($GLOBALS['dmc_mounted_sandbox_actions'][ $tag ]);
38+
foreach ( $GLOBALS['dmc_mounted_sandbox_actions'][ $tag ] as $callbacks ) {
39+
foreach ( $callbacks as $callback ) {
40+
$callback(...$args);
41+
}
42+
}
43+
}
44+
45+
function add_filter( string $tag, callable|string $callback, int $priority = 10 ): void {
46+
$GLOBALS['dmc_mounted_sandbox_filters'][ $tag ][ $priority ][] = $callback;
47+
}
48+
49+
function apply_filters( string $tag, $value ) {
50+
if ( empty($GLOBALS['dmc_mounted_sandbox_filters'][ $tag ]) ) {
51+
return $value;
52+
}
53+
ksort($GLOBALS['dmc_mounted_sandbox_filters'][ $tag ]);
54+
foreach ( $GLOBALS['dmc_mounted_sandbox_filters'][ $tag ] as $callbacks ) {
55+
foreach ( $callbacks as $callback ) {
56+
$value = is_string($callback) && '__return_false' === $callback ? false : $callback($value);
57+
}
58+
}
59+
return $value;
60+
}
61+
62+
function wp_mkdir_p( string $path ): bool {
63+
return is_dir($path) || mkdir($path, 0775, true);
64+
}
65+
66+
require __DIR__ . '/../inc/Runtime/MountedSandboxBootstrap.php';
67+
68+
$failures = array();
69+
$passes = 0;
70+
$assert = function ( string $label, bool $condition ) use ( &$failures, &$passes ): void {
71+
if ( $condition ) {
72+
++$passes;
73+
echo " ok {$label}\n";
74+
return;
75+
}
76+
77+
$failures[] = $label;
78+
echo " fail {$label}\n";
79+
};
80+
81+
echo "Mounted sandbox bootstrap - smoke\n";
82+
83+
$root = sys_get_temp_dir() . '/dmc-mounted-sandbox-' . bin2hex(random_bytes(4));
84+
$repo_backed = $root . '/repo-backed';
85+
$local_repo = $root . '/local-repo';
86+
mkdir($repo_backed . '/.git', 0775, true);
87+
mkdir($local_repo . '/.git', 0775, true);
88+
register_shutdown_function(
89+
static function () use ( $root ): void {
90+
if ( is_dir($root) ) {
91+
exec('rm -rf ' . escapeshellarg($root));
92+
}
93+
}
94+
);
95+
96+
\DataMachineCode\Runtime\MountedSandboxBootstrap::register();
97+
do_action(
98+
'wp_codebox_sandbox_runtime_bootstrap',
99+
array(
100+
'workspace_root' => $root,
101+
'sandbox_workspace' => array(
102+
'root' => $root,
103+
'mounts' => array(
104+
array( 'target' => $repo_backed, 'sourceMode' => 'repo-backed' ),
105+
array( 'target' => $local_repo, 'sourceMode' => 'local' ),
106+
),
107+
),
108+
)
109+
);
110+
111+
$assert('defines workspace path from generic sandbox context', defined('DATAMACHINE_WORKSPACE_PATH') && DATAMACHINE_WORKSPACE_PATH === $root);
112+
$assert('forces mounted local workspace backend', false === apply_filters('datamachine_code_remote_workspace_backend_should_handle', true));
113+
114+
do_action('wp_abilities_api_init');
115+
$adopted = \DataMachineCode\Abilities\WorkspaceAbilities::$adopted;
116+
$assert('adopts exactly one non repo-backed workspace mount', 1 === count($adopted));
117+
$assert('adopts the local workspace mount', isset($adopted[0]['path']) && $adopted[0]['path'] === $local_repo);
118+
119+
if ( $failures ) {
120+
echo "\nFailures:\n";
121+
foreach ( $failures as $failure ) {
122+
echo " - {$failure}\n";
123+
}
124+
exit(1);
125+
}
126+
127+
echo "\nOK ({$passes} assertions)\n";
128+
}

0 commit comments

Comments
 (0)