Skip to content

Commit 4a95483

Browse files
authored
Merge pull request #724 from Extra-Chill/issue-wp-codebox-842-sandbox-self-config
Add mounted sandbox self-configuration
2 parents 2b55a9c + be8a20e commit 4a95483

3 files changed

Lines changed: 311 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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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('mounted_runtime_bootstrap', array( self::class, 'configure' ), 10, 1);
23+
add_action('wordpress_runtime_bootstrap', array( self::class, 'configure' ), 10, 1);
24+
25+
$context = self::discover_context();
26+
if ( self::should_configure($context) ) {
27+
self::configure($context);
28+
}
29+
}
30+
31+
/**
32+
* Configure DMC from a generic mounted-runtime context.
33+
*
34+
* @param array<string,mixed> $context Runtime context supplied by the sandbox substrate.
35+
*/
36+
public static function configure( array $context = array() ): void {
37+
self::$context = $context;
38+
self::configure_workspace_root($context);
39+
self::force_mounted_workspace_backend();
40+
41+
add_action('wp_abilities_api_init', array( self::class, 'adopt_workspace_mounts' ), 100);
42+
}
43+
44+
/** @return array<string,mixed> */
45+
public static function context(): array {
46+
return self::$context;
47+
}
48+
49+
/** @return array<string,mixed> */
50+
private static function discover_context(): array {
51+
foreach ( array( 'mounted_runtime_context', 'wordpress_runtime_context' ) as $global_key ) {
52+
$context = $GLOBALS[ $global_key ] ?? null;
53+
if ( is_array($context) ) {
54+
return $context;
55+
}
56+
}
57+
58+
$encoded = getenv('MOUNTED_RUNTIME_CONTEXT');
59+
if ( is_string($encoded) && '' !== $encoded ) {
60+
$decoded = json_decode($encoded, true);
61+
if ( is_array($decoded) ) {
62+
return $decoded;
63+
}
64+
}
65+
66+
$workspace_root = getenv('MOUNTED_RUNTIME_WORKSPACE_ROOT');
67+
if ( is_string($workspace_root) && '' !== $workspace_root ) {
68+
return array( 'workspace_root' => $workspace_root );
69+
}
70+
71+
return is_dir(self::DEFAULT_WORKSPACE_ROOT)
72+
? array( 'workspace_root' => self::DEFAULT_WORKSPACE_ROOT )
73+
: array();
74+
}
75+
76+
/** @param array<string,mixed> $context */
77+
private static function should_configure( array $context ): bool {
78+
return '' !== self::workspace_root_from_context($context);
79+
}
80+
81+
/** @param array<string,mixed> $context */
82+
private static function configure_workspace_root( array $context ): void {
83+
if ( defined('DATAMACHINE_WORKSPACE_PATH') ) {
84+
return;
85+
}
86+
87+
$root = self::workspace_root_from_context($context);
88+
if ( '' === $root ) {
89+
return;
90+
}
91+
92+
if ( ! is_dir($root) ) {
93+
if ( function_exists('wp_mkdir_p') ) {
94+
wp_mkdir_p($root);
95+
} else {
96+
@mkdir($root, 0775, true); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
97+
}
98+
}
99+
100+
if ( is_dir($root) ) {
101+
define('DATAMACHINE_WORKSPACE_PATH', rtrim($root, '/'));
102+
}
103+
}
104+
105+
private static function force_mounted_workspace_backend(): void {
106+
add_filter('datamachine_code_remote_workspace_backend_should_handle', '__return_false', 100);
107+
}
108+
109+
public static function adopt_workspace_mounts(): void {
110+
if ( ! defined('DATAMACHINE_WORKSPACE_PATH') || ! class_exists(WorkspaceAbilities::class) ) {
111+
return;
112+
}
113+
114+
$workspace_root = rtrim( (string) DATAMACHINE_WORKSPACE_PATH, '/');
115+
$mounts = self::workspace_mounts(self::$context, $workspace_root);
116+
foreach ( $mounts as $mount ) {
117+
if ( ! empty($mount['repo_backed']) ) {
118+
continue;
119+
}
120+
121+
$path = (string) $mount['path'];
122+
if ( '' === $path || ! is_dir($path) || ! is_dir($path . '/.git') ) {
123+
continue;
124+
}
125+
126+
WorkspaceAbilities::adoptRepo(
127+
array(
128+
'path' => $path,
129+
'name' => basename($path),
130+
)
131+
);
132+
}
133+
}
134+
135+
/** @param array<string,mixed> $context */
136+
private static function workspace_root_from_context( array $context ): string {
137+
$root = isset($context['workspace_root']) ? (string) $context['workspace_root'] : '';
138+
if ( '' === $root ) {
139+
$workspace = is_array($context['sandbox_workspace'] ?? null) ? $context['sandbox_workspace'] : array();
140+
$root = isset($workspace['root']) ? (string) $workspace['root'] : '';
141+
}
142+
143+
return '' !== $root ? rtrim($root, '/') : '';
144+
}
145+
146+
/**
147+
* @param array<string,mixed> $context
148+
* @return array<int,array{path:string,repo_backed:bool}>
149+
*/
150+
private static function workspace_mounts( array $context, string $workspace_root ): array {
151+
$workspace = is_array($context['sandbox_workspace'] ?? null) ? $context['sandbox_workspace'] : array();
152+
$mounts = array();
153+
if ( is_array($workspace['mounts'] ?? null) ) {
154+
foreach ( $workspace['mounts'] as $mount ) {
155+
if ( ! is_array($mount) ) {
156+
continue;
157+
}
158+
$target = rtrim( (string) ( $mount['target'] ?? '' ), '/');
159+
if ( '' === $target || 0 !== strpos($target . '/', $workspace_root . '/') ) {
160+
continue;
161+
}
162+
$mounts[] = array(
163+
'path' => $target,
164+
'repo_backed' => 'repo-backed' === (string) ( $mount['sourceMode'] ?? '' ),
165+
);
166+
}
167+
}
168+
169+
if ( ! empty($mounts) ) {
170+
return $mounts;
171+
}
172+
173+
$paths = glob($workspace_root . '/*', GLOB_ONLYDIR);
174+
foreach ( false !== $paths ? $paths : array() as $path ) {
175+
$mounts[] = array(
176+
'path' => $path,
177+
'repo_backed' => is_file($path . '/.git'),
178+
);
179+
}
180+
181+
return $mounts;
182+
}
183+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
$GLOBALS['mounted_runtime_context'] = array(
97+
'workspace_root' => $root,
98+
'sandbox_workspace' => array(
99+
'root' => $root,
100+
'mounts' => array(
101+
array( 'target' => $repo_backed, 'sourceMode' => 'repo-backed' ),
102+
array( 'target' => $local_repo, 'sourceMode' => 'local' ),
103+
),
104+
),
105+
);
106+
\DataMachineCode\Runtime\MountedSandboxBootstrap::register();
107+
108+
$assert('defines workspace path from generic sandbox context', defined('DATAMACHINE_WORKSPACE_PATH') && DATAMACHINE_WORKSPACE_PATH === $root);
109+
$assert('forces mounted local workspace backend', false === apply_filters('datamachine_code_remote_workspace_backend_should_handle', true));
110+
$registered_actions = array_keys($GLOBALS['dmc_mounted_sandbox_actions']);
111+
$assert('registers only neutral mounted-runtime actions', $registered_actions === array( 'mounted_runtime_bootstrap', 'wordpress_runtime_bootstrap', 'wp_abilities_api_init' ));
112+
113+
do_action('wp_abilities_api_init');
114+
$adopted = \DataMachineCode\Abilities\WorkspaceAbilities::$adopted;
115+
$assert('adopts exactly one non repo-backed workspace mount', 1 === count($adopted));
116+
$assert('adopts the local workspace mount', isset($adopted[0]['path']) && $adopted[0]['path'] === $local_repo);
117+
118+
if ( $failures ) {
119+
echo "\nFailures:\n";
120+
foreach ( $failures as $failure ) {
121+
echo " - {$failure}\n";
122+
}
123+
exit(1);
124+
}
125+
126+
echo "\nOK ({$passes} assertions)\n";
127+
}

0 commit comments

Comments
 (0)