Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions data-machine-code.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__ ) );

Expand Down
10 changes: 10 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 88 additions & 10 deletions inc/Workspace/Workspace.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand All @@ -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 ) );
}

Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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[<repo_name>]` 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<string, array<string, mixed>>}
*/
private function get_workspace_git_policies(): array {
$defaults = array(
Expand All @@ -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: { <name>: {...} } }.
*/
$settings = apply_filters( 'datamachine_workspace_git_policies', $settings );

if ( ! is_array( $settings ) || ! isset( $settings['repos'] ) || ! is_array( $settings['repos'] ) ) {
return $defaults;
}

return $settings;
}

Expand Down