diff --git a/data-machine-code.php b/data-machine-code.php index 1bc36b7..d0cded6 100644 --- a/data-machine-code.php +++ b/data-machine-code.php @@ -3,7 +3,7 @@ * Plugin Name: Data Machine Code * Plugin URI: https://github.com/Extra-Chill/data-machine-code * Description: Developer tools extension for Data Machine. GitHub integration, workspace management, git operations, and code tools for WordPress AI agents. - * Version: 0.6.2 + * Version: 0.6.3 * Requires at least: 6.9 * Requires PHP: 8.2 * Requires Plugins: data-machine @@ -18,7 +18,7 @@ die; } -define( 'DATAMACHINE_CODE_VERSION', '0.6.2' ); +define( 'DATAMACHINE_CODE_VERSION', '0.6.3' ); define( 'DATAMACHINE_CODE_PATH', plugin_dir_path( __FILE__ ) ); define( 'DATAMACHINE_CODE_URL', plugin_dir_url( __FILE__ ) ); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3f9ba04..97349d5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to Data Machine Code will be documented in this file. +## [0.6.3] - 2026-04-21 + +### Fixed +- workspace git wrapper: `git pull/add/commit/push` no longer fail with `git_write_disabled` on repos that have no entry in `datamachine_workspace_git_policies`. Unconfigured repos now default to permissive (primary-vs-worktree protection remains via `--allow-primary-mutation`). Explicit `write_enabled: false` / `push_enabled: false` still deny. Error messages clarified to reference the policy option. +- `workspace git add`: `no_allowed_paths` no longer blocks unconfigured repos. The `allowed_paths` list is now opt-in — when configured, it restricts; when absent, any relative path is accepted (sensitive-path + traversal checks still enforced). + +### Added +- `datamachine_workspace_git_policies` filter for runtime-injected policy (alongside the existing option). Allows site config to declare per-repo policy without persisting to the option. +- Documentation in `get_workspace_git_policies()` describing the policy schema and defaults. + ## [0.6.2] - 2026-04-19 ### Changed diff --git a/inc/Workspace/Workspace.php b/inc/Workspace/Workspace.php index fd76a9d..657fa82 100644 --- a/inc/Workspace/Workspace.php +++ b/inc/Workspace/Workspace.php @@ -568,10 +568,11 @@ public function git_add( string $handle, array $paths, bool $allow_primary_mutat return new \WP_Error( 'missing_paths', 'At least one path is required for git add.', array( 'status' => 400 ) ); } + // Allowed paths are opt-in: when configured, they restrict which relative + // paths may be staged; when absent, any path within the repo is allowed. + // This mirrors ensure_git_mutation_allowed's permissive-by-default model. + // Sensitive-path + traversal checks still apply unconditionally. $allowed_roots = $this->get_repo_allowed_paths( $repo_name ); - if ( empty( $allowed_roots ) ) { - return new \WP_Error( 'no_allowed_paths', sprintf( 'No allowed paths configured for repo "%s".', $repo_name ), array( 'status' => 403 ) ); - } $clean_paths = array(); foreach ( $paths as $path ) { @@ -588,7 +589,8 @@ public function git_add( string $handle, array $paths, bool $allow_primary_mutat return new \WP_Error( 'sensitive_path', sprintf( 'Refusing to stage sensitive path: %s', $relative ), array( 'status' => 403 ) ); } - if ( ! $this->is_path_allowed( $relative, $allowed_roots ) ) { + // Only enforce the allowlist when one has been configured. + if ( ! empty( $allowed_roots ) && ! $this->is_path_allowed( $relative, $allowed_roots ) ) { return new \WP_Error( 'path_not_allowed', sprintf( 'Path "%s" is outside configured allowlist.', $relative ), array( 'status' => 403 ) ); } @@ -1650,6 +1652,19 @@ private function ensure_primary_mutation_allowed( array $parsed, bool $allow ): /** * Check if repo has git mutation permissions enabled. * + * Unconfigured repos are permissive by default: `datamachine_workspace_git_policies` + * is an opt-in restriction layer. When no entry exists for $repo_name, both + * write and push are allowed. When an entry exists, its flags (`write_enabled`, + * `push_enabled`) are honored; missing flags default to `true` so a partial + * config doesn't accidentally lock out ops that weren't explicitly restricted. + * + * The primary-vs-worktree gate (see ensure_primary_mutation_allowed) remains + * the documented default safety mechanism for protecting tracked branches. + * + * To explicitly deny a repo, add an entry with `write_enabled: false` and/or + * `push_enabled: false`. The `datamachine_workspace_git_policies` filter also + * lets plugins inject policy at runtime. + * * @param string $repo_name Repository name. * @param bool $require_push Whether push must also be enabled. * @return true|\WP_Error @@ -1658,12 +1673,43 @@ private function ensure_git_mutation_allowed( string $repo_name, bool $require_p $policies = $this->get_workspace_git_policies(); $repo = $policies['repos'][ $repo_name ] ?? null; - if ( ! is_array( $repo ) || empty( $repo['write_enabled'] ) ) { - return new \WP_Error( 'git_write_disabled', sprintf( 'Git write operations are disabled for repo "%s".', $repo_name ), array( 'status' => 403 ) ); + // No entry = permissive default. Callers should still respect + // primary-vs-worktree separation via ensure_primary_mutation_allowed. + if ( null === $repo ) { + return true; + } + + if ( ! is_array( $repo ) ) { + return true; } - if ( $require_push && empty( $repo['push_enabled'] ) ) { - return new \WP_Error( 'git_push_disabled', sprintf( 'Git push is disabled for repo "%s".', $repo_name ), array( 'status' => 403 ) ); + // Entry exists — honor explicit flags. Missing flags default to true + // so a partial config (e.g. only setting allowed_paths) doesn't + // accidentally disable write/push. + $write_enabled = array_key_exists( 'write_enabled', $repo ) ? (bool) $repo['write_enabled'] : true; + if ( ! $write_enabled ) { + return new \WP_Error( + 'git_write_disabled', + sprintf( + 'Git write operations are explicitly disabled for repo "%s" via datamachine_workspace_git_policies.', + $repo_name + ), + array( 'status' => 403 ) + ); + } + + if ( $require_push ) { + $push_enabled = array_key_exists( 'push_enabled', $repo ) ? (bool) $repo['push_enabled'] : true; + if ( ! $push_enabled ) { + return new \WP_Error( + 'git_push_disabled', + sprintf( + 'Git push is explicitly disabled for repo "%s" via datamachine_workspace_git_policies.', + $repo_name + ), + array( 'status' => 403 ) + ); + } } return true; @@ -1760,7 +1806,21 @@ private function has_traversal( string $path ): bool { /** * Read workspace git policy settings. * - * @return array + * The option defaults to an empty array, which is treated as "no + * restrictions" by the mutation gates (see ensure_git_mutation_allowed + * and git_add's allowed_paths check). To restrict a repo, configure an + * entry under `repos[]` with any combination of: + * + * - write_enabled (bool, default true) + * - push_enabled (bool, default true) + * - allowed_paths (string[], optional — if set, restricts git_add) + * - fixed_branch (string, optional — constrains push on primary) + * + * The `datamachine_workspace_git_policies` filter allows plugins and + * site configuration to inject or override policy at runtime without + * touching the stored option. + * + * @return array{repos: array>} */ private function get_workspace_git_policies(): array { $defaults = array( @@ -1769,13 +1829,31 @@ private function get_workspace_git_policies(): array { $settings = get_option( 'datamachine_workspace_git_policies', $defaults ); if ( ! is_array( $settings ) ) { - return $defaults; + $settings = $defaults; } if ( ! isset( $settings['repos'] ) || ! is_array( $settings['repos'] ) ) { $settings['repos'] = array(); } + /** + * Filter the workspace git policy map. + * + * Allows plugins to inject or override per-repo git mutation policy + * without persisting changes to the `datamachine_workspace_git_policies` + * option. Useful for environment-specific policy (dev vs prod) or for + * tying policy to site configuration managed elsewhere. + * + * @since 0.x.0 + * + * @param array $settings Current policy array, shape { repos: { : {...} } }. + */ + $settings = apply_filters( 'datamachine_workspace_git_policies', $settings ); + + if ( ! is_array( $settings ) || ! isset( $settings['repos'] ) || ! is_array( $settings['repos'] ) ) { + return $defaults; + } + return $settings; }