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.
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/.claude/CLAUDE.md +3 -3
- {dev_bubble-0.7.8/dev_bubble.egg-info → dev_bubble-0.7.10}/PKG-INFO +7 -9
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/README.md +6 -8
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/SPEC.md +102 -50
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/__init__.py +1 -1
- dev_bubble-0.7.8/bubble/claude.py → dev_bubble-0.7.10/bubble/ai.py +136 -43
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/auth_proxy.py +79 -123
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/cli.py +99 -64
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/settings.py +67 -47
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/config.py +24 -19
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/data/skill.md +11 -1
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/finalization.py +10 -5
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/github_token.py +41 -24
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/gh.sh +1 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/vscode.sh +80 -135
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/provisioning.py +2 -2
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/remote.py +9 -9
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/security.py +1 -93
- {dev_bubble-0.7.8 → dev_bubble-0.7.10/dev_bubble.egg-info}/PKG-INFO +7 -9
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/SOURCES.txt +2 -2
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/conftest.py +2 -2
- dev_bubble-0.7.8/tests/test_claude.py → dev_bubble-0.7.10/tests/test_ai.py +239 -85
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_auth_proxy.py +80 -86
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_branch_no_target.py +1 -1
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_claude_projects_symlink.py +28 -28
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_config.py +64 -137
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_github_token.py +148 -17
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_graphql_validator.py +0 -38
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_mounts.py +8 -8
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_remote.py +6 -6
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_security.py +22 -112
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_tools.py +0 -12
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/.github/workflows/ci.yml +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/.github/workflows/publish.yml +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/.gitignore +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/CHANGELOG.md +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/LICENSE +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/__main__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/automation.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/clean.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/clone.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/cloud.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/cloud_types.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/__init__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/cloud_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/completion.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/doctor.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/images.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/infrastructure.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/lifecycle.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/list_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/relay_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/remote_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/security_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/commands/status_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/container_helpers.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/default_repos.json +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/git_store.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/graphql_validator.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/hooks/__init__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/hooks/lean.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/hooks/python.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/image_management.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/__init__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/builder.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/base.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/cloud-init.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/lean-toolchain.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/lean.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/python.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/claude.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/codex.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/elan.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/emacs.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/neovim.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/images/scripts/tools/pins.json +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/lean.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/lifecycle.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/naming.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/native.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/network.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/notices.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/output.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/relay.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/repo_registry.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/runtime/__init__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/runtime/base.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/runtime/colima.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/runtime/incus.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/setup.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/skill.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/spinner.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/target.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/token_store.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/tools.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/tunnel.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/bubble/vscode.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/config/com.bubble.git-update.plist +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/config/com.bubble.image-refresh.plist +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/config/com.bubble.relay-daemon.plist +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/conftest.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/dependency_links.txt +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/entry_points.txt +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/requires.txt +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/dev_bubble.egg-info/top_level.txt +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/pyproject.toml +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/setup.cfg +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_build_lock.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_cloud.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_completion.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_customize.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_editor.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_git_store.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_hooks.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_integration.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_lifecycle.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_list_columns.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_list_remote.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_multi_target.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_naming.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_native.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_network.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_notices.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_relay.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_repo_registry.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_skill.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_spinner.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_status.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_target.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.10}/tests/test_tunnel.py +0 -0
- {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.
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
128
|
-
|
|
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 `
|
|
264
|
+
You can also provide a custom prompt for any bubble via the `BUBBLE_AI_PROMPT` environment variable:
|
|
267
265
|
|
|
268
266
|
```bash
|
|
269
|
-
|
|
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
|
|
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
|
|
94
|
-
|
|
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 `
|
|
230
|
+
You can also provide a custom prompt for any bubble via the `BUBBLE_AI_PROMPT` environment variable:
|
|
233
231
|
|
|
234
232
|
```bash
|
|
235
|
-
|
|
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
|
-
| `--
|
|
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
|
-
>
|
|
343
|
-
>
|
|
344
|
-
>
|
|
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 `--
|
|
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.
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
**
|
|
951
|
-
1
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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": {
|
|
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 (
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
|
977
|
-
|
|
|
978
|
-
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
994
|
-
|
|
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
|
|
997
|
-
|
|
998
|
-
`~/.bubble/tunnels/`. One tunnel per remote
|
|
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
|
|
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-
|
|
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
|
-
|
|
1103
|
-
|
|
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) —
|
|
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
|
|
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`
|
|
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
|
|
1130
|
-
|
|
1131
|
-
|
|
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/
|
|
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,20 +1,52 @@
|
|
|
1
|
-
"""
|
|
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
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
264
|
-
|
|
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
|
|
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/
|
|
269
|
-
- Creates/updates .vscode/tasks.json with
|
|
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
|
|
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/
|
|
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
|
|
287
|
-
|
|
288
|
-
"label": "
|
|
393
|
+
# Create or update tasks.json with AI task
|
|
394
|
+
ai_task = {
|
|
395
|
+
"label": "AI",
|
|
289
396
|
"type": "shell",
|
|
290
|
-
"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(
|
|
401
|
+
tasks_json_str = shlex.quote(json.dumps(ai_task))
|
|
295
402
|
|
|
296
|
-
# Script: if tasks.json exists, add
|
|
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')
|
|
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/
|
|
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
|
-
|
|
454
|
+
label = provider.capitalize()
|
|
455
|
+
detail(f"{label} task injected (will start on VS Code folder open).")
|