From 6508679a1fa54449fb16cb145341c0770fa13f70 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 7 Jun 2026 23:29:33 -0400 Subject: [PATCH] Project write and git workspace tools for coding agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AbilityToolProjections only exposed read-only workspace tools (workspace_ls/read/grep/etc.) as model-facing tools, so agents in sandbox and chat runs could never receive file-mutating tools — the model had no write tool to call and would either fail or fabricate edits. Project the mutating workspace tools (workspace_write, workspace_edit, workspace_apply_patch, workspace_delete, and the git add/commit/push and worktree/PR tools), each gated behind requires_opt_in so a read-only task never receives file-mutating tools by default. They surface only when named via allow_only or an allow-mode tool policy. Extends the projection smoke test to assert the write tools project with requires_opt_in set. --- inc/Tools/AbilityToolProjections.php | 34 ++++++++++++++++++++++++ tests/smoke-ability-tool-projections.php | 21 +++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/inc/Tools/AbilityToolProjections.php b/inc/Tools/AbilityToolProjections.php index b4cbeac..eefa689 100644 --- a/inc/Tools/AbilityToolProjections.php +++ b/inc/Tools/AbilityToolProjections.php @@ -51,6 +51,23 @@ public static function projected_tools(): array { 'workspace_read' => self::workspace('datamachine-code/workspace-read'), 'workspace_grep' => self::workspace('datamachine-code/workspace-grep'), + 'workspace_write' => self::workspace_write('datamachine-code/workspace-write'), + 'workspace_edit' => self::workspace_write('datamachine-code/workspace-edit'), + 'workspace_apply_patch' => self::workspace_write('datamachine-code/workspace-apply-patch'), + 'workspace_delete' => self::workspace_write('datamachine-code/workspace-delete'), + 'workspace_git_status' => self::workspace_write('datamachine-code/workspace-git-status'), + 'workspace_git_log' => self::workspace_write('datamachine-code/workspace-git-log'), + 'workspace_git_diff' => self::workspace_write('datamachine-code/workspace-git-diff'), + 'workspace_git_pull' => self::workspace_write('datamachine-code/workspace-git-pull'), + 'workspace_git_add' => self::workspace_write('datamachine-code/workspace-git-add'), + 'workspace_git_commit' => self::workspace_write('datamachine-code/workspace-git-commit'), + 'workspace_git_push' => self::workspace_write('datamachine-code/workspace-git-push'), + 'workspace_git_rebase' => self::workspace_write('datamachine-code/workspace-git-rebase'), + 'workspace_git_reset' => self::workspace_write('datamachine-code/workspace-git-reset'), + 'workspace_worktree_add' => self::workspace_write('datamachine-code/workspace-worktree-add'), + 'workspace_pr_status' => self::workspace_write('datamachine-code/workspace-pr-status'), + 'workspace_pr_rebase' => self::workspace_write('datamachine-code/workspace-pr-rebase'), + 'list_github_issues' => self::github('datamachine-code/list-github-issues'), 'get_github_issue' => self::github('datamachine-code/get-github-issue'), 'list_github_pulls' => self::github('datamachine-code/list-github-pulls'), @@ -80,6 +97,23 @@ private static function workspace( string $ability ): array { ); } + /** + * Build a mutating workspace projection declaration. + * + * Write/git workspace tools require explicit opt-in (via `allow_only` or an + * allow-mode tool policy) before they are exposed to a model request, so a + * read-only inspection task never receives file-mutating tools by default. + * + * @return array + */ + private static function workspace_write( string $ability ): array { + return array( + 'ability' => $ability, + 'modes' => array( 'chat', 'pipeline' ), + 'requires_opt_in' => true, + ); + } + /** * Build a GitHub projection declaration. * diff --git a/tests/smoke-ability-tool-projections.php b/tests/smoke-ability-tool-projections.php index 6c3adc8..6e2e173 100644 --- a/tests/smoke-ability-tool-projections.php +++ b/tests/smoke-ability-tool-projections.php @@ -133,6 +133,27 @@ function dmc_projection_find_data_machine_conversation_manager(): ?string { $assert("{$tool_name} does not duplicate parameter schema", ! array_key_exists('parameters', $declaration)); } + // Write/git workspace tools must be projected so coding agents can mutate + // files, but gated behind explicit opt-in so read-only tasks never receive + // file-mutating tools by default. + $expected_write = array( + 'workspace_write' => 'datamachine-code/workspace-write', + 'workspace_edit' => 'datamachine-code/workspace-edit', + 'workspace_apply_patch' => 'datamachine-code/workspace-apply-patch', + 'workspace_delete' => 'datamachine-code/workspace-delete', + 'workspace_git_add' => 'datamachine-code/workspace-git-add', + 'workspace_git_commit' => 'datamachine-code/workspace-git-commit', + 'workspace_git_push' => 'datamachine-code/workspace-git-push', + ); + + foreach ( $expected_write as $tool_name => $ability_slug ) { + $declaration = $tools[ $tool_name ] ?? array(); + $assert("{$tool_name} is projected", isset($tools[ $tool_name ])); + $assert("{$tool_name} points at canonical ability", $ability_slug === ( $declaration['ability'] ?? '' )); + $assert("{$tool_name} is available to chat", in_array('chat', $declaration['modes'] ?? array(), true)); + $assert("{$tool_name} requires opt-in", true === ( $declaration['requires_opt_in'] ?? false )); + } + $workspace_result = dmc_projection_normalize_tool_result( array( 'success' => true,