@@ -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 *
0 commit comments