Skip to content

Commit 763885d

Browse files
authored
add workspace context repositories ability (#623)
1 parent 29193f8 commit 763885d

2 files changed

Lines changed: 245 additions & 0 deletions

File tree

inc/Abilities/WorkspaceAbilities.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,44 @@ private function registerAbilities(): void {
428428
)
429429
);
430430

431+
AbilityRegistry::register(
432+
'datamachine-code/workspace-context-repositories',
433+
array(
434+
'label' => 'Register Workspace Context Repositories',
435+
'description' => 'Register read-only context repositories for the current workspace run. Context repositories are exposed through workspace read/list/grep tools with path allowlists and are rejected by mutating workspace operations.',
436+
'category' => 'datamachine-code-workspace',
437+
'input_schema' => array(
438+
'type' => 'object',
439+
'properties' => array(
440+
'target_repo' => array( 'type' => 'string' ),
441+
'target_workspace' => array( 'type' => 'string' ),
442+
'access' => array(
443+
'type' => 'string',
444+
'enum' => array( 'readonly', 'read_only' ),
445+
'description' => 'Context repositories are currently read-only.',
446+
),
447+
'repositories' => array(
448+
'type' => 'array',
449+
'description' => 'Read-only context repository specs. Each entry is { repo, ref, alias, paths }.',
450+
),
451+
),
452+
'required' => array( 'repositories' ),
453+
),
454+
'output_schema' => array(
455+
'type' => 'object',
456+
'properties' => array(
457+
'success' => array( 'type' => 'boolean' ),
458+
'access' => array( 'type' => 'string' ),
459+
'count' => array( 'type' => 'integer' ),
460+
'repositories' => array( 'type' => 'array' ),
461+
),
462+
),
463+
'execute_callback' => array( self::class, 'registerContextRepositories' ),
464+
'permission_callback' => fn() => PermissionHelper::can_manage(),
465+
'meta' => array( 'show_in_rest' => false ),
466+
)
467+
);
468+
431469
AbilityRegistry::register(
432470
'datamachine-code/workspace-adopt',
433471
array(
@@ -2643,6 +2681,79 @@ public static function cloneRepo( array $input ): array|\WP_Error {
26432681
return $result;
26442682
}
26452683

2684+
/**
2685+
* Register read-only context repositories for workspace tools.
2686+
*
2687+
* @param array $input Input parameters with repositories list.
2688+
* @return array<string,mixed>|\WP_Error
2689+
*/
2690+
public static function registerContextRepositories( array $input ): array|\WP_Error {
2691+
$repositories = self::normalizeContextRepositories($input['repositories'] ?? array());
2692+
if ( is_wp_error($repositories) ) {
2693+
return $repositories;
2694+
}
2695+
2696+
if ( ! function_exists('update_option') ) {
2697+
return new \WP_Error('context_repositories_storage_unavailable', 'Context repositories cannot be registered because option storage is unavailable.', array( 'status' => 500 ));
2698+
}
2699+
2700+
update_option('datamachine_code_context_repositories', $repositories, false);
2701+
2702+
return array(
2703+
'success' => true,
2704+
'access' => 'readonly',
2705+
'count' => count($repositories),
2706+
'repositories' => array_values($repositories),
2707+
);
2708+
}
2709+
2710+
/**
2711+
* @param mixed $repositories Raw repository specs.
2712+
* @return array<string,array<string,mixed>>|\WP_Error
2713+
*/
2714+
private static function normalizeContextRepositories( mixed $repositories ): array|\WP_Error {
2715+
if ( ! is_array($repositories) ) {
2716+
return new \WP_Error('invalid_context_repositories', 'repositories must be an array.', array( 'status' => 400 ));
2717+
}
2718+
2719+
$normalized = array();
2720+
foreach ( $repositories as $repository ) {
2721+
if ( ! is_array($repository) ) {
2722+
continue;
2723+
}
2724+
2725+
$repo = trim( (string) ( $repository['repo'] ?? '' ));
2726+
if ( '' === $repo ) {
2727+
return new \WP_Error('invalid_context_repository', 'Each context repository requires a repo value.', array( 'status' => 400 ));
2728+
}
2729+
2730+
$alias = sanitize_key( (string) ( $repository['alias'] ?? basename($repo) ) );
2731+
if ( '' === $alias ) {
2732+
return new \WP_Error('invalid_context_repository_alias', sprintf('Could not derive a context repository alias for %s.', $repo), array( 'status' => 400 ));
2733+
}
2734+
2735+
$paths = array();
2736+
if ( is_array($repository['paths'] ?? null) ) {
2737+
foreach ( $repository['paths'] as $path ) {
2738+
$path = trim( (string) $path );
2739+
if ( '' !== $path ) {
2740+
$paths[] = $path;
2741+
}
2742+
}
2743+
}
2744+
2745+
$normalized[ $alias ] = array(
2746+
'alias' => $alias,
2747+
'repo' => $repo,
2748+
'ref' => trim( (string) ( $repository['ref'] ?? '' )),
2749+
'target' => $alias,
2750+
'paths' => array_values(array_unique($paths)),
2751+
);
2752+
}
2753+
2754+
return $normalized;
2755+
}
2756+
26462757
/**
26472758
* Adopt an existing primary checkout already under the workspace root.
26482759
*
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
/**
3+
* Pure-PHP smoke for workspace context repository ability registration.
4+
*
5+
* Run: php tests/smoke-workspace-context-repositories-ability.php
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace DataMachineCode\Abilities {
11+
class AbilityRegistry
12+
{
13+
}
14+
}
15+
16+
namespace DataMachineCode\Support {
17+
class RuntimeCapabilities
18+
{
19+
}
20+
}
21+
22+
namespace {
23+
if ( ! defined('ABSPATH') ) {
24+
define('ABSPATH', __DIR__);
25+
}
26+
27+
class WP_Error
28+
{
29+
public function __construct( private string $code, private string $message, private array $data = array() )
30+
{
31+
}
32+
33+
public function get_error_code(): string
34+
{
35+
return $this->code;
36+
}
37+
38+
public function get_error_message(): string
39+
{
40+
return $this->message;
41+
}
42+
43+
public function get_error_data(): array
44+
{
45+
return $this->data;
46+
}
47+
}
48+
49+
function is_wp_error( $value ): bool
50+
{
51+
return $value instanceof WP_Error;
52+
}
53+
54+
function sanitize_key( string $key ): string
55+
{
56+
$key = strtolower($key);
57+
return preg_replace('/[^a-z0-9_\-]/', '', $key) ?? '';
58+
}
59+
60+
function update_option( string $name, $value, $autoload = null ): bool
61+
{
62+
$GLOBALS['dmc_context_repository_options'][ $name ] = $value;
63+
$GLOBALS['dmc_context_repository_autoload'][ $name ] = $autoload;
64+
return true;
65+
}
66+
67+
function get_option( string $name, $default = false )
68+
{
69+
return $GLOBALS['dmc_context_repository_options'][ $name ] ?? $default;
70+
}
71+
72+
function apply_filters( string $tag, $value )
73+
{
74+
return $value;
75+
}
76+
77+
function wp_json_encode( $data, int $flags = 0 )
78+
{
79+
return json_encode($data, $flags);
80+
}
81+
82+
require __DIR__ . '/../inc/Abilities/WorkspaceAbilities.php';
83+
require __DIR__ . '/../inc/Workspace/WorkspaceAliasResolver.php';
84+
85+
$failures = array();
86+
$assert = function ( string $label, bool $condition ) use ( &$failures ): void {
87+
if ( $condition ) {
88+
echo " ok {$label}\n";
89+
return;
90+
}
91+
92+
$failures[] = $label;
93+
echo " fail {$label}\n";
94+
};
95+
96+
echo "Workspace context repositories ability - smoke\n";
97+
98+
$result = \DataMachineCode\Abilities\WorkspaceAbilities::registerContextRepositories(
99+
array(
100+
'target_repo' => 'Automattic/build-with-wordpress',
101+
'target_workspace' => 'build-with-wordpress@skills',
102+
'access' => 'readonly',
103+
'repositories' => array(
104+
array(
105+
'repo' => 'Automattic/studio',
106+
'ref' => 'trunk',
107+
'alias' => 'studio',
108+
'paths' => array( 'apps/cli/ai/tools/**', 'apps/cli/ai/tools/**', 'README.md' ),
109+
),
110+
),
111+
)
112+
);
113+
114+
$stored = get_option('datamachine_code_context_repositories', array());
115+
$policy = \DataMachineCode\Workspace\WorkspaceAliasResolver::policy_attestation('studio');
116+
117+
$assert('registers successfully', is_array($result) && true === ( $result['success'] ?? false ));
118+
$assert('reports repository count', is_array($result) && 1 === ( $result['count'] ?? 0 ));
119+
$assert('stores context repositories option', isset($stored['studio']));
120+
$assert('stores option with autoload disabled', false === ( $GLOBALS['dmc_context_repository_autoload']['datamachine_code_context_repositories'] ?? null ));
121+
$assert('deduplicates allowed paths', array( 'apps/cli/ai/tools/**', 'README.md' ) === ( $stored['studio']['paths'] ?? array() ));
122+
$assert('resolver exposes read-only policy', false === ( $policy['writable'] ?? true ) && true === ( $policy['read_only'] ?? false ));
123+
$assert('resolver keeps repo and ref', 'Automattic/studio' === ( $policy['repo'] ?? '' ) && 'trunk' === ( $policy['ref'] ?? '' ));
124+
125+
if ( $failures ) {
126+
echo "\nFailures:\n";
127+
foreach ( $failures as $failure ) {
128+
echo " - {$failure}\n";
129+
}
130+
exit(1);
131+
}
132+
133+
echo "\nOK\n";
134+
}

0 commit comments

Comments
 (0)