Skip to content

Commit fbccdd6

Browse files
authored
Normalize mounted workspace paths in workspace tools (#505)
* fix: normalize mounted workspace paths * fix: allow mounted workspace path tool calls * fix: normalize mounted workspace ability inputs * fix: honor workspace backend override * fix: accept workspace edit aliases * fix: normalize mounted workspace edits * fix: accept short workspace edit aliases
1 parent 8138ff4 commit fbccdd6

5 files changed

Lines changed: 414 additions & 31 deletions

File tree

inc/Abilities/WorkspaceAbilities.php

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ private function registerAbilities(): void {
233233
'description' => 'Maximum number of lines to return.',
234234
),
235235
),
236-
'required' => array( 'repo', 'path' ),
236+
'required' => array( 'path' ),
237237
),
238238
'output_schema' => array(
239239
'type' => 'object',
@@ -270,7 +270,7 @@ private function registerAbilities(): void {
270270
'description' => 'Relative directory path within the repo (omit for root).',
271271
),
272272
),
273-
'required' => array( 'repo' ),
273+
'required' => array(),
274274
),
275275
'output_schema' => array(
276276
'type' => 'object',
@@ -331,7 +331,7 @@ private function registerAbilities(): void {
331331
'description' => 'Number of surrounding lines to include for each match (default 0, max 10).',
332332
),
333333
),
334-
'required' => array( 'repo', 'pattern' ),
334+
'required' => array( 'pattern' ),
335335
),
336336
'output_schema' => array(
337337
'type' => 'object',
@@ -496,7 +496,7 @@ private function registerAbilities(): void {
496496
'description' => 'File content to write.',
497497
),
498498
),
499-
'required' => array( 'repo', 'path', 'content' ),
499+
'required' => array( 'path', 'content' ),
500500
),
501501
'output_schema' => array(
502502
'type' => 'object',
@@ -538,12 +538,28 @@ private function registerAbilities(): void {
538538
'type' => 'string',
539539
'description' => 'Replacement text.',
540540
),
541+
'search' => array(
542+
'type' => 'string',
543+
'description' => 'Alias for old_string.',
544+
),
545+
'replace' => array(
546+
'type' => 'string',
547+
'description' => 'Alias for new_string.',
548+
),
549+
'old' => array(
550+
'type' => 'string',
551+
'description' => 'Alias for old_string.',
552+
),
553+
'new' => array(
554+
'type' => 'string',
555+
'description' => 'Alias for new_string.',
556+
),
541557
'replace_all' => array(
542558
'type' => 'boolean',
543559
'description' => 'Replace all occurrences (default false).',
544560
),
545561
),
546-
'required' => array( 'repo', 'path', 'old_string', 'new_string' ),
562+
'required' => array( 'path' ),
547563
),
548564
'output_schema' => array(
549565
'type' => 'object',
@@ -617,7 +633,7 @@ private function registerAbilities(): void {
617633
'description' => 'Workspace handle: `<repo>` (primary) or `<repo>@<branch-slug>` (worktree).',
618634
),
619635
),
620-
'required' => array( 'name' ),
636+
'required' => array(),
621637
),
622638
'output_schema' => array(
623639
'type' => 'object',
@@ -843,7 +859,7 @@ private function registerAbilities(): void {
843859
'description' => 'Permit mutation on the primary checkout (default false). Worktrees are always allowed.',
844860
),
845861
),
846-
'required' => array( 'repo', 'path' ),
862+
'required' => array( 'path' ),
847863
),
848864
'output_schema' => array(
849865
'type' => 'object',
@@ -2275,6 +2291,7 @@ public static function showRepo( array $input ): array|\WP_Error {
22752291
* @return array Result.
22762292
*/
22772293
public static function readFile( array $input ): array|\WP_Error {
2294+
$input = self::normalize_mounted_workspace_path_input($input, array( 'repo' ));
22782295
if ( RemoteWorkspaceBackend::should_handle() ) {
22792296
return ( new RemoteWorkspaceBackend() )->read_file(
22802297
$input['repo'] ?? '',
@@ -2304,6 +2321,7 @@ public static function readFile( array $input ): array|\WP_Error {
23042321
* @return array Result.
23052322
*/
23062323
public static function listDirectory( array $input ): array|\WP_Error {
2324+
$input = self::normalize_mounted_workspace_path_input($input, array( 'repo' ));
23072325
if ( RemoteWorkspaceBackend::should_handle() ) {
23082326
return ( new RemoteWorkspaceBackend() )->list_directory(
23092327
$input['repo'] ?? '',
@@ -2327,6 +2345,7 @@ public static function listDirectory( array $input ): array|\WP_Error {
23272345
* @return array Result.
23282346
*/
23292347
public static function grepFiles( array $input ): array|\WP_Error {
2348+
$input = self::normalize_mounted_workspace_path_input($input, array( 'repo' ));
23302349
if ( RemoteWorkspaceBackend::should_handle() ) {
23312350
return ( new RemoteWorkspaceBackend() )->grep(
23322351
$input['repo'] ?? '',
@@ -2433,12 +2452,24 @@ public static function writeFile( array $input ): array|\WP_Error {
24332452
* @return array Result.
24342453
*/
24352454
public static function editFile( array $input ): array|\WP_Error {
2455+
$input = self::normalize_mounted_workspace_path_input($input, array( 'repo' ));
2456+
$old_string = (string) ( $input['old_string'] ?? $input['search'] ?? $input['old'] ?? '' );
2457+
$new_string = (string) ( $input['new_string'] ?? $input['replace'] ?? $input['new'] ?? '' );
2458+
2459+
if ( '' === $old_string ) {
2460+
return new \WP_Error('missing_old_string', 'old_string is required.', array( 'status' => 400 ));
2461+
}
2462+
2463+
if ( ! array_key_exists('new_string', $input) && ! array_key_exists('replace', $input) && ! array_key_exists('new', $input) ) {
2464+
return new \WP_Error('missing_new_string', 'new_string is required.', array( 'status' => 400 ));
2465+
}
2466+
24362467
if ( RemoteWorkspaceBackend::should_handle() ) {
24372468
return ( new RemoteWorkspaceBackend() )->edit_file(
24382469
$input['repo'] ?? '',
24392470
$input['path'] ?? '',
2440-
$input['old_string'] ?? '',
2441-
$input['new_string'] ?? '',
2471+
$old_string,
2472+
$new_string,
24422473
! empty($input['replace_all'])
24432474
);
24442475
}
@@ -2449,8 +2480,8 @@ public static function editFile( array $input ): array|\WP_Error {
24492480
return $writer->edit_file(
24502481
$input['repo'] ?? '',
24512482
$input['path'] ?? '',
2452-
$input['old_string'] ?? '',
2453-
$input['new_string'] ?? '',
2483+
$old_string,
2484+
$new_string,
24542485
! empty($input['replace_all'])
24552486
);
24562487
}
@@ -3309,6 +3340,100 @@ public static function workspaceCleanupCancel( array $input ): array|\WP_Error {
33093340
return ( new CleanupRunService() )->cancel( (string) ( $input['run_id'] ?? '' ));
33103341
}
33113342

3343+
/**
3344+
* Normalize mounted workspace absolute paths into ability-native inputs.
3345+
*
3346+
* @param array<string,mixed> $input Ability input.
3347+
* @param string[] $handle_keys Keys that can hold workspace handles.
3348+
* @return array<string,mixed>
3349+
*/
3350+
private static function normalize_mounted_workspace_path_input( array $input, array $handle_keys ): array {
3351+
$workspace_root = defined('DATAMACHINE_WORKSPACE_PATH') ? self::normalize_workspace_root( (string) DATAMACHINE_WORKSPACE_PATH ) : '';
3352+
if ( '' === $workspace_root ) {
3353+
return $input;
3354+
}
3355+
3356+
foreach ( $handle_keys as $key ) {
3357+
if ( isset($input[ $key ]) && is_string($input[ $key ]) && self::is_absolute_path($input[ $key ]) ) {
3358+
$parts = self::split_workspace_root_path($input[ $key ], $workspace_root);
3359+
if ( null === $parts ) {
3360+
return $input;
3361+
}
3362+
3363+
$input[ $key ] = $parts['repo'];
3364+
if ( '' !== $parts['path'] ) {
3365+
$existing_path = isset($input['path']) && is_string($input['path']) ? trim($input['path'], '/') : '';
3366+
$input['path'] = '' === $existing_path ? $parts['path'] : $parts['path'] . '/' . $existing_path;
3367+
}
3368+
}
3369+
}
3370+
3371+
if ( isset($input['path']) && is_string($input['path']) && self::is_absolute_path($input['path']) ) {
3372+
$parts = self::split_workspace_root_path($input['path'], $workspace_root);
3373+
if ( null === $parts ) {
3374+
return $input;
3375+
}
3376+
3377+
$current_handle = '';
3378+
foreach ( $handle_keys as $key ) {
3379+
if ( isset($input[ $key ]) && is_string($input[ $key ]) && '' !== trim($input[ $key ]) ) {
3380+
$current_handle = trim($input[ $key ]);
3381+
break;
3382+
}
3383+
}
3384+
3385+
if ( '' === $current_handle && ! empty($handle_keys) ) {
3386+
$input[ $handle_keys[0] ] = $parts['repo'];
3387+
}
3388+
if ( '' === $current_handle || $current_handle === $parts['repo'] ) {
3389+
$input['path'] = $parts['path'];
3390+
}
3391+
}
3392+
3393+
return $input;
3394+
}
3395+
3396+
private static function normalize_workspace_root( string $root ): string {
3397+
$root = trim(str_replace('\\', '/', trim($root)), '/');
3398+
return '' === $root ? '' : '/' . $root;
3399+
}
3400+
3401+
private static function is_absolute_path( string $path ): bool {
3402+
$path = str_replace('\\', '/', trim($path));
3403+
return str_starts_with($path, '/') || (bool) preg_match('#^[a-zA-Z][a-zA-Z0-9+.-]*://#', $path);
3404+
}
3405+
3406+
/**
3407+
* @return array{repo:string,path:string}|null
3408+
*/
3409+
private static function split_workspace_root_path( string $path, string $workspace_root ): ?array {
3410+
$path = str_replace('\\', '/', trim($path));
3411+
if ( preg_match('#^[a-zA-Z][a-zA-Z0-9+.-]*://#', $path) ) {
3412+
return null;
3413+
}
3414+
3415+
$root = rtrim($workspace_root, '/');
3416+
if ( $path !== $root && ! str_starts_with($path, $root . '/') ) {
3417+
return null;
3418+
}
3419+
3420+
$relative = ltrim(substr($path, strlen($root)), '/');
3421+
if ( '' === $relative ) {
3422+
return null;
3423+
}
3424+
3425+
$segments = array_values(array_filter(explode('/', $relative), static fn( string $segment ): bool => '' !== $segment && '.' !== $segment));
3426+
if ( empty($segments) || in_array('..', $segments, true) ) {
3427+
return null;
3428+
}
3429+
3430+
$repo = array_shift($segments);
3431+
return array(
3432+
'repo' => $repo,
3433+
'path' => implode('/', $segments),
3434+
);
3435+
}
3436+
33123437
/**
33133438
* Read git log entries for a workspace repository.
33143439
*
@@ -3330,6 +3455,7 @@ public static function gitLog( array $input ): array|\WP_Error {
33303455
* @return array
33313456
*/
33323457
public static function gitDiff( array $input ): array|\WP_Error {
3458+
$input = self::normalize_mounted_workspace_path_input($input, array( 'name' ));
33333459
if ( RemoteWorkspaceBackend::should_handle() ) {
33343460
return ( new RemoteWorkspaceBackend() )->git_diff(
33353461
$input['name'] ?? '',

0 commit comments

Comments
 (0)