dev-bubble 0.7.5__tar.gz → 0.7.8__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.5 → dev_bubble-0.7.8}/.claude/CLAUDE.md +13 -8
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/CHANGELOG.md +1 -1
- {dev_bubble-0.7.5/dev_bubble.egg-info → dev_bubble-0.7.8}/PKG-INFO +1 -1
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/__init__.py +1 -1
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/claude.py +98 -7
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/clean.py +3 -1
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/cli.py +37 -9
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/security_cmd.py +6 -4
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/settings.py +68 -20
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/config.py +2 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/container_helpers.py +8 -6
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/github_token.py +41 -23
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/builder.py +22 -14
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/base.sh +5 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/lean-toolchain.sh +1 -1
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/lean.sh +3 -1
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/python.sh +2 -2
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/tools/gh.sh +10 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/tools/vscode.sh +44 -10
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/runtime/base.py +22 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/runtime/incus.py +47 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/security.py +187 -34
- {dev_bubble-0.7.5 → dev_bubble-0.7.8/dev_bubble.egg-info}/PKG-INFO +1 -1
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_claude.py +188 -8
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_config.py +61 -2
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_github_token.py +11 -14
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_security.py +260 -151
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_tools.py +54 -6
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/.github/workflows/ci.yml +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/.github/workflows/publish.yml +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/.gitignore +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/LICENSE +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/README.md +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/SPEC.md +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/__main__.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/auth_proxy.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/automation.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/clone.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/cloud.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/cloud_types.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/__init__.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/cloud_cmd.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/completion.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/doctor.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/images.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/infrastructure.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/lifecycle.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/list_cmd.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/relay_cmd.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/remote_cmd.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/commands/status_cmd.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/data/skill.md +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/default_repos.json +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/finalization.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/git_store.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/graphql_validator.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/hooks/__init__.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/hooks/lean.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/hooks/python.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/image_management.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/__init__.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/cloud-init.sh +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/tools/claude.sh +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/tools/codex.sh +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/tools/elan.sh +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/tools/emacs.sh +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/tools/neovim.sh +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/images/scripts/tools/pins.json +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/lean.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/lifecycle.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/naming.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/native.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/network.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/notices.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/output.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/provisioning.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/relay.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/remote.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/repo_registry.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/runtime/__init__.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/runtime/colima.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/setup.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/skill.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/spinner.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/target.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/token_store.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/tools.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/tunnel.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/bubble/vscode.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/config/com.bubble.git-update.plist +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/config/com.bubble.image-refresh.plist +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/config/com.bubble.relay-daemon.plist +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/conftest.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/dev_bubble.egg-info/SOURCES.txt +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/dev_bubble.egg-info/dependency_links.txt +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/dev_bubble.egg-info/entry_points.txt +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/dev_bubble.egg-info/requires.txt +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/dev_bubble.egg-info/top_level.txt +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/pyproject.toml +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/setup.cfg +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/conftest.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_auth_proxy.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_branch_no_target.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_build_lock.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_claude_projects_symlink.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_cloud.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_completion.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_customize.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_editor.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_git_store.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_graphql_validator.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_hooks.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_integration.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_lifecycle.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_list_columns.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_list_remote.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_mounts.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_multi_target.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_naming.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_native.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_network.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_notices.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_relay.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_remote.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_repo_registry.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_skill.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_spinner.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_status.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_target.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_tunnel.py +0 -0
- {dev_bubble-0.7.5 → dev_bubble-0.7.8}/tests/test_vscode.py +0 -0
|
@@ -121,14 +121,19 @@ Bubbles can run on a remote machine instead of locally. The `--ssh HOST` flag (o
|
|
|
121
121
|
Users can place a `customize.sh` script at `~/.bubble/customize.sh` to run custom setup in all container images. The script runs as root as the final step when building any image (base, lean, lean-v4.X.Y). This lets users add tools, dotfiles, shell config, etc. without forking image scripts. The script's content hash is tracked in `~/.bubble/customize-hash`; on `bubble open`, if the hash differs from the stored value, a background rebuild of the base image is triggered (same pattern as VS Code commit hash drift). Code is in `builder.py` (`customize_hash()`, `_run_customize_script()`).
|
|
122
122
|
|
|
123
123
|
### GitHub Auth Proxy
|
|
124
|
-
The auth proxy (`auth_proxy.py`) provides repo-scoped GitHub authentication without injecting the host's token into containers. It's an HTTP reverse proxy that runs on the host
|
|
125
|
-
|
|
126
|
-
|
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
124
|
+
The auth proxy (`auth_proxy.py`) provides repo-scoped GitHub authentication without injecting the host's token into containers. It's an HTTP reverse proxy that runs on the host. The access level is controlled by the unified `github` security setting (`security.py`), which picks one level from a graduated escalation ladder:
|
|
125
|
+
|
|
126
|
+
| `github` level | Behavior |
|
|
127
|
+
|----------------|----------|
|
|
128
|
+
| `off` | no GitHub access at all |
|
|
129
|
+
| `basic` | git push/pull only (proxy rewrites, repo-scoped) |
|
|
130
|
+
| `rest` | + repo-scoped REST API |
|
|
131
|
+
| `allowlist-read-graphql` | + allowlisted GraphQL queries |
|
|
132
|
+
| `allowlist-write-graphql` | + allowlisted GraphQL mutations (default) |
|
|
133
|
+
| `write-graphql` | + arbitrary GraphQL, no allowlist filtering |
|
|
134
|
+
| `direct` | inject the raw token, no proxy |
|
|
135
|
+
|
|
136
|
+
`auto` defaults to `allowlist-write-graphql`. The old `github-auth`, `github-api`, and `github-token-inject` settings are deprecated but migrated automatically.
|
|
132
137
|
|
|
133
138
|
**Git flow:** Container git → `url.insteadOf` rewrites to `http://127.0.0.1:7654/git/...` → proxy validates `X-Bubble-Token` header → checks path matches allowed `owner/repo` → adds `Authorization: token <real-token>` → forwards to `https://github.com` → returns response.
|
|
134
139
|
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
- Templated Claude prompts for issues and PRs (#33)
|
|
219
219
|
- PR bubbles now auto-inject a Claude prompt that checks CI status and summarizes PR comments
|
|
220
220
|
- User-customizable prompt templates via `~/.bubble/templates/issue.txt` and `~/.bubble/templates/pr.txt`
|
|
221
|
-
- Issue template placeholders: `{owner}`, `{repo}`, `{issue_num}`, `{title}`, `{body}`, `{comments}`, `{comments_section}`, `{branch}`
|
|
221
|
+
- Issue template placeholders: `{owner}`, `{repo}`, `{issue_num}`, `{title}`, `{body}`, `{comments}`, `{comments_section}`, `{branch}`, `{instructions}`
|
|
222
222
|
- PR template placeholders: `{owner}`, `{repo}`, `{pr_num}`, `{title}`, `{body}`, `{branch}`
|
|
223
223
|
- Falls back to built-in defaults when no custom template exists
|
|
224
224
|
|
|
@@ -18,18 +18,56 @@ CLAUDE_TASK_COMMAND = (
|
|
|
18
18
|
|
|
19
19
|
TEMPLATES_DIR = DATA_DIR / "templates"
|
|
20
20
|
|
|
21
|
+
# Ordered autonomy levels. Each level implies all lower levels.
|
|
22
|
+
AUTONOMY_LEVELS = ("read", "plan", "implement", "pr", "merge")
|
|
23
|
+
|
|
24
|
+
# Valid values for the second_opinion setting.
|
|
25
|
+
SECOND_OPINION_VALUES = ("auto", "on", "off")
|
|
26
|
+
|
|
27
|
+
# Issue prompt instructions keyed by autonomy level.
|
|
28
|
+
_ISSUE_INSTRUCTIONS = {
|
|
29
|
+
"read": (
|
|
30
|
+
"Please read and understand the issue. "
|
|
31
|
+
"Summarize the problem and any relevant context, but take no further action."
|
|
32
|
+
),
|
|
33
|
+
"plan": (
|
|
34
|
+
"Please read and understand the issue, then propose a plan to fix it. "
|
|
35
|
+
"Describe what files need to change and how, but do not implement anything yet."
|
|
36
|
+
),
|
|
37
|
+
"implement": (
|
|
38
|
+
"Please claim this issue (assign it to yourself if possible), "
|
|
39
|
+
"then implement a fix or feature as described. "
|
|
40
|
+
"Work on a branch named `{branch}`. Do not commit or open a PR."
|
|
41
|
+
),
|
|
42
|
+
"pr": (
|
|
43
|
+
"Please claim this issue (assign it to yourself if possible), "
|
|
44
|
+
"then implement a fix or feature as described. "
|
|
45
|
+
"Work on a branch named `{branch}`, and open a PR when done."
|
|
46
|
+
),
|
|
47
|
+
"merge": (
|
|
48
|
+
"Please claim this issue (assign it to yourself if possible), "
|
|
49
|
+
"then implement a fix or feature as described. "
|
|
50
|
+
"Work on a branch named `{branch}`, open a PR, "
|
|
51
|
+
"rebase onto the default branch, watch CI, and merge when it passes."
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_SECOND_OPINION_SUFFIX = (
|
|
56
|
+
"\n\nBefore proceeding, get a second opinion from another AI "
|
|
57
|
+
"(e.g. Codex) to review your approach."
|
|
58
|
+
)
|
|
59
|
+
|
|
21
60
|
# Default issue prompt template. Placeholders:
|
|
22
61
|
# {owner}, {repo}, {issue_num}, {title}, {body}, {comments},
|
|
23
|
-
# {comments_section} (pre-formatted, empty when no comments), {branch}
|
|
62
|
+
# {comments_section} (pre-formatted, empty when no comments), {branch},
|
|
63
|
+
# {instructions}
|
|
24
64
|
_DEFAULT_ISSUE_TEMPLATE = (
|
|
25
65
|
'Please read and understand GitHub issue #{issue_num}: "{title}".\n'
|
|
26
66
|
"\n"
|
|
27
67
|
"Issue description:\n"
|
|
28
68
|
"{body}\n"
|
|
29
69
|
"{comments_section}"
|
|
30
|
-
"\
|
|
31
|
-
"then implement a fix or feature as described. "
|
|
32
|
-
"Work on a branch named `{branch}`, and open a PR when done."
|
|
70
|
+
"\n{instructions}"
|
|
33
71
|
)
|
|
34
72
|
|
|
35
73
|
# Default PR prompt template. Placeholders:
|
|
@@ -105,12 +143,44 @@ def _fetch_github_item(owner: str, repo: str, endpoint: str, jq: str) -> str | N
|
|
|
105
143
|
return None
|
|
106
144
|
|
|
107
145
|
|
|
108
|
-
def
|
|
146
|
+
def _resolve_second_opinion(second_opinion: str, config: dict | None = None) -> bool:
|
|
147
|
+
"""Resolve the second_opinion setting to a boolean.
|
|
148
|
+
|
|
149
|
+
'on' → True, 'off' → False, 'auto' → True if codex will be
|
|
150
|
+
available in the container (checked via tool resolution, not host PATH).
|
|
151
|
+
"""
|
|
152
|
+
if second_opinion == "on":
|
|
153
|
+
return True
|
|
154
|
+
if second_opinion == "off":
|
|
155
|
+
return False
|
|
156
|
+
# auto: check if codex is resolved as an enabled tool
|
|
157
|
+
if config is not None:
|
|
158
|
+
from .tools import resolve_tools
|
|
159
|
+
|
|
160
|
+
return "codex" in resolve_tools(config)
|
|
161
|
+
# Fallback when no config available: check host PATH
|
|
162
|
+
import shutil
|
|
163
|
+
|
|
164
|
+
return shutil.which("codex") is not None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def generate_issue_prompt(
|
|
168
|
+
owner: str,
|
|
169
|
+
repo: str,
|
|
170
|
+
issue_num: str,
|
|
171
|
+
branch: str,
|
|
172
|
+
autonomy: str = "plan",
|
|
173
|
+
second_opinion: str = "auto",
|
|
174
|
+
config: dict | None = None,
|
|
175
|
+
) -> str | None:
|
|
109
176
|
"""Fetch GitHub issue details and generate a Claude prompt.
|
|
110
177
|
|
|
111
178
|
Returns the prompt string, or None if the issue can't be fetched.
|
|
112
179
|
Uses a custom template from ~/.bubble/templates/issue.txt if present,
|
|
113
180
|
otherwise falls back to the built-in default.
|
|
181
|
+
|
|
182
|
+
The autonomy level controls what action instructions are included:
|
|
183
|
+
read, plan, implement, pr, merge.
|
|
114
184
|
"""
|
|
115
185
|
raw = _fetch_github_item(owner, repo, f"issues/{issue_num}", ".title,.body")
|
|
116
186
|
if raw is None:
|
|
@@ -129,8 +199,21 @@ def generate_issue_prompt(owner: str, repo: str, issue_num: str, branch: str) ->
|
|
|
129
199
|
if comments_text:
|
|
130
200
|
comments_section = f"\nComments:\n{comments_text}\n"
|
|
131
201
|
|
|
132
|
-
|
|
133
|
-
|
|
202
|
+
# Build instructions from autonomy level
|
|
203
|
+
if autonomy not in AUTONOMY_LEVELS:
|
|
204
|
+
autonomy = "plan"
|
|
205
|
+
instructions = _ISSUE_INSTRUCTIONS[autonomy]
|
|
206
|
+
# The instructions may contain {branch} placeholder
|
|
207
|
+
instructions = instructions.format(branch=branch)
|
|
208
|
+
|
|
209
|
+
# Append second opinion request if enabled
|
|
210
|
+
if _resolve_second_opinion(second_opinion, config=config):
|
|
211
|
+
instructions += _SECOND_OPINION_SUFFIX
|
|
212
|
+
|
|
213
|
+
custom_template = _load_template("issue")
|
|
214
|
+
template = custom_template or _DEFAULT_ISSUE_TEMPLATE
|
|
215
|
+
|
|
216
|
+
prompt = _render_template(
|
|
134
217
|
template,
|
|
135
218
|
owner=owner,
|
|
136
219
|
repo=repo,
|
|
@@ -140,8 +223,16 @@ def generate_issue_prompt(owner: str, repo: str, issue_num: str, branch: str) ->
|
|
|
140
223
|
comments=comments_text,
|
|
141
224
|
comments_section=comments_section,
|
|
142
225
|
branch=branch,
|
|
226
|
+
instructions=instructions,
|
|
143
227
|
)
|
|
144
228
|
|
|
229
|
+
# If a custom template didn't use {instructions}, append them so
|
|
230
|
+
# autonomy/second-opinion settings are never silently lost.
|
|
231
|
+
if custom_template and "{instructions}" not in custom_template:
|
|
232
|
+
prompt = prompt.rstrip() + "\n\n" + instructions
|
|
233
|
+
|
|
234
|
+
return prompt
|
|
235
|
+
|
|
145
236
|
|
|
146
237
|
def generate_pr_prompt(owner: str, repo: str, pr_num: str, branch: str) -> str | None:
|
|
147
238
|
"""Fetch GitHub PR details and generate a Claude prompt.
|
|
@@ -70,7 +70,9 @@ REASONS=""
|
|
|
70
70
|
EXPECTED=$(echo {q_repo} | tr '[:upper:]' '[:lower:]')
|
|
71
71
|
|
|
72
72
|
# Check 1: no unexpected non-hidden items in home
|
|
73
|
-
|
|
73
|
+
# Filter out known infrastructure files (build.log from background builds,
|
|
74
|
+
# bin/ from tool installation)
|
|
75
|
+
ITEMS=$(ls /home/user/ 2>/dev/null | grep -v -x -e 'build.log' -e 'bin' || true)
|
|
74
76
|
if [ -n "$EXPECTED" ]; then
|
|
75
77
|
if [ "$(echo "$ITEMS" | tr '[:upper:]' '[:lower:]')" != "$EXPECTED" ]; then
|
|
76
78
|
CLEAN=false
|
|
@@ -51,6 +51,7 @@ from .native import open_native
|
|
|
51
51
|
from .provisioning import mount_overlaps, provision_container
|
|
52
52
|
from .repo_registry import RepoRegistry
|
|
53
53
|
from .security import (
|
|
54
|
+
get_github_level,
|
|
54
55
|
is_enabled,
|
|
55
56
|
is_locked_off,
|
|
56
57
|
print_warnings,
|
|
@@ -264,7 +265,20 @@ def _resolve_claude_prompt_locally(target: str, new_branch: str | None = None) -
|
|
|
264
265
|
from .output import detail
|
|
265
266
|
|
|
266
267
|
detail(f"Fetching issue #{t.ref} for Claude prompt...")
|
|
267
|
-
|
|
268
|
+
_cfg = load_config()
|
|
269
|
+
_claude_cfg = _cfg.get("claude", {})
|
|
270
|
+
prompt = (
|
|
271
|
+
generate_issue_prompt(
|
|
272
|
+
t.owner,
|
|
273
|
+
t.repo,
|
|
274
|
+
t.ref,
|
|
275
|
+
branch,
|
|
276
|
+
autonomy=_claude_cfg.get("autonomy", "plan"),
|
|
277
|
+
second_opinion=_claude_cfg.get("second_opinion", "auto"),
|
|
278
|
+
config=_cfg,
|
|
279
|
+
)
|
|
280
|
+
or ""
|
|
281
|
+
)
|
|
268
282
|
elif t.kind == "pr":
|
|
269
283
|
from .claude import generate_pr_prompt
|
|
270
284
|
|
|
@@ -329,8 +343,9 @@ def _open_remote(
|
|
|
329
343
|
# Inject local SSH keys into the container so the chained ProxyCommand works
|
|
330
344
|
inject_local_ssh_keys(remote_host, name)
|
|
331
345
|
|
|
332
|
-
# Set up GitHub auth
|
|
333
|
-
|
|
346
|
+
# Set up GitHub auth based on the unified github security level
|
|
347
|
+
gh_level = get_github_level(config)
|
|
348
|
+
if gh_level == "direct":
|
|
334
349
|
from .github_token import setup_gh_token
|
|
335
350
|
|
|
336
351
|
setup_gh_token(
|
|
@@ -339,7 +354,7 @@ def _open_remote(
|
|
|
339
354
|
remote_host=remote_host,
|
|
340
355
|
token_inject=True,
|
|
341
356
|
)
|
|
342
|
-
elif
|
|
357
|
+
elif gh_level != "off":
|
|
343
358
|
from .github_token import setup_gh_token
|
|
344
359
|
from .tools import resolve_tools
|
|
345
360
|
|
|
@@ -993,9 +1008,10 @@ def _open_single(
|
|
|
993
1008
|
)
|
|
994
1009
|
|
|
995
1010
|
# Set up GitHub auth BEFORE clone — network allowlisting strips
|
|
996
|
-
# github.com from allowed domains when using the auth proxy
|
|
997
|
-
#
|
|
998
|
-
|
|
1011
|
+
# github.com from allowed domains when using the auth proxy, so
|
|
1012
|
+
# git must be configured to route through the proxy first.
|
|
1013
|
+
gh_level = get_github_level(config)
|
|
1014
|
+
if gh_level == "direct":
|
|
999
1015
|
from .github_token import setup_gh_token
|
|
1000
1016
|
|
|
1001
1017
|
setup_gh_token(
|
|
@@ -1004,7 +1020,7 @@ def _open_single(
|
|
|
1004
1020
|
machine_readable=machine_readable,
|
|
1005
1021
|
token_inject=True,
|
|
1006
1022
|
)
|
|
1007
|
-
elif
|
|
1023
|
+
elif gh_level != "off":
|
|
1008
1024
|
from .github_token import setup_gh_token
|
|
1009
1025
|
from .tools import resolve_tools
|
|
1010
1026
|
|
|
@@ -1033,7 +1049,19 @@ def _open_single(
|
|
|
1033
1049
|
from .output import detail
|
|
1034
1050
|
|
|
1035
1051
|
detail(f"Fetching issue #{t.ref} for Claude prompt...")
|
|
1036
|
-
|
|
1052
|
+
claude_cfg = config.get("claude", {})
|
|
1053
|
+
claude_prompt = (
|
|
1054
|
+
generate_issue_prompt(
|
|
1055
|
+
t.owner,
|
|
1056
|
+
t.repo,
|
|
1057
|
+
t.ref,
|
|
1058
|
+
checkout_branch,
|
|
1059
|
+
autonomy=claude_cfg.get("autonomy", "plan"),
|
|
1060
|
+
second_opinion=claude_cfg.get("second_opinion", "auto"),
|
|
1061
|
+
config=config,
|
|
1062
|
+
)
|
|
1063
|
+
or ""
|
|
1064
|
+
)
|
|
1037
1065
|
elif not claude_prompt and t.kind == "pr" and not machine_readable:
|
|
1038
1066
|
from .claude import generate_pr_prompt
|
|
1039
1067
|
from .output import detail
|
|
@@ -38,8 +38,9 @@ def register_security_commands(main):
|
|
|
38
38
|
if changed:
|
|
39
39
|
save_config(config)
|
|
40
40
|
for name in changed:
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
val = config["security"][name]
|
|
42
|
+
click.echo(f" security.{display_setting_name(name)} = {val}")
|
|
43
|
+
click.echo(f"Set {len(changed)} setting(s) to most permissive.")
|
|
43
44
|
else:
|
|
44
45
|
click.echo("All settings are already on.")
|
|
45
46
|
|
|
@@ -75,11 +76,12 @@ def register_security_commands(main):
|
|
|
75
76
|
def security_set(key, value):
|
|
76
77
|
"""Set a security setting: bubble security set <name> <value>.
|
|
77
78
|
|
|
78
|
-
Setting names use hyphens (e.g. github
|
|
79
|
+
Setting names use hyphens (e.g. github, claude-credentials).
|
|
79
80
|
Underscores are also accepted as permanent aliases.
|
|
80
81
|
|
|
81
82
|
Most settings accept: auto, on, off.
|
|
82
|
-
github
|
|
83
|
+
github accepts: off, basic, rest, allowlist-read-graphql,
|
|
84
|
+
allowlist-write-graphql, write-graphql, direct.
|
|
83
85
|
"""
|
|
84
86
|
# Accept both "security.X" and bare "X", normalize hyphens to underscores
|
|
85
87
|
name = normalize_setting_name(key.removeprefix("security."))
|
|
@@ -157,12 +157,64 @@ def register_settings_commands(main):
|
|
|
157
157
|
else:
|
|
158
158
|
click.echo("Claude credentials disabled.")
|
|
159
159
|
|
|
160
|
+
@claude_group.command("set")
|
|
161
|
+
@click.argument("key", type=click.Choice(["autonomy", "second-opinion"]))
|
|
162
|
+
@click.argument("value")
|
|
163
|
+
def claude_set_cmd(key, value):
|
|
164
|
+
"""Set a Claude Code setting.
|
|
165
|
+
|
|
166
|
+
\b
|
|
167
|
+
Settings:
|
|
168
|
+
autonomy read, plan, implement, pr, merge (default: plan)
|
|
169
|
+
second-opinion auto, on, off (default: auto)
|
|
170
|
+
|
|
171
|
+
\b
|
|
172
|
+
Examples:
|
|
173
|
+
bubble claude set autonomy pr
|
|
174
|
+
bubble claude set second-opinion on
|
|
175
|
+
"""
|
|
176
|
+
from ..claude import AUTONOMY_LEVELS, SECOND_OPINION_VALUES
|
|
177
|
+
|
|
178
|
+
config_key = key.replace("-", "_")
|
|
179
|
+
|
|
180
|
+
if config_key == "autonomy":
|
|
181
|
+
if value not in AUTONOMY_LEVELS:
|
|
182
|
+
click.echo(
|
|
183
|
+
f"Invalid autonomy level: {value}. Choose from: {', '.join(AUTONOMY_LEVELS)}",
|
|
184
|
+
err=True,
|
|
185
|
+
)
|
|
186
|
+
sys.exit(1)
|
|
187
|
+
elif config_key == "second_opinion":
|
|
188
|
+
if value not in SECOND_OPINION_VALUES:
|
|
189
|
+
click.echo(
|
|
190
|
+
f"Invalid second-opinion value: {value}. "
|
|
191
|
+
f"Choose from: {', '.join(SECOND_OPINION_VALUES)}",
|
|
192
|
+
err=True,
|
|
193
|
+
)
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
config = load_config()
|
|
197
|
+
config.setdefault("claude", {})[config_key] = value
|
|
198
|
+
save_config(config)
|
|
199
|
+
click.echo(f"Set claude.{key} = {value}")
|
|
200
|
+
|
|
160
201
|
@claude_group.command("status")
|
|
161
202
|
def claude_status_cmd():
|
|
162
203
|
"""Show current Claude Code settings."""
|
|
204
|
+
from ..claude import _resolve_second_opinion
|
|
205
|
+
|
|
163
206
|
config = load_config()
|
|
164
|
-
|
|
165
|
-
|
|
207
|
+
claude_cfg = config.get("claude", {})
|
|
208
|
+
creds = claude_cfg.get("credentials", True)
|
|
209
|
+
autonomy = claude_cfg.get("autonomy", "plan")
|
|
210
|
+
second_opinion = claude_cfg.get("second_opinion", "auto")
|
|
211
|
+
resolved_so = _resolve_second_opinion(second_opinion, config=config)
|
|
212
|
+
|
|
213
|
+
click.echo(f" credentials: {'on' if creds else 'off'}")
|
|
214
|
+
click.echo(f" autonomy: {autonomy}")
|
|
215
|
+
so_resolved = "on" if resolved_so else "off"
|
|
216
|
+
so_extra = f" (resolved: {so_resolved})" if second_opinion == "auto" else ""
|
|
217
|
+
click.echo(f" second-opinion: {second_opinion}{so_extra}")
|
|
166
218
|
|
|
167
219
|
# --- codex ---
|
|
168
220
|
|
|
@@ -321,34 +373,30 @@ def register_settings_commands(main):
|
|
|
321
373
|
"""Show GitHub integration status."""
|
|
322
374
|
from ..automation import is_auth_proxy_installed
|
|
323
375
|
from ..github_token import has_gh_auth
|
|
324
|
-
from ..security import
|
|
376
|
+
from ..security import get_github_level
|
|
325
377
|
|
|
326
378
|
config = load_config()
|
|
327
|
-
|
|
328
|
-
|
|
379
|
+
gh_level = get_github_level(config)
|
|
380
|
+
raw_value = get_setting(config, "github")
|
|
329
381
|
host_auth = has_gh_auth()
|
|
330
382
|
proxy_installed = is_auth_proxy_installed()
|
|
331
383
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
click.echo(
|
|
337
|
-
f"Token injection: {token_inject} (effectively {'on' if inject_enabled else 'off'})"
|
|
338
|
-
)
|
|
384
|
+
if raw_value == "auto":
|
|
385
|
+
click.echo(f"GitHub level: auto (effectively {gh_level})")
|
|
386
|
+
else:
|
|
387
|
+
click.echo(f"GitHub level: {gh_level}")
|
|
339
388
|
click.echo(f"Host gh auth: {'authenticated' if host_auth else 'not authenticated'}")
|
|
340
389
|
click.echo(f"Auth proxy: {'installed' if proxy_installed else 'not installed'}")
|
|
341
|
-
if
|
|
390
|
+
if gh_level == "direct":
|
|
342
391
|
click.echo(
|
|
343
|
-
"\nWarning: token injection is enabled.
|
|
344
|
-
"
|
|
392
|
+
"\nWarning: direct token injection is enabled."
|
|
393
|
+
" Containers get your full GitHub token."
|
|
394
|
+
"\nChange: bubble security set github allowlist-write-graphql"
|
|
345
395
|
)
|
|
396
|
+
elif gh_level == "off":
|
|
397
|
+
click.echo("\nGitHub access is disabled. Enable: bubble security set github auto")
|
|
346
398
|
elif not host_auth:
|
|
347
399
|
click.echo("\nRun 'gh auth login' to authenticate on the host first.")
|
|
348
|
-
elif not enabled:
|
|
349
|
-
click.echo(
|
|
350
|
-
"\nRun 'bubble security set github-auth on' to enable GitHub auth in bubbles."
|
|
351
|
-
)
|
|
352
400
|
|
|
353
401
|
@gh_group.group("proxy")
|
|
354
402
|
def gh_proxy_group():
|
|
@@ -479,7 +527,7 @@ def register_settings_commands(main):
|
|
|
479
527
|
"""Set a security setting: bubble config set security.<name> <value>.
|
|
480
528
|
|
|
481
529
|
Alias for `bubble security set <name> <value>`.
|
|
482
|
-
Setting names use hyphens (e.g. github
|
|
530
|
+
Setting names use hyphens (e.g. github, claude-credentials).
|
|
483
531
|
Underscores are also accepted as permanent aliases.
|
|
484
532
|
"""
|
|
485
533
|
# Accept both "security.X" and bare "X", normalize hyphens to underscores
|
|
@@ -11,7 +11,7 @@ import click
|
|
|
11
11
|
from .lifecycle import load_registry
|
|
12
12
|
from .output import detail, step
|
|
13
13
|
from .runtime.base import ContainerRuntime
|
|
14
|
-
from .security import filter_github_domains
|
|
14
|
+
from .security import filter_github_domains
|
|
15
15
|
from .vscode import add_ssh_config
|
|
16
16
|
|
|
17
17
|
|
|
@@ -123,11 +123,13 @@ def apply_network(
|
|
|
123
123
|
for d in tool_runtime_domains(resolve_tools(config)):
|
|
124
124
|
if d not in domains:
|
|
125
125
|
domains.append(d)
|
|
126
|
-
# Direct GitHub network access is only allowed when
|
|
127
|
-
# is
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
|
|
126
|
+
# Direct GitHub network access is only allowed when the github level
|
|
127
|
+
# is "direct" (raw token injection). For proxy-mediated access or no
|
|
128
|
+
# auth, iptables blocks direct GitHub traffic — all GitHub communication
|
|
129
|
+
# is forced through the auth proxy on loopback.
|
|
130
|
+
from .security import get_github_level
|
|
131
|
+
|
|
132
|
+
if get_github_level(config) != "direct":
|
|
131
133
|
domains = filter_github_domains(domains)
|
|
132
134
|
if domains:
|
|
133
135
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""GitHub authentication for containers via auth proxy or direct injection.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Most github levels use an HTTP reverse proxy on the host:
|
|
4
4
|
1. Receives plain HTTP git requests from the container
|
|
5
5
|
2. Validates the request targets only the allowed repository
|
|
6
6
|
3. Adds the real Authorization header
|
|
@@ -9,8 +9,8 @@ Levels 1-4 use an HTTP reverse proxy on the host:
|
|
|
9
9
|
The host GitHub token never enters the container. Each container
|
|
10
10
|
gets a per-container bearer token scoped to one repository.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
The "direct" level bypasses the proxy entirely: the host's actual
|
|
13
|
+
GitHub token is injected into the container as GH_TOKEN and
|
|
14
14
|
GITHUB_TOKEN environment variables, giving unrestricted access.
|
|
15
15
|
|
|
16
16
|
For local containers, the proxy is exposed via Incus proxy devices.
|
|
@@ -18,15 +18,18 @@ For remote/cloud containers, an SSH reverse tunnel forwards the
|
|
|
18
18
|
local proxy port to the remote host, then an Incus proxy device
|
|
19
19
|
on the remote exposes it into the container.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
GitHub levels (each a strict superset of the one above):
|
|
22
|
+
off: no GitHub access
|
|
23
|
+
basic: git push/pull only
|
|
24
|
+
rest: + repo-scoped REST API
|
|
25
|
+
allowlist-read-graphql: + allowlisted GraphQL queries
|
|
26
|
+
allowlist-write-graphql: + allowlisted GraphQL mutations (default)
|
|
27
|
+
write-graphql: + arbitrary GraphQL
|
|
28
|
+
direct: raw token injection, no proxy
|
|
29
|
+
|
|
30
|
+
When gh is installed and the github level includes REST or higher,
|
|
31
|
+
the proxy is also exposed as a Unix socket at /bubble/gh-proxy.sock
|
|
32
|
+
and gh is configured to route through it via http_unix_socket.
|
|
30
33
|
"""
|
|
31
34
|
|
|
32
35
|
import platform
|
|
@@ -109,16 +112,23 @@ def _ensure_auth_proxy_running() -> int | None:
|
|
|
109
112
|
def _resolve_access_level(config: dict, gh_enabled: bool) -> int:
|
|
110
113
|
"""Determine the auth proxy REST access level for a container.
|
|
111
114
|
|
|
112
|
-
Returns the REST access level (1 or 4) based on
|
|
113
|
-
availability. GraphQL policies are resolved
|
|
114
|
-
_resolve_graphql_config().
|
|
115
|
+
Returns the REST access level (1 or 4) based on the unified github
|
|
116
|
+
security level and tool availability. GraphQL policies are resolved
|
|
117
|
+
separately by _resolve_graphql_config().
|
|
115
118
|
"""
|
|
116
119
|
from .auth_proxy import LEVEL_GH_READWRITE, LEVEL_GIT_ONLY
|
|
117
|
-
from .security import
|
|
120
|
+
from .security import get_github_level
|
|
121
|
+
|
|
122
|
+
level = get_github_level(config)
|
|
123
|
+
|
|
124
|
+
# basic = git only; off/direct shouldn't reach here but return git-only
|
|
125
|
+
if level in ("off", "basic", "direct"):
|
|
126
|
+
return LEVEL_GIT_ONLY
|
|
118
127
|
|
|
119
|
-
if not gh_enabled
|
|
128
|
+
if not gh_enabled:
|
|
120
129
|
return LEVEL_GIT_ONLY
|
|
121
130
|
|
|
131
|
+
# rest, allowlist-read-graphql, allowlist-write-graphql, write-graphql
|
|
122
132
|
# REST is already repo-scoped by path validation, so read-write is
|
|
123
133
|
# safe by default. This enables REST POST operations like
|
|
124
134
|
# gh run rerun (/repos/{owner}/{repo}/actions/runs/{id}/rerun).
|
|
@@ -128,18 +138,26 @@ def _resolve_access_level(config: dict, gh_enabled: bool) -> int:
|
|
|
128
138
|
def _resolve_graphql_config(config: dict, gh_enabled: bool) -> tuple[str, str]:
|
|
129
139
|
"""Determine GraphQL policies for a container.
|
|
130
140
|
|
|
131
|
-
Returns (graphql_read, graphql_write)
|
|
141
|
+
Returns (graphql_read, graphql_write) based on the unified github
|
|
142
|
+
security level.
|
|
132
143
|
"""
|
|
133
|
-
from .security import
|
|
144
|
+
from .security import get_github_level
|
|
134
145
|
|
|
135
|
-
|
|
146
|
+
level = get_github_level(config)
|
|
147
|
+
|
|
148
|
+
if not gh_enabled or level in ("off", "basic", "rest", "direct"):
|
|
136
149
|
return "none", "none"
|
|
137
150
|
|
|
138
|
-
if
|
|
151
|
+
if level == "allowlist-read-graphql":
|
|
152
|
+
return "whitelisted", "none"
|
|
153
|
+
|
|
154
|
+
if level == "allowlist-write-graphql":
|
|
155
|
+
return "whitelisted", "whitelisted"
|
|
156
|
+
|
|
157
|
+
if level == "write-graphql":
|
|
139
158
|
return "unrestricted", "unrestricted"
|
|
140
159
|
|
|
141
|
-
|
|
142
|
-
return "whitelisted", "whitelisted"
|
|
160
|
+
return "none", "none"
|
|
143
161
|
|
|
144
162
|
|
|
145
163
|
def _describe_graphql_mode(graphql_read: str, graphql_write: str) -> str:
|