dev-bubble 0.7.9__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.9 → dev_bubble-0.7.10}/.claude/CLAUDE.md +3 -3
- {dev_bubble-0.7.9/dev_bubble.egg-info → dev_bubble-0.7.10}/PKG-INFO +1 -1
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/SPEC.md +98 -45
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/__init__.py +1 -1
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/auth_proxy.py +27 -71
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/github_token.py +41 -24
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/tools/gh.sh +1 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/tools/vscode.sh +15 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/security.py +1 -93
- {dev_bubble-0.7.9 → dev_bubble-0.7.10/dev_bubble.egg-info}/PKG-INFO +1 -1
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_auth_proxy.py +63 -86
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_github_token.py +5 -16
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_graphql_validator.py +0 -38
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_security.py +22 -112
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/.github/workflows/ci.yml +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/.github/workflows/publish.yml +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/.gitignore +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/CHANGELOG.md +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/LICENSE +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/README.md +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/__main__.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/ai.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/automation.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/clean.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/cli.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/clone.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/cloud.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/cloud_types.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/__init__.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/cloud_cmd.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/completion.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/doctor.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/images.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/infrastructure.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/lifecycle.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/list_cmd.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/relay_cmd.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/remote_cmd.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/security_cmd.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/settings.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/commands/status_cmd.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/config.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/container_helpers.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/data/skill.md +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/default_repos.json +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/finalization.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/git_store.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/graphql_validator.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/hooks/__init__.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/hooks/lean.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/hooks/python.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/image_management.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/__init__.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/builder.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/base.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/cloud-init.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/lean-toolchain.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/lean.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/python.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/tools/claude.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/tools/codex.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/tools/elan.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/tools/emacs.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/tools/neovim.sh +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/images/scripts/tools/pins.json +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/lean.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/lifecycle.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/naming.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/native.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/network.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/notices.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/output.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/provisioning.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/relay.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/remote.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/repo_registry.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/runtime/__init__.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/runtime/base.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/runtime/colima.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/runtime/incus.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/setup.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/skill.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/spinner.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/target.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/token_store.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/tools.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/tunnel.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/bubble/vscode.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/config/com.bubble.git-update.plist +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/config/com.bubble.image-refresh.plist +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/config/com.bubble.relay-daemon.plist +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/conftest.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/dev_bubble.egg-info/SOURCES.txt +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/dev_bubble.egg-info/dependency_links.txt +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/dev_bubble.egg-info/entry_points.txt +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/dev_bubble.egg-info/requires.txt +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/dev_bubble.egg-info/top_level.txt +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/pyproject.toml +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/setup.cfg +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/conftest.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_ai.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_branch_no_target.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_build_lock.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_claude_projects_symlink.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_cloud.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_completion.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_config.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_customize.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_editor.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_git_store.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_hooks.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_integration.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_lifecycle.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_list_columns.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_list_remote.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_mounts.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_multi_target.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_naming.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_native.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_network.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_notices.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_relay.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_remote.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_repo_registry.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_skill.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_spinner.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_status.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_target.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_tools.py +0 -0
- {dev_bubble-0.7.9 → dev_bubble-0.7.10}/tests/test_tunnel.py +0 -0
- {dev_bubble-0.7.9 → 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.
|
|
@@ -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 +
|
|
@@ -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
|
|
|
@@ -1098,19 +1148,18 @@ get/set interface:
|
|
|
1098
1148
|
|
|
1099
1149
|
### Security settings
|
|
1100
1150
|
|
|
1101
|
-
|
|
1102
|
-
|
|
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.
|
|
1103
1154
|
|
|
1104
1155
|
| Setting | Auto default | Description |
|
|
1105
1156
|
|---------|-------------|-------------|
|
|
1106
|
-
| `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 |
|
|
1107
1158
|
| `user-mounts` | on | `--mount` flag support |
|
|
1108
1159
|
| `git-manifest-trust` | on | Auto-clone Lake manifest dependencies |
|
|
1109
1160
|
| `claude-credentials` | on | Mount Claude credentials into containers |
|
|
1110
1161
|
| `codex-credentials` | on | Mount Codex credentials into containers |
|
|
1111
|
-
| `github
|
|
1112
|
-
| `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 |
|
|
1113
|
-
| `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)) |
|
|
1114
1163
|
| `relay` | on | Bubble-in-bubble relay |
|
|
1115
1164
|
| `host-key-trust` | on | Disable SSH StrictHostKeyChecking |
|
|
1116
1165
|
|
|
@@ -1119,15 +1168,15 @@ user to `bubble security`. Suppressed by `BUBBLE_QUIET_SECURITY=1`.
|
|
|
1119
1168
|
|
|
1120
1169
|
**Commands:**
|
|
1121
1170
|
- `bubble security` — show full security posture
|
|
1122
|
-
- `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`)
|
|
1123
1172
|
- `bubble security permissive` — set all to `on`
|
|
1124
1173
|
- `bubble security lockdown` — set all to `off`
|
|
1125
1174
|
- `bubble security default` — reset all to `auto`
|
|
1126
1175
|
|
|
1127
1176
|
**GitHub network access:** Direct GitHub network access (via iptables) is only
|
|
1128
|
-
allowed when `github
|
|
1129
|
-
|
|
1130
|
-
|
|
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.
|
|
1131
1180
|
|
|
1132
1181
|
---
|
|
1133
1182
|
|
|
@@ -1159,6 +1208,10 @@ container lifecycle, but a complete implementation should include them:
|
|
|
1159
1208
|
- `bubble remote clear-default` — clear default remote host
|
|
1160
1209
|
- `bubble remote status` — show remote configuration and list remote bubbles
|
|
1161
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
|
|
1162
1215
|
|
|
1163
1216
|
---
|
|
1164
1217
|
|
|
@@ -1189,7 +1242,7 @@ container lifecycle, but a complete implementation should include them:
|
|
|
1189
1242
|
| `~/.bubble/cloud_key` | SSH private key for cloud (ed25519, mode 0600) |
|
|
1190
1243
|
| `~/.bubble/cloud_key.pub` | SSH public key for cloud |
|
|
1191
1244
|
| `~/.bubble/known_hosts` | SSH known_hosts for cloud |
|
|
1192
|
-
| `~/.bubble/
|
|
1245
|
+
| `~/.bubble/ai-projects/` | AI provider session state for containers |
|
|
1193
1246
|
| `~/.ssh/config.d/bubble` | Auto-managed SSH config entries |
|
|
1194
1247
|
|
|
1195
1248
|
---
|
|
@@ -9,17 +9,15 @@ The host GitHub token never enters the container. Each container
|
|
|
9
9
|
gets a per-container bearer token that only works against this
|
|
10
10
|
proxy and is scoped to a single repository.
|
|
11
11
|
|
|
12
|
-
Access
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
4 = git + gh read-write (REST read-write + GraphQL queries + mutations)
|
|
12
|
+
Access policies (per-container):
|
|
13
|
+
rest_api: whether repo-scoped REST API access is allowed
|
|
14
|
+
graphql_read: "whitelisted", "unrestricted", or "none"
|
|
15
|
+
graphql_write: "whitelisted", "unrestricted", or "none"
|
|
17
16
|
|
|
18
17
|
Security model:
|
|
19
18
|
- Git: strict 4-pattern allowlist (git smart HTTP protocol only)
|
|
20
19
|
- REST API: path-validated against /repos/{owner}/{repo}/...
|
|
21
|
-
- GraphQL:
|
|
22
|
-
(NOT repo-scoped — queries can access any data the host token can read)
|
|
20
|
+
- GraphQL: controlled by graphql_read/graphql_write policies
|
|
23
21
|
- Path canonicalization rejects encoded separators, dot-segments,
|
|
24
22
|
duplicate slashes
|
|
25
23
|
- Redirect following for API responses (CI logs) with hardened rules:
|
|
@@ -76,18 +74,6 @@ RATE_LIMIT_PER_HOUR = 600
|
|
|
76
74
|
# Maximum tracked containers
|
|
77
75
|
MAX_TRACKED_CONTAINERS = 100
|
|
78
76
|
|
|
79
|
-
# ---------------------------------------------------------------------------
|
|
80
|
-
# Access levels
|
|
81
|
-
# ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
LEVEL_GIT_ONLY = 1
|
|
84
|
-
LEVEL_REST_READ = 2
|
|
85
|
-
LEVEL_GH_READ = 3
|
|
86
|
-
LEVEL_GH_READWRITE = 4
|
|
87
|
-
LEVEL_TOKEN_INJECT = 5
|
|
88
|
-
|
|
89
|
-
DEFAULT_LEVEL = LEVEL_GH_READ
|
|
90
|
-
|
|
91
77
|
# ---------------------------------------------------------------------------
|
|
92
78
|
# Path patterns
|
|
93
79
|
# ---------------------------------------------------------------------------
|
|
@@ -144,13 +130,13 @@ def generate_auth_token(
|
|
|
144
130
|
container_name: str,
|
|
145
131
|
owner: str,
|
|
146
132
|
repo: str,
|
|
147
|
-
|
|
133
|
+
rest_api: bool = True,
|
|
148
134
|
graphql_read: str = "whitelisted",
|
|
149
135
|
graphql_write: str = "whitelisted",
|
|
150
136
|
) -> str:
|
|
151
137
|
"""Generate an auth proxy token for a container.
|
|
152
138
|
|
|
153
|
-
The token maps to (container_name, owner, repo,
|
|
139
|
+
The token maps to (container_name, owner, repo, rest_api, graphql_read,
|
|
154
140
|
graphql_write) — the proxy uses this to validate requests and enforce
|
|
155
141
|
the access policy.
|
|
156
142
|
Uses file locking to prevent read-modify-write races.
|
|
@@ -160,7 +146,7 @@ def generate_auth_token(
|
|
|
160
146
|
"container": container_name,
|
|
161
147
|
"owner": owner,
|
|
162
148
|
"repo": repo,
|
|
163
|
-
"
|
|
149
|
+
"rest_api": rest_api,
|
|
164
150
|
"graphql_read": graphql_read,
|
|
165
151
|
"graphql_write": graphql_write,
|
|
166
152
|
}
|
|
@@ -278,14 +264,15 @@ def _build_api_url(path: str, query: str) -> str:
|
|
|
278
264
|
|
|
279
265
|
|
|
280
266
|
# ---------------------------------------------------------------------------
|
|
281
|
-
# API path validation
|
|
267
|
+
# API path validation
|
|
282
268
|
# ---------------------------------------------------------------------------
|
|
283
269
|
|
|
284
270
|
|
|
285
|
-
def validate_api_path(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
271
|
+
def validate_api_path(path: str, query: str, method: str, owner: str, repo: str) -> str | None:
|
|
272
|
+
"""Validate a REST API request path.
|
|
273
|
+
|
|
274
|
+
REST is always repo-scoped by path validation, so all HTTP methods
|
|
275
|
+
are allowed when REST access is enabled.
|
|
289
276
|
|
|
290
277
|
Returns an error message string, or None if the path is valid.
|
|
291
278
|
"""
|
|
@@ -313,11 +300,6 @@ def validate_api_path(
|
|
|
313
300
|
if path_owner.lower() != owner.lower() or path_repo.lower() != repo.lower():
|
|
314
301
|
return f"Repository mismatch: {path_owner}/{path_repo} != {owner}/{repo}"
|
|
315
302
|
|
|
316
|
-
# Method checks: levels 1-3 are read-only REST, level 4 allows writes
|
|
317
|
-
if level < LEVEL_GH_READWRITE:
|
|
318
|
-
if method not in ("GET", "HEAD"):
|
|
319
|
-
return f"Method {method} not allowed at access level {level}"
|
|
320
|
-
|
|
321
303
|
return None
|
|
322
304
|
|
|
323
305
|
|
|
@@ -433,31 +415,6 @@ def _parse_graphql_op_type(query: str) -> str | None:
|
|
|
433
415
|
return "query"
|
|
434
416
|
|
|
435
417
|
|
|
436
|
-
def _resolve_graphql_policies(info: dict) -> tuple[str, str]:
|
|
437
|
-
"""Extract GraphQL policies from token info, with backward compat.
|
|
438
|
-
|
|
439
|
-
Returns (graphql_read, graphql_write).
|
|
440
|
-
For tokens created before the graphql_read/graphql_write fields
|
|
441
|
-
existed, derives policies from the legacy level field.
|
|
442
|
-
"""
|
|
443
|
-
from .graphql_validator import VALID_GRAPHQL_POLICIES
|
|
444
|
-
|
|
445
|
-
graphql_read = info.get("graphql_read")
|
|
446
|
-
graphql_write = info.get("graphql_write")
|
|
447
|
-
|
|
448
|
-
if graphql_read in VALID_GRAPHQL_POLICIES and graphql_write in VALID_GRAPHQL_POLICIES:
|
|
449
|
-
return graphql_read, graphql_write
|
|
450
|
-
|
|
451
|
-
# Backward compat: derive from level
|
|
452
|
-
level = info.get("level", LEVEL_GIT_ONLY)
|
|
453
|
-
if level < LEVEL_GH_READ:
|
|
454
|
-
return "none", "none"
|
|
455
|
-
elif level < LEVEL_GH_READWRITE:
|
|
456
|
-
return "unrestricted", "none"
|
|
457
|
-
else:
|
|
458
|
-
return "unrestricted", "unrestricted"
|
|
459
|
-
|
|
460
|
-
|
|
461
418
|
def classify_graphql(body: bytes) -> tuple[str | None, str | None]:
|
|
462
419
|
"""Classify a GraphQL request body.
|
|
463
420
|
|
|
@@ -704,8 +661,9 @@ class AuthProxyHandler(BaseHTTPRequestHandler):
|
|
|
704
661
|
container = info["container"]
|
|
705
662
|
owner = info["owner"]
|
|
706
663
|
repo = info["repo"]
|
|
707
|
-
|
|
708
|
-
graphql_read
|
|
664
|
+
rest_api = info.get("rest_api", False)
|
|
665
|
+
graphql_read = info.get("graphql_read", "none")
|
|
666
|
+
graphql_write = info.get("graphql_write", "none")
|
|
709
667
|
|
|
710
668
|
# Rate limit
|
|
711
669
|
if not self.rate_limiter.check(container):
|
|
@@ -727,7 +685,7 @@ class AuthProxyHandler(BaseHTTPRequestHandler):
|
|
|
727
685
|
|
|
728
686
|
# Route: git smart HTTP (/git/...)
|
|
729
687
|
if path.startswith("/git/"):
|
|
730
|
-
self._handle_git_request(method, path, query, body, container, owner, repo
|
|
688
|
+
self._handle_git_request(method, path, query, body, container, owner, repo)
|
|
731
689
|
return
|
|
732
690
|
|
|
733
691
|
# Route: GraphQL (/graphql)
|
|
@@ -737,7 +695,7 @@ class AuthProxyHandler(BaseHTTPRequestHandler):
|
|
|
737
695
|
|
|
738
696
|
# Route: REST API (/repos/{owner}/{repo}/...)
|
|
739
697
|
if path.startswith("/repos/"):
|
|
740
|
-
self._handle_api_request(method, path, query, body, container, owner, repo,
|
|
698
|
+
self._handle_api_request(method, path, query, body, container, owner, repo, rest_api)
|
|
741
699
|
return
|
|
742
700
|
|
|
743
701
|
self._send_error(403, "Path not recognized")
|
|
@@ -759,8 +717,8 @@ class AuthProxyHandler(BaseHTTPRequestHandler):
|
|
|
759
717
|
return self._read_chunked()
|
|
760
718
|
return b""
|
|
761
719
|
|
|
762
|
-
def _handle_git_request(self, method, path, query, body, container, owner, repo
|
|
763
|
-
"""Handle git smart HTTP requests (
|
|
720
|
+
def _handle_git_request(self, method, path, query, body, container, owner, repo):
|
|
721
|
+
"""Handle git smart HTTP requests (always allowed for valid tokens)."""
|
|
764
722
|
error = validate_path(path, query, owner, repo)
|
|
765
723
|
if error:
|
|
766
724
|
self._send_error(403, error)
|
|
@@ -772,16 +730,14 @@ class AuthProxyHandler(BaseHTTPRequestHandler):
|
|
|
772
730
|
method, upstream_url, body, container, path, host=GITHUB_HOST, follow_redirects=False
|
|
773
731
|
)
|
|
774
732
|
|
|
775
|
-
def _handle_api_request(self, method, path, query, body, container, owner, repo,
|
|
776
|
-
"""Handle REST API requests (
|
|
777
|
-
if
|
|
778
|
-
self._send_error(403, "REST API access not enabled
|
|
779
|
-
logger.info(
|
|
780
|
-
"BLOCKED %s %s container=%s reason=level_%d", method, path, container, level
|
|
781
|
-
)
|
|
733
|
+
def _handle_api_request(self, method, path, query, body, container, owner, repo, rest_api):
|
|
734
|
+
"""Handle REST API requests (requires rest_api=True)."""
|
|
735
|
+
if not rest_api:
|
|
736
|
+
self._send_error(403, "REST API access not enabled")
|
|
737
|
+
logger.info("BLOCKED %s %s container=%s reason=rest_disabled", method, path, container)
|
|
782
738
|
return
|
|
783
739
|
|
|
784
|
-
error = validate_api_path(path, query, method, owner, repo
|
|
740
|
+
error = validate_api_path(path, query, method, owner, repo)
|
|
785
741
|
if error:
|
|
786
742
|
self._send_error(403, error)
|
|
787
743
|
logger.info("BLOCKED %s %s container=%s reason=%s", method, path, container, error)
|