Routes LLM operations through the claude / codex CLI subscription.
| 121 | _patch_codex_tool_timeout() |
| 122 | # --- end stopgap -------------------------------------------------------------- |
| 123 | |
| 124 | |
| 125 | # --- claude-code allowedTools stopgap ----------------------------------------- |
| 126 | # Custom MCP-server tools (added via `--mcp-config`) are not auto-approved under |
| 127 | # `acceptEdits`, and `bypassPermissions` may be disabled by an org managed |
| 128 | # policy (see the cwd note below), so caw's `--dangerously-skip-permissions` |
| 129 | # alone is not enough: CodeWiki's own toolkit (str_replace_editor, |
| 130 | # read_code_components, generate_sub_module_documentation) gets denied |
| 131 | # ("you haven't granted it yet"), the agent writes nothing, and the run |
| 132 | # "succeeds" with an empty module tree. Grant the toolkit explicitly with |
| 133 | # `--allowedTools` using the permission rule syntax: |
| 134 | # https://code.claude.com/docs/en/settings#permission-rule-syntax |
| 135 | # `--allowedTools` flag: https://code.claude.com/docs/en/cli-reference |
| 136 | # caw's ClaudeCodeSession only ever emits `--disallowedTools`, so rewrite its |
| 137 | # `claude` command to add `--allowedTools mcp__<server>` for every server in |
| 138 | # the --mcp-config. The patch swaps the `subprocess` module reference INSIDE |
| 139 | # caw.providers.claude_code for a thin proxy — the global subprocess.Popen |
| 140 | # class stays untouched (isinstance / subclass safe) and no other claude |
| 141 | # invocation in this process is affected. Belongs upstream in caw; remove |
| 142 | # once it grows a first-class allowed_tools knob for its toolkit servers. |
| 143 | _CLAUDE_ALLOWED_PATCH_APPLIED = False |
| 144 | |
| 145 | |
| 146 | def _with_allowed_tools(cmd): |
| 147 | """Append ``--allowedTools mcp__<server>,...`` to a ``claude`` command. |
| 148 | |
| 149 | Pure ``list -> list`` transform. Applies only when *cmd* is a claude |
| 150 | invocation carrying ``--mcp-config`` and no explicit allow-list already; |
| 151 | otherwise (or on any error) *cmd* is returned unchanged. |
| 152 | """ |
| 153 | import json |
| 154 | |
| 155 | try: |
| 156 | if not ( |
| 157 | isinstance(cmd, (list, tuple)) |
| 158 | and cmd |
| 159 | and os.path.basename(str(cmd[0])) == "claude" |
| 160 | and "--mcp-config" in cmd |
| 161 | and "--allowedTools" not in cmd |
| 162 | and "--allowed-tools" not in cmd |
| 163 | ): |
| 164 | return cmd |
| 165 | # caw emits a single `--mcp-config <path>`; only the first occurrence |
| 166 | # is considered. |
| 167 | cfg_path = cmd[list(cmd).index("--mcp-config") + 1] |
| 168 | with open(cfg_path) as f: |
| 169 | servers = list(json.load(f).get("mcpServers", {}).keys()) |
| 170 | if not servers: |
| 171 | return cmd |
| 172 | allowed = ",".join(f"mcp__{s}" for s in servers) |
| 173 | logger.info("Injected --allowedTools for MCP servers: %s", servers) |
| 174 | return list(cmd) + ["--allowedTools", allowed] |
| 175 | except Exception as e: # noqa: BLE001 — never break the spawn on a patch hiccup |
| 176 | logger.warning("claude allowedTools patch skipped: %s", e) |
| 177 | return cmd |
| 178 | |
| 179 | |
| 180 | def _patch_claude_allowed_tools() -> None: |