dev-bubble 0.7.8__tar.gz → 0.7.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/.claude/CLAUDE.md +3 -3
  2. {dev_bubble-0.7.8/dev_bubble.egg-info → dev_bubble-0.7.10}/PKG-INFO +7 -9
  3. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/README.md +6 -8
  4. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/SPEC.md +102 -50
  5. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/__init__.py +1 -1
  6. dev_bubble-0.7.8/bubble/claude.py → dev_bubble-0.7.10/bubble/ai.py +136 -43
  7. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/auth_proxy.py +79 -123
  8. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/cli.py +99 -64
  9. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/settings.py +67 -47
  10. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/config.py +24 -19
  11. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/data/skill.md +11 -1
  12. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/finalization.py +10 -5
  13. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/github_token.py +41 -24
  14. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/gh.sh +1 -0
  15. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/vscode.sh +80 -135
  16. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/provisioning.py +2 -2
  17. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/remote.py +9 -9
  18. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/security.py +1 -93
  19. {dev_bubble-0.7.8 → dev_bubble-0.7.10/dev_bubble.egg-info}/PKG-INFO +7 -9
  20. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/SOURCES.txt +2 -2
  21. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/conftest.py +2 -2
  22. dev_bubble-0.7.8/tests/test_claude.py → dev_bubble-0.7.10/tests/test_ai.py +239 -85
  23. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_auth_proxy.py +80 -86
  24. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_branch_no_target.py +1 -1
  25. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_claude_projects_symlink.py +28 -28
  26. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_config.py +64 -137
  27. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_github_token.py +148 -17
  28. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_graphql_validator.py +0 -38
  29. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_mounts.py +8 -8
  30. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_remote.py +6 -6
  31. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_security.py +22 -112
  32. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_tools.py +0 -12
  33. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/.github/workflows/ci.yml +0 -0
  34. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/.github/workflows/publish.yml +0 -0
  35. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/.gitignore +0 -0
  36. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/CHANGELOG.md +0 -0
  37. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/LICENSE +0 -0
  38. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/__main__.py +0 -0
  39. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/automation.py +0 -0
  40. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/clean.py +0 -0
  41. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/clone.py +0 -0
  42. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/cloud.py +0 -0
  43. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/cloud_types.py +0 -0
  44. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/__init__.py +0 -0
  45. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/cloud_cmd.py +0 -0
  46. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/completion.py +0 -0
  47. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/doctor.py +0 -0
  48. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/images.py +0 -0
  49. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/infrastructure.py +0 -0
  50. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/lifecycle.py +0 -0
  51. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/list_cmd.py +0 -0
  52. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/relay_cmd.py +0 -0
  53. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/remote_cmd.py +0 -0
  54. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/security_cmd.py +0 -0
  55. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/status_cmd.py +0 -0
  56. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/container_helpers.py +0 -0
  57. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/default_repos.json +0 -0
  58. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/git_store.py +0 -0
  59. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/graphql_validator.py +0 -0
  60. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/hooks/__init__.py +0 -0
  61. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/hooks/lean.py +0 -0
  62. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/hooks/python.py +0 -0
  63. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/image_management.py +0 -0
  64. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/__init__.py +0 -0
  65. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/builder.py +0 -0
  66. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/base.sh +0 -0
  67. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/cloud-init.sh +0 -0
  68. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/lean-toolchain.sh +0 -0
  69. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/lean.sh +0 -0
  70. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/python.sh +0 -0
  71. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/claude.sh +0 -0
  72. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/codex.sh +0 -0
  73. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/elan.sh +0 -0
  74. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/emacs.sh +0 -0
  75. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/neovim.sh +0 -0
  76. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/pins.json +0 -0
  77. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/lean.py +0 -0
  78. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/lifecycle.py +0 -0
  79. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/naming.py +0 -0
  80. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/native.py +0 -0
  81. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/network.py +0 -0
  82. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/notices.py +0 -0
  83. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/output.py +0 -0
  84. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/relay.py +0 -0
  85. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/repo_registry.py +0 -0
  86. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/runtime/__init__.py +0 -0
  87. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/runtime/base.py +0 -0
  88. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/runtime/colima.py +0 -0
  89. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/runtime/incus.py +0 -0
  90. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/setup.py +0 -0
  91. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/skill.py +0 -0
  92. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/spinner.py +0 -0
  93. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/target.py +0 -0
  94. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/token_store.py +0 -0
  95. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/tools.py +0 -0
  96. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/tunnel.py +0 -0
  97. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/vscode.py +0 -0
  98. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/config/com.bubble.git-update.plist +0 -0
  99. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/config/com.bubble.image-refresh.plist +0 -0
  100. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/config/com.bubble.relay-daemon.plist +0 -0
  101. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/conftest.py +0 -0
  102. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/dependency_links.txt +0 -0
  103. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/entry_points.txt +0 -0
  104. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/requires.txt +0 -0
  105. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/top_level.txt +0 -0
  106. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/pyproject.toml +0 -0
  107. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/setup.cfg +0 -0
  108. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_build_lock.py +0 -0
  109. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_cloud.py +0 -0
  110. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_completion.py +0 -0
  111. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_customize.py +0 -0
  112. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_editor.py +0 -0
  113. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_git_store.py +0 -0
  114. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_hooks.py +0 -0
  115. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_integration.py +0 -0
  116. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_lifecycle.py +0 -0
  117. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_list_columns.py +0 -0
  118. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_list_remote.py +0 -0
  119. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_multi_target.py +0 -0
  120. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_naming.py +0 -0
  121. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_native.py +0 -0
  122. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_network.py +0 -0
  123. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_notices.py +0 -0
  124. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_relay.py +0 -0
  125. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_repo_registry.py +0 -0
  126. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_skill.py +0 -0
  127. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_spinner.py +0 -0
  128. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_status.py +0 -0
  129. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_target.py +0 -0
  130. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_tunnel.py +0 -0
  131. {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_vscode.py +0 -0
@@ -139,15 +139,15 @@ The auth proxy (`auth_proxy.py`) provides repo-scoped GitHub authentication with
139
139
 
140
140
  **gh CLI flow:** `gh` configured with `http_unix_socket: /bubble/gh-proxy.sock` (via `GH_CONFIG_DIR=/etc/bubble/gh`) → sends requests through Unix socket → proxy validates token from `Authorization` header → enforces access level (REST repo-scoping, GraphQL mutation filtering) → adds real token → forwards to `https://api.github.com`.
141
141
 
142
- **GraphQL validation** (`graphql_validator.py`): GraphQL access is controlled by two independent axes — `graphql_read` and `graphql_write` — each supporting `whitelisted`, `unrestricted`, or `none` modes. The default is `whitelisted` for both. In whitelisted mode, a lightweight tokenizer/parser validates structure (single operation, single top-level field, no aliases/directives, no fragments in mutations) and semantics. Read validation repo-scopes `repository` queries via variables, verifies `node` queries via pre-flight ownership checks, and checks second-level fields against an allowlist. Write validation checks mutations against an allowlist (createPullRequest, addComment, mergePullRequest, etc.) with repo-scoping via repositoryId comparison or pre-flight node ownership verification. Old tokens without `graphql_*` fields derive policies from the legacy `level` field for backward compatibility.
142
+ **GraphQL validation** (`graphql_validator.py`): GraphQL access is controlled by two independent axes — `graphql_read` and `graphql_write` — each supporting `whitelisted`, `unrestricted`, or `none` modes. The default is `whitelisted` for both. In whitelisted mode, a lightweight tokenizer/parser validates structure (single operation, single top-level field, no aliases/directives, no fragments in mutations) and semantics. Read validation repo-scopes `repository` queries via variables, verifies `node` queries via pre-flight ownership checks, and checks second-level fields against an allowlist. Write validation checks mutations against an allowlist (createPullRequest, addComment, mergePullRequest, etc.) with repo-scoping via repositoryId comparison or pre-flight node ownership verification.
143
143
 
144
- **REST security:** REST paths validated against `/repos/{owner}/{repo}/...` (repo-scoped). REST level defaults to `LEVEL_GH_READWRITE` since path validation already constrains access. API redirects (e.g. CI log downloads) followed with hardened rules: GET/HEAD only, HTTPS only, allowlisted hosts, max 2 hops, auth headers stripped. GitHub 4xx errors are passed through to clients (not collapsed to 502).
144
+ **REST security:** REST paths validated against `/repos/{owner}/{repo}/...` (repo-scoped). All HTTP methods are allowed when REST access is enabled, since path validation already constrains access to the scoped repo. API redirects (e.g. CI log downloads) followed with hardened rules: GET/HEAD only, HTTPS only, allowlisted hosts, max 2 hops, auth headers stripped. GitHub 4xx errors are passed through to clients (not collapsed to 502).
145
145
 
146
146
  **Local bubbles:** Exposed via Incus proxy devices — TCP for git, Unix socket for gh (`listen=unix:/bubble/gh-proxy.sock`).
147
147
 
148
148
  **Remote/cloud bubbles:** SSH reverse tunnel forwards the local proxy port. Incus proxy devices on the remote expose both TCP and Unix socket endpoints.
149
149
 
150
- **Token management:** Per-container tokens in `~/.bubble/auth-tokens.json` map to `{container, owner, repo, level, graphql_read, graphql_write}`. Tokens are cleaned up on `bubble pop`. The daemon is managed via launchd/systemd.
150
+ **Token management:** Per-container tokens in `~/.bubble/auth-tokens.json` map to `{container, owner, repo, rest_api, graphql_read, graphql_write}`. Tokens are cleaned up on `bubble pop`. The daemon is managed via launchd/systemd.
151
151
 
152
152
  ### Security Model
153
153
  The `user` account has no sudo and a locked password. Network allowlisting is applied on container creation. SSH keys are injected via `incus file push` (not shell interpolation). All user-supplied values in shell commands are quoted with `shlex.quote()`. Each container mounts only its specific bare repo, not the entire git store.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dev-bubble
3
- Version: 0.7.8
3
+ Version: 0.7.10
4
4
  Summary: Containerized development environments powered by Incus
5
5
  Author-email: Kim Morrison <kim@tqft.net>
6
6
  License-Expression: Apache-2.0
@@ -43,7 +43,7 @@ We assume that you work using VSCode, emacs, or neovim, and that you collaborate
43
43
 
44
44
  ```bash
45
45
  # Install
46
- uv tool install dev-bubble
46
+ uv tool install git+https://github.com/kim-em/bubble.git
47
47
 
48
48
  # Open a bubble for a GitHub PR — just paste the URL, and you get a containerized VSCode window!
49
49
  bubble https://github.com/leanprover-community/mathlib4/pull/35219
@@ -124,11 +124,9 @@ bubble pop mathlib4-pr-35219
124
124
  ## Development Install
125
125
 
126
126
  ```bash
127
- # Install from GitHub
128
- uv tool install git+https://github.com/kim-em/bubble.git
129
-
130
- # For development
131
- uv pip install -e '.[dev]'
127
+ # Install from a local clone (editable — always uses latest code)
128
+ git clone https://github.com/kim-em/bubble.git
129
+ uv tool install --force --editable bubble
132
130
  ```
133
131
 
134
132
  ## How It Works
@@ -263,10 +261,10 @@ bubble mathlib4/issues/42
263
261
 
264
262
  Claude is instructed to read the issue, implement a fix on the `issue-<number>` branch, and open a PR. This turns `bubble 42` (for an issue) into an autonomous coding agent workflow.
265
263
 
266
- You can also provide a custom prompt for any bubble via the `BUBBLE_CLAUDE_PROMPT` environment variable:
264
+ You can also provide a custom prompt for any bubble via the `BUBBLE_AI_PROMPT` environment variable:
267
265
 
268
266
  ```bash
269
- BUBBLE_CLAUDE_PROMPT="Refactor the parser module" bubble leanprover/lean4
267
+ BUBBLE_AI_PROMPT="Refactor the parser module" bubble leanprover/lean4
270
268
  ```
271
269
 
272
270
  Requirements: Claude Code must be installed in the container (see tool settings above), the `gh` CLI must be available on the host (for fetching issue/PR metadata), and the default VS Code editor must be used. With `--shell` or `--no-interactive`, the prompt is not injected. If `gh` is unavailable or the API call fails, bubble proceeds without injecting a prompt.
@@ -9,7 +9,7 @@ We assume that you work using VSCode, emacs, or neovim, and that you collaborate
9
9
 
10
10
  ```bash
11
11
  # Install
12
- uv tool install dev-bubble
12
+ uv tool install git+https://github.com/kim-em/bubble.git
13
13
 
14
14
  # Open a bubble for a GitHub PR — just paste the URL, and you get a containerized VSCode window!
15
15
  bubble https://github.com/leanprover-community/mathlib4/pull/35219
@@ -90,11 +90,9 @@ bubble pop mathlib4-pr-35219
90
90
  ## Development Install
91
91
 
92
92
  ```bash
93
- # Install from GitHub
94
- uv tool install git+https://github.com/kim-em/bubble.git
95
-
96
- # For development
97
- uv pip install -e '.[dev]'
93
+ # Install from a local clone (editable — always uses latest code)
94
+ git clone https://github.com/kim-em/bubble.git
95
+ uv tool install --force --editable bubble
98
96
  ```
99
97
 
100
98
  ## How It Works
@@ -229,10 +227,10 @@ bubble mathlib4/issues/42
229
227
 
230
228
  Claude is instructed to read the issue, implement a fix on the `issue-<number>` branch, and open a PR. This turns `bubble 42` (for an issue) into an autonomous coding agent workflow.
231
229
 
232
- You can also provide a custom prompt for any bubble via the `BUBBLE_CLAUDE_PROMPT` environment variable:
230
+ You can also provide a custom prompt for any bubble via the `BUBBLE_AI_PROMPT` environment variable:
233
231
 
234
232
  ```bash
235
- BUBBLE_CLAUDE_PROMPT="Refactor the parser module" bubble leanprover/lean4
233
+ BUBBLE_AI_PROMPT="Refactor the parser module" bubble leanprover/lean4
236
234
  ```
237
235
 
238
236
  Requirements: Claude Code must be installed in the container (see tool settings above), the `gh` CLI must be available on the host (for fetching issue/PR metadata), and the default VS Code editor must be used. With `--shell` or `--no-interactive`, the prompt is not injected. If `gh` is unavailable or the API call fails, bubble proceeds without injecting a prompt.
@@ -56,7 +56,7 @@ equivalent to `bubble open <url>`.
56
56
  | `-b`, `--new-branch` | string | | Create a new branch |
57
57
  | `--base` | string | | Base branch for `-b` |
58
58
  | `--mount` | string (repeatable) | | Mount host dir into container |
59
- | `--claude-config/--no-claude-config` | flag | enabled | Mount ~/.claude config read-only |
59
+ | `--ai-config/--no-ai-config` | flag | enabled | Mount AI provider configs read-only |
60
60
  | `--claude-credentials/--no-claude-credentials` | flag | enabled | Mount Claude credentials |
61
61
  | `--codex-credentials/--no-codex-credentials` | flag | enabled | Mount Codex credentials |
62
62
  | `--ssh HOST` | string | | Run on remote host |
@@ -339,9 +339,16 @@ of the versioned image for next time.
339
339
  > compromised container could write poisoned `.olean` files that subsequent
340
340
  > containers would pick up via `lake exe cache get`. The cache is *not* shared
341
341
  > with `lake exe cache` run on the host — only bubble containers are affected.
342
- > Set `shared-cache` to `off` (via `bubble security set shared-cache off`) to
343
- > mount the cache read-only and prevent future poisoning. If you suspect the
344
- > cache has already been compromised, delete `~/.bubble/mathlib-cache/`.
342
+ > Three modes are available via `bubble security set shared-cache`:
343
+ > - `on` (default): shared read-write fastest, but vulnerable to cache
344
+ > poisoning across containers
345
+ > - `overlay`: shared cache mounted read-only with a per-container writable
346
+ > overlayfs layer — fast reads from the shared cache with isolated writes
347
+ > that don't affect other containers (recommended for security-conscious use)
348
+ > - `off`: shared cache mounted read-only — prevents future poisoning
349
+ >
350
+ > If you suspect the cache has already been compromised, delete
351
+ > `~/.bubble/mathlib-cache/`.
345
352
 
346
353
  **Post-clone behavior:**
347
354
  - Pre-populate Lake dependencies from `lake-manifest.json` (see 2.4)
@@ -670,6 +677,7 @@ Each tool has:
670
677
  | `elan` | 10 | `elan` | — |
671
678
  | `claude` | 50 | `claude` | `api.anthropic.com` |
672
679
  | `codex` | 50 | `codex` | `api.openai.com` |
680
+ | `gh` | 50 | `gh` | — |
673
681
  | `vscode` | 90 | `code` | VS Code marketplace domains |
674
682
  | `emacs` | 90 | `emacs` | — |
675
683
  | `neovim` | 90 | `nvim` | — |
@@ -701,6 +709,10 @@ versions from a `pins.json` file.
701
709
 
702
710
  **`bubble tools set TOOL yes|no|auto`** — configure a tool.
703
711
 
712
+ **VS Code extension auto-install:** When both `claude` and `vscode` tools are
713
+ enabled, the Claude Code VS Code extension (`anthropic.claude-code`) is
714
+ pre-installed in the base image.
715
+
704
716
  ### 6.4 Drift detection
705
717
 
706
718
  A content-aware hash of the enabled tool set (tool names + script contents +
@@ -912,7 +924,7 @@ validation already confirmed the repo exists).
912
924
 
913
925
  ### 10.1 Claude Code config mounting
914
926
 
915
- When `--claude-config` is enabled (default), mount specific items from
927
+ When `--ai-config` is enabled (default), mount specific items from
916
928
  `~/.claude/` into `/home/user/.claude/` read-only:
917
929
  - `CLAUDE.md`, `settings.json`, `skills/`, `keybindings.json`, `commands/`
918
930
 
@@ -922,6 +934,12 @@ Credential files (`.credentials.json`) are mounted by default. Disable with
922
934
  **Symlink safety:** Reject symlinks that escape `~/.claude/` to prevent
923
935
  exposing arbitrary host files.
924
936
 
937
+ **Settings pre-population:** During container finalization, `~/.claude.json` is
938
+ written in all containers (not just those with AI task injection). Allowlisted
939
+ settings (theme, onboarding state) are copied from the host, project trust is
940
+ pre-configured for the container's project directory, and
941
+ `hasCompletedOnboarding` is set to skip the first-run wizard.
942
+
925
943
  ### 10.2 Codex config mounting
926
944
 
927
945
  Similar to Claude. Config: `config.toml` (read-only). Credentials: `auth.json`
@@ -941,27 +959,33 @@ Similar to Claude. Config: `config.toml` (read-only). Credentials: `auth.json`
941
959
  ### 10.4 GitHub auth proxy
942
960
 
943
961
  An HTTP reverse proxy on the host provides GitHub authentication without
944
- exposing the host's token. Git and REST API requests are repo-scoped;
945
- GraphQL requests are operation-validated (queries vs mutations) but not
946
- repo-scoped see access levels below.
962
+ exposing the host's token. The access level is controlled by the unified
963
+ `github` security setting, which picks one level from a graduated escalation
964
+ ladder (see Security section).
947
965
 
948
966
  **Port:** 7654 (default, configurable).
949
967
 
950
- **Flow:**
951
- 1. Container git is configured with `url.insteadOf` to route HTTPS through the proxy
952
- 2. Container sends request with `X-Bubble-Token` header
953
- 3. Proxy validates token against `~/.bubble/auth-tokens.json` (mode 0600)
954
- 4. For git/REST: proxy checks path matches the allowed `owner/repo`. For GraphQL: proxy validates operation type (queries allowed at level 3, mutations require level 4) but does not scope to a specific repo
955
- 5. Proxy adds `Authorization: token <real-token>` header
956
- 6. Proxy forwards to `https://github.com`
957
- 7. Response returned to container
968
+ **Git flow:** Container git → `url.insteadOf` rewrites to
969
+ `http://127.0.0.1:7654/git/...` proxy validates `X-Bubble-Token` header
970
+ checks path matches allowed `owner/repo` → adds `Authorization` header
971
+ forwards to `https://github.com` returns response.
972
+
973
+ **gh CLI flow:** `gh` configured with `http_unix_socket: /bubble/gh-proxy.sock`
974
+ (via `GH_CONFIG_DIR=/etc/bubble/gh`) sends requests through Unix socket →
975
+ proxy validates token enforces access level (REST repo-scoping, GraphQL
976
+ allowlist validation) → adds real token → forwards to `https://api.github.com`.
958
977
 
959
978
  **Token format:** `~/.bubble/auth-tokens.json`
960
979
  ```json
961
980
  {
962
- "hex_token": {"container": "name", "owner": "owner", "repo": "repo"}
981
+ "hex_token": {
982
+ "container": "name",
983
+ "owner": "owner",
984
+ "repo": "repo",
985
+ "graphql_read": "whitelisted",
986
+ "graphql_write": "whitelisted"
987
+ }
963
988
  }
964
- ```
965
989
 
966
990
  **Allowed paths (git smart HTTP only):**
967
991
  - `GET /git/{owner}/{repo}[.git]/info/refs?service=git-upload-pack`
@@ -969,33 +993,53 @@ repo-scoped — see access levels below.
969
993
  - `POST /git/{owner}/{repo}[.git]/git-upload-pack`
970
994
  - `POST /git/{owner}/{repo}[.git]/git-receive-pack`
971
995
 
972
- **Access levels (per-container):**
973
- | Level | Description | Scope |
974
- |-------|-------------|-------|
975
- | 1 | Git smart HTTP only (push/pull) | Repo-scoped |
976
- | 2 | Git + REST API read-only | Repo-scoped (REST paths validated against `/repos/{owner}/{repo}/...`) |
977
- | 3 (default) | Git + gh read-only (REST read + GraphQL queries) | Git and REST are repo-scoped; **GraphQL is account-wide** — queries can read any data the host token can access |
978
- | 4 | Git + gh read-write (REST + GraphQL + mutations) | Git and REST are repo-scoped; **GraphQL queries and mutations are account-wide** |
979
-
980
- > **Note:** GitHub's GraphQL API does not support path-based scoping.
981
- > At the default level 3, a container can query any repository, org membership,
982
- > or user data readable by the host token. To restrict containers to git-only
983
- > access, use `bubble security set github-api off`.
996
+ **Access levels** (set via `bubble security set github <level>`):
997
+
998
+ | `github` level | Behavior |
999
+ |----------------|----------|
1000
+ | `off` | No GitHub access at all |
1001
+ | `basic` | Git push/pull only (proxy rewrites, repo-scoped) |
1002
+ | `rest` | + repo-scoped REST API |
1003
+ | `allowlist-read-graphql` | + allowlisted read-only GraphQL queries |
1004
+ | `allowlist-write-graphql` (default) | + allowlisted GraphQL mutations |
1005
+ | `write-graphql` | + arbitrary GraphQL, no allowlist filtering |
1006
+ | `direct` | Inject the raw token into the container, no proxy |
1007
+
1008
+ **`direct` mode:** The host's `GH_TOKEN` and `GITHUB_TOKEN` environment
1009
+ variables are injected into the container via `/etc/profile.d`. No proxy is
1010
+ used — git and `gh` connect directly to GitHub. Network allowlisting permits
1011
+ direct GitHub access (iptables allows `github.com`).
1012
+
1013
+ **GraphQL validation** (`allowlist-read-graphql` and `allowlist-write-graphql`
1014
+ levels): GraphQL access is controlled by two independent axes — `graphql_read`
1015
+ and `graphql_write` — each supporting `whitelisted`, `unrestricted`, or `none`
1016
+ modes. In whitelisted mode, a lightweight tokenizer/parser validates structure
1017
+ (single operation, single top-level field, no aliases/directives, no fragments
1018
+ in mutations) and semantics. Read validation repo-scopes `repository` queries
1019
+ via variables and verifies `node` queries via pre-flight ownership checks.
1020
+ Write validation checks mutations against an allowlist with repo-scoping via
1021
+ repositoryId comparison or pre-flight node ownership verification.
1022
+
1023
+ **REST security:** REST paths validated against `/repos/{owner}/{repo}/...`
1024
+ (repo-scoped). API redirects (e.g., CI log downloads) followed with hardened
1025
+ rules: GET/HEAD only, HTTPS only, allowlisted hosts
1026
+ (`*.blob.core.windows.net`, `*.githubusercontent.com`), max 2 hops, auth
1027
+ headers stripped. GitHub 4xx errors are passed through to clients.
984
1028
 
985
1029
  **Security:**
986
1030
  - Path canonicalization: reject encoded separators, dot-segments, duplicate slashes
987
- - No redirect following (returns redirects as-is to prevent token leakage)
988
1031
  - Pinned to `github.com:443` with TLS verification
989
1032
  - Ignores `HTTPS_PROXY`/`ALL_PROXY` environment variables
990
1033
  - Rate limited: 60/minute, 600/hour per container
991
1034
  - Maximum request body: 256 MB
992
1035
 
993
- **Local containers:** Exposed via Incus proxy device connecting host TCP port to
994
- container `127.0.0.1:7654`.
1036
+ **Local containers:** Exposed via Incus proxy devices TCP for git, Unix
1037
+ socket for gh (`listen=unix:/bubble/gh-proxy.sock`).
995
1038
 
996
- **Remote containers:** SSH reverse tunnel (`ssh -R 7654:127.0.0.1:7654 remote`)
997
- forwards the local proxy port to the remote host. Tunnel PID files in
998
- `~/.bubble/tunnels/`. One tunnel per remote host (shared across containers).
1039
+ **Remote containers:** SSH reverse tunnel forwards the local proxy port to the
1040
+ remote host. Incus proxy devices on the remote expose both TCP and Unix socket
1041
+ endpoints. Tunnel PID files in `~/.bubble/tunnels/`. One tunnel per remote
1042
+ host (shared across containers).
999
1043
 
1000
1044
  **Daemon management:** launchd (`com.bubble.auth-proxy`) or systemd
1001
1045
  (`bubble-auth-proxy.service`).
@@ -1067,6 +1111,12 @@ location = "fsn1"
1067
1111
  server_name = "bubble-cloud"
1068
1112
  default = false
1069
1113
 
1114
+ [ai]
1115
+ preferred = "claude"
1116
+ autonomy = "plan"
1117
+ second_opinion = "auto"
1118
+ second_opinion_provider = "codex"
1119
+
1070
1120
  [claude]
1071
1121
  credentials = true
1072
1122
 
@@ -1087,11 +1137,10 @@ Configuration is managed through dedicated subcommands rather than a generic
1087
1137
  get/set interface:
1088
1138
 
1089
1139
  - `bubble tools set TOOL yes|no|auto` — configure tool installation
1090
- - `bubble claude credentials on|off` — toggle Claude credential mounting
1091
- - `bubble codex credentials on|off` — toggle Codex credential mounting
1140
+ - `bubble ai credentials on|off [--provider claude|codex]` — toggle AI credential mounting
1092
1141
  - `bubble security set NAME on|off|auto` — configure security settings
1093
1142
  - `bubble config set KEY VALUE` — set security settings (alias)
1094
- - `bubble config symlink-claude-projects` — symlink Claude projects directory
1143
+ - `bubble config symlink-ai-projects` — symlink AI projects directory
1095
1144
 
1096
1145
  ---
1097
1146
 
@@ -1099,19 +1148,18 @@ get/set interface:
1099
1148
 
1100
1149
  ### Security settings
1101
1150
 
1102
- Every isolation-weakening feature is individually configurable with three
1103
- values: `auto`, `on`, `off`.
1151
+ Most settings are individually configurable with three values: `auto`, `on`,
1152
+ `off`. The `github` setting uses graduated named levels (see auth proxy
1153
+ section) and `shared-cache` accepts an additional `overlay` value.
1104
1154
 
1105
1155
  | Setting | Auto default | Description |
1106
1156
  |---------|-------------|-------------|
1107
- | `shared-cache` | on | Writable shared mounts (mathlib cache) — see [Lean 4 hook](#22-lean-4-hook) security note |
1157
+ | `shared-cache` | on | Writable shared mounts (mathlib cache) — `on`, `off`, or `overlay`. See [Lean 4 hook](#22-lean-4-hook) security note |
1108
1158
  | `user-mounts` | on | `--mount` flag support |
1109
1159
  | `git-manifest-trust` | on | Auto-clone Lake manifest dependencies |
1110
1160
  | `claude-credentials` | on | Mount Claude credentials into containers |
1111
1161
  | `codex-credentials` | on | Mount Codex credentials into containers |
1112
- | `github-auth` | on | Repo-scoped GitHub auth via proxy (git push/pull) |
1113
- | `github-api` | on | GitHub API access via auth proxy: REST is repo-scoped; **GraphQL queries are read-only but account-wide** (can read any repo the host token can access). Set to `off` for git-only, or `read-write` for mutations |
1114
- | `github-token-inject` | off | Direct GitHub token injection (bypasses proxy) |
1162
+ | `github` | allowlist-write-graphql | Unified GitHub access level graduated from `off` through `basic`, `rest`, `allowlist-read-graphql`, `allowlist-write-graphql`, `write-graphql`, to `direct` (see [auth proxy](#104-github-auth-proxy)) |
1115
1163
  | `relay` | on | Bubble-in-bubble relay |
1116
1164
  | `host-key-trust` | on | Disable SSH StrictHostKeyChecking |
1117
1165
 
@@ -1120,15 +1168,15 @@ user to `bubble security`. Suppressed by `BUBBLE_QUIET_SECURITY=1`.
1120
1168
 
1121
1169
  **Commands:**
1122
1170
  - `bubble security` — show full security posture
1123
- - `bubble security set NAME on|off|auto` set individual setting
1171
+ - `bubble security set NAME VALUE` — set individual setting (most accept `on|off|auto`; `github` accepts named levels; `shared-cache` also accepts `overlay`)
1124
1172
  - `bubble security permissive` — set all to `on`
1125
1173
  - `bubble security lockdown` — set all to `off`
1126
1174
  - `bubble security default` — reset all to `auto`
1127
1175
 
1128
1176
  **GitHub network access:** Direct GitHub network access (via iptables) is only
1129
- allowed when `github-token-inject` is enabled (level 5). At all other auth
1130
- levels (0–4), iptables blocks direct GitHub traffic and forces it through the
1131
- auth proxy on loopback, which enforces repo-scoping and rate limits.
1177
+ allowed when `github` is set to `direct`. At all other levels, iptables blocks
1178
+ direct GitHub traffic and forces it through the auth proxy on loopback, which
1179
+ enforces rate limits and (for levels below `write-graphql`) repo-scoping.
1132
1180
 
1133
1181
  ---
1134
1182
 
@@ -1160,6 +1208,10 @@ container lifecycle, but a complete implementation should include them:
1160
1208
  - `bubble remote clear-default` — clear default remote host
1161
1209
  - `bubble remote status` — show remote configuration and list remote bubbles
1162
1210
  - `bubble gh status` — show GitHub authentication status
1211
+ - `bubble completion <shell>` — output shell completion script (`bash`, `zsh`, `fish`); `--install` writes to a persistent location
1212
+ - `bubble ai set autonomy read|plan|implement|pr|merge` — set AI autonomy level
1213
+ - `bubble ai set second-opinion auto|on|off` — toggle second-opinion provider
1214
+ - `bubble ai status` — show AI provider settings and credential status
1163
1215
 
1164
1216
  ---
1165
1217
 
@@ -1190,7 +1242,7 @@ container lifecycle, but a complete implementation should include them:
1190
1242
  | `~/.bubble/cloud_key` | SSH private key for cloud (ed25519, mode 0600) |
1191
1243
  | `~/.bubble/cloud_key.pub` | SSH public key for cloud |
1192
1244
  | `~/.bubble/known_hosts` | SSH known_hosts for cloud |
1193
- | `~/.bubble/claude-projects/` | Claude session state for containers |
1245
+ | `~/.bubble/ai-projects/` | AI provider session state for containers |
1194
1246
  | `~/.ssh/config.d/bubble` | Auto-managed SSH config entries |
1195
1247
 
1196
1248
  ---
@@ -1,3 +1,3 @@
1
1
  """bubble: Containerized development environments."""
2
2
 
3
- __version__ = "0.7.8"
3
+ __version__ = "0.7.10"
@@ -1,20 +1,52 @@
1
- """Claude Code integration for bubble containers."""
1
+ """AI provider integration for bubble containers.
2
+
3
+ Handles prompt generation and task injection for the configured preferred
4
+ AI provider (default: Claude Code). Provider-neutral where possible;
5
+ provider-specific details (binary names, env vars, prompt file paths)
6
+ are dispatched via the ``[ai] preferred`` config key.
7
+ """
2
8
 
3
9
  import json
4
10
  import re
5
11
  import shlex
6
12
  import subprocess
13
+ from pathlib import Path
7
14
 
8
15
  from .config import DATA_DIR
9
16
  from .runtime.base import ContainerRuntime
10
17
 
11
- # Claude task command: reads prompt from file, runs Claude with skip-permissions,
12
- # then deletes the prompt file so reopening is clean.
13
- CLAUDE_TASK_COMMAND = (
14
- "test -f .vscode/claude-prompt.txt && ANTHROPIC_API_KEY= CLAUDECODE="
15
- ' claude --dangerously-skip-permissions "$(cat .vscode/claude-prompt.txt)"'
16
- " && rm -f .vscode/claude-prompt.txt"
17
- )
18
+ # Provider-specific task commands.
19
+ # Each reads the prompt from .vscode/ai-prompt.txt, runs the AI tool
20
+ # with autonomous permissions, then deletes the prompt file.
21
+ _TASK_COMMANDS = {
22
+ "claude": (
23
+ "test -f .vscode/ai-prompt.txt && ANTHROPIC_API_KEY= CLAUDECODE="
24
+ ' claude --dangerously-skip-permissions "$(cat .vscode/ai-prompt.txt)"'
25
+ " && rm -f .vscode/ai-prompt.txt"
26
+ ),
27
+ "codex": (
28
+ "test -f .vscode/ai-prompt.txt &&"
29
+ ' codex --approval-mode full-auto "$(cat .vscode/ai-prompt.txt)"'
30
+ " && rm -f .vscode/ai-prompt.txt"
31
+ ),
32
+ }
33
+
34
+ SUPPORTED_PROVIDERS = frozenset(_TASK_COMMANDS)
35
+
36
+
37
+ def _task_command_for(provider: str) -> str:
38
+ """Return the VS Code task shell command for the given AI provider.
39
+
40
+ Raises ``ValueError`` for unknown providers so typos are caught early.
41
+ """
42
+ try:
43
+ return _TASK_COMMANDS[provider]
44
+ except KeyError:
45
+ supported = ", ".join(sorted(SUPPORTED_PROVIDERS))
46
+ raise ValueError(f"Unknown AI provider {provider!r} (supported: {supported})") from None
47
+
48
+
49
+ AI_TASK_COMMAND = _TASK_COMMANDS["claude"]
18
50
 
19
51
  TEMPLATES_DIR = DATA_DIR / "templates"
20
52
 
@@ -173,7 +205,7 @@ def generate_issue_prompt(
173
205
  second_opinion: str = "auto",
174
206
  config: dict | None = None,
175
207
  ) -> str | None:
176
- """Fetch GitHub issue details and generate a Claude prompt.
208
+ """Fetch GitHub issue details and generate an AI prompt.
177
209
 
178
210
  Returns the prompt string, or None if the issue can't be fetched.
179
211
  Uses a custom template from ~/.bubble/templates/issue.txt if present,
@@ -235,7 +267,7 @@ def generate_issue_prompt(
235
267
 
236
268
 
237
269
  def generate_pr_prompt(owner: str, repo: str, pr_num: str, branch: str) -> str | None:
238
- """Fetch GitHub PR details and generate a Claude prompt.
270
+ """Fetch GitHub PR details and generate an AI prompt.
239
271
 
240
272
  Returns the prompt string, or None if the PR can't be fetched.
241
273
  Uses a custom template from ~/.bubble/templates/pr.txt if present,
@@ -260,17 +292,92 @@ def generate_pr_prompt(owner: str, repo: str, pr_num: str, branch: str) -> str |
260
292
  )
261
293
 
262
294
 
263
- def inject_claude_task(
264
- runtime: ContainerRuntime, container: str, project_dir: str, prompt: str, quiet: bool = False
295
+ # Known-safe keys to copy from the host's ~/.claude.json.
296
+ # Only cosmetic/UX settings no credentials, MCP config, or host-specific paths.
297
+ _CLAUDE_JSON_SAFE_KEYS = frozenset(
298
+ {
299
+ "theme",
300
+ "hasCompletedOnboarding",
301
+ "numStartups",
302
+ "preferredNotifChannel",
303
+ "autoUpdaterStatus",
304
+ }
305
+ )
306
+
307
+
308
+ def setup_claude_settings(
309
+ runtime: ContainerRuntime,
310
+ container: str,
311
+ project_dir: str,
312
+ ):
313
+ """Pre-populate ~/.claude.json in the container to skip the first-run wizard.
314
+
315
+ Copies allowlisted settings (theme, onboarding state, etc.) from the host's
316
+ ~/.claude.json if it exists. Always ensures hasCompletedOnboarding=True
317
+ and pre-trusts the project directory. This runs for ALL bubbles, not just
318
+ those with AI task injection.
319
+
320
+ Best-effort: failures are logged but do not abort bubble creation.
321
+ """
322
+ host_claude_json = Path.home() / ".claude.json"
323
+
324
+ # Extract only allowlisted keys from host settings
325
+ settings: dict = {}
326
+ if host_claude_json.is_file():
327
+ try:
328
+ host_data = json.loads(host_claude_json.read_text())
329
+ if isinstance(host_data, dict):
330
+ settings = {k: v for k, v in host_data.items() if k in _CLAUDE_JSON_SAFE_KEYS}
331
+ except (OSError, json.JSONDecodeError):
332
+ pass
333
+
334
+ # Ensure onboarding is marked complete
335
+ settings["hasCompletedOnboarding"] = True
336
+ n = settings.get("numStartups", 0)
337
+ settings["numStartups"] = (n if isinstance(n, int) else 0) + 1
338
+
339
+ # Pre-trust the project directory
340
+ settings["projects"] = {
341
+ project_dir: {"hasTrustDialogAccepted": True, "allowedTools": []},
342
+ }
343
+
344
+ # Write to container (best-effort — don't abort bubble creation on failure)
345
+ settings_json = shlex.quote(json.dumps(settings, indent=2))
346
+ try:
347
+ runtime.exec(
348
+ container,
349
+ ["su", "-", "user", "-c", f"printf '%s' {settings_json} > ~/.claude.json"],
350
+ )
351
+ except Exception:
352
+ from .output import detail
353
+
354
+ detail("Warning: could not pre-populate Claude Code settings.", err=True)
355
+
356
+
357
+ def inject_ai_task(
358
+ runtime: ContainerRuntime,
359
+ container: str,
360
+ project_dir: str,
361
+ prompt: str,
362
+ config: dict | None = None,
363
+ quiet: bool = False,
265
364
  ):
266
- """Inject Claude auto-start task into a container's VS Code configuration.
365
+ """Inject AI auto-start task into a container's VS Code configuration.
366
+
367
+ Dispatches to the configured preferred AI provider (default: Claude).
267
368
 
268
- - Writes prompt to .vscode/claude-prompt.txt
269
- - Creates/updates .vscode/tasks.json with Claude task (runOn: folderOpen)
369
+ - Writes prompt to .vscode/ai-prompt.txt
370
+ - Creates/updates .vscode/tasks.json with AI task (runOn: folderOpen)
270
371
  - Configures .vscode/settings.json for automatic tasks
271
372
  - Adds generated files to git exclude
272
- - Pre-trusts the project directory in .claude.json
373
+ - Pre-trusts the project directory in the preferred provider's config
273
374
  """
375
+ provider = "claude"
376
+ if config:
377
+ provider = config.get("ai", {}).get("preferred", "claude")
378
+
379
+ task_command = _task_command_for(provider)
380
+
274
381
  q_dir = shlex.quote(project_dir)
275
382
  q_prompt = shlex.quote(prompt)
276
383
 
@@ -280,20 +387,21 @@ def inject_claude_task(
280
387
  # Write prompt to file
281
388
  runtime.exec(
282
389
  container,
283
- ["su", "-", "user", "-c", f"printf '%s' {q_prompt} > {q_dir}/.vscode/claude-prompt.txt"],
390
+ ["su", "-", "user", "-c", f"printf '%s' {q_prompt} > {q_dir}/.vscode/ai-prompt.txt"],
284
391
  )
285
392
 
286
- # Create or update tasks.json with Claude task
287
- claude_task = {
288
- "label": "Claude",
393
+ # Create or update tasks.json with AI task
394
+ ai_task = {
395
+ "label": "AI",
289
396
  "type": "shell",
290
- "command": CLAUDE_TASK_COMMAND,
397
+ "command": task_command,
291
398
  "runOptions": {"runOn": "folderOpen"},
292
399
  "presentation": {"reveal": "always", "panel": "dedicated"},
293
400
  }
294
- tasks_json_str = shlex.quote(json.dumps(claude_task))
401
+ tasks_json_str = shlex.quote(json.dumps(ai_task))
295
402
 
296
- # Script: if tasks.json exists, add Claude task; otherwise create new file
403
+ # Script: if tasks.json exists, add AI task (removing old Claude and AI labels);
404
+ # otherwise create new file
297
405
  script = (
298
406
  f"cd {q_dir} && "
299
407
  f"if [ -f .vscode/tasks.json ]; then "
@@ -301,7 +409,7 @@ def inject_claude_task(
301
409
  f"import json,sys; "
302
410
  f"t=json.load(open('.vscode/tasks.json')); "
303
411
  f"t['tasks']=[x for x in t.get('tasks',[])"
304
- f" if x.get('label')!='Claude']+[json.loads(sys.argv[1])]; "
412
+ f" if x.get('label') not in ('Claude','AI')]+[json.loads(sys.argv[1])]; "
305
413
  f"json.dump(t,open('.vscode/tasks.json','w'),indent=2)"
306
414
  f'" {tasks_json_str}; '
307
415
  f"else "
@@ -332,31 +440,16 @@ def inject_claude_task(
332
440
  f"cd {q_dir} && "
333
441
  f"GIT_DIR=$(git rev-parse --git-dir) && "
334
442
  f"mkdir -p $GIT_DIR/info && "
335
- f"for f in .vscode/claude-prompt.txt .vscode/settings.json .vscode/tasks.json; do "
443
+ f"for f in .vscode/ai-prompt.txt .vscode/claude-prompt.txt"
444
+ f" .vscode/settings.json .vscode/tasks.json; do "
336
445
  f' grep -qxF "$f" $GIT_DIR/info/exclude 2>/dev/null'
337
446
  f' || echo "$f" >> $GIT_DIR/info/exclude; '
338
447
  f"done"
339
448
  )
340
449
  runtime.exec(container, ["su", "-", "user", "-c", exclude_script])
341
450
 
342
- # Pre-trust the project directory and skip onboarding in .claude.json
343
- trust_script = (
344
- f'python3 -c "'
345
- f"import json,os; "
346
- f"p=os.path.expanduser('~/.claude.json'); "
347
- f"d=json.load(open(p)) if os.path.exists(p) else {{}}; "
348
- f"d['hasCompletedOnboarding']=True; "
349
- f"n=d.get('numStartups',0); d['numStartups']=(n if isinstance(n,int) else 0)+1; "
350
- f"d.setdefault('projects',{{}}); "
351
- f"proj=d['projects'].setdefault({shlex.quote(project_dir)!r},{{}}); " # noqa: E501
352
- f"proj['hasTrustDialogAccepted']=True; "
353
- f"proj.setdefault('allowedTools',[]); "
354
- f"json.dump(d,open(p,'w'),indent=2)"
355
- f'"'
356
- )
357
- runtime.exec(container, ["su", "-", "user", "-c", trust_script])
358
-
359
451
  if not quiet:
360
452
  from .output import detail
361
453
 
362
- detail("Claude Code task injected (will start on VS Code folder open).")
454
+ label = provider.capitalize()
455
+ detail(f"{label} task injected (will start on VS Code folder open).")