Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ inputs:
description: "API key for your review LLM (e.g. an OpenAI key for Codex). Pass a secret."
required: true
llm-api-key-env:
description: "Env var name your agent CLI reads the key from."
description: "DEPRECATED — no longer needed; the agent authenticates via its own login. Honored when set."
required: false
default: "OPENAI_API_KEY"
default: ""
llm-model:
description: "Model label for cost/telemetry attribution (the real model is set by your agent profile)."
description: "Model for the review. bubo init templates it into the agent profile and labels cost metrics."
required: false
default: "gpt-5.5"
llm-model-effort:
description: "Reasoning effort: low | medium | high."
required: false
default: "medium"
llm-base-url:
description: "Custom OpenAI-compatible endpoint. When set, the key is passed to the agent's environment."
required: false
default: ""
github-token:
description: "Token used to read the PR diff and post review comments. Needs pull-requests: write."
required: false
Expand Down Expand Up @@ -101,7 +109,10 @@ runs:
env:
BUBO_ROOT: ${{ runner.temp }}/bubo
run: |
bubo init --root "${BUBO_ROOT}"
# First pass: create the workspace, seed env.toml, init the DB. The
# agent profile is templated by the SECOND init below, after the real
# config (model / effort / base_url) is written.
bubo init --root "${BUBO_ROOT}" --no-agent-config

- name: Write config from inputs
shell: bash
Expand All @@ -113,6 +124,8 @@ runs:
LLM_API_KEY: ${{ inputs.llm-api-key }}
LLM_API_KEY_ENV: ${{ inputs.llm-api-key-env }}
LLM_MODEL: ${{ inputs.llm-model }}
LLM_MODEL_EFFORT: ${{ inputs.llm-model-effort }}
LLM_BASE_URL: ${{ inputs.llm-base-url }}
REVIEWER_COMMAND: ${{ inputs.reviewer-command }}
DRY_RUN: ${{ inputs.dry-run }}
TONE: ${{ inputs.tone }}
Expand All @@ -132,8 +145,10 @@ runs:
echo
echo '[agents]'
printf 'llm_model = "%s"\n' "${LLM_MODEL}"
printf 'llm_model_effort = "%s"\n' "${LLM_MODEL_EFFORT}"
printf 'llm_api_key = "%s"\n' "${LLM_API_KEY}"
printf 'llm_api_key_env = "%s"\n' "${LLM_API_KEY_ENV}"
[ -n "${LLM_BASE_URL}" ] && printf 'llm_base_url = "%s"\n' "${LLM_BASE_URL}"
[ -n "${LLM_API_KEY_ENV}" ] && printf 'llm_api_key_env = "%s"\n' "${LLM_API_KEY_ENV}"
if [ -n "${REVIEWER_COMMAND}" ]; then
printf 'reviewer_command = ['
first=1
Expand All @@ -160,6 +175,17 @@ runs:
} > "${cfg}"
echo "Wrote ${cfg} (provider=github, PR #${PR_NUMBER}, dry_run=${DRY_RUN})"

- name: Template the agent profile from the written config
shell: bash
env:
BUBO_ROOT: ${{ runner.temp }}/bubo
run: |
# Second pass: now that env.toml carries the real model / effort /
# base_url, init templates them into the agent profile. No --force: the
# agent config doesn't exist yet (first pass was --no-agent-config), and
# we must NOT re-seed (clobber) the env.toml just written above.
bubo init --root "${BUBO_ROOT}"

- name: Authenticate the default Codex agent (best-effort)
if: ${{ inputs.reviewer-command == '' }}
shell: bash
Expand Down
57 changes: 7 additions & 50 deletions bin/bubo
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
#!/bin/sh
# Single repo-root / deploy-bundle dispatcher for bubo's shell launchers.
# Replaces the former per-command bin/ scripts (bubo-poller, bubo-mcp,
# mcp-upstream-gitlab, mcp-upstream-github, bubo-env). The environment
# loading that bin/bubo-env did is now the internal `load_env` function every
# subcommand calls.
# bubo-env). The environment loading that bin/bubo-env did is now the internal
# `load_env` function every subcommand calls.
#
# Usage:
# bin/bubo poll [args...] # one poll cycle (bubo-poller)
# bin/bubo mcp [args...] # bubo's own MCP server (bubo-mcp)
# bin/bubo mcp-upstream <github|gitlab> [args] # run the third-party MCP server
# bin/bubo poll [args...] # one poll cycle (bubo-poller)
# bin/bubo mcp [args...] # bubo's own read-only MCP server (bubo-mcp)
set -eu

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ROOT="${BUBO_ROOT:-$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)}"

# Put common tool dirs on PATH, export BUBO_ROOT, load config/env.toml into the
# environment (via `python -m bubo.env_config`), and mirror GITLAB_TOKEN into
# the GITLAB_PERSONAL_ACCESS_TOKEN the upstream MCP server expects. (Formerly
# bin/bubo-env.)
# Put common tool dirs on PATH, export BUBO_ROOT, and load config/env.toml into
# the environment (via `python -m bubo.env_config`). (Formerly bin/bubo-env.)
load_env() {
PATH="/usr/local/bin:/opt/homebrew/bin:$HOME/.local/bin:$PATH"
export PATH BUBO_ROOT="$ROOT"
if [ -r "$ROOT/config/env.toml" ]; then
eval "$(PYTHONPATH="$ROOT/src" python3 -m bubo.env_config "$ROOT")"
fi
if [ -n "${GITLAB_TOKEN:-}" ] && [ -z "${GITLAB_PERSONAL_ACCESS_TOKEN:-}" ]; then
export GITLAB_PERSONAL_ACCESS_TOKEN="$GITLAB_TOKEN"
fi
}

cmd="${1:-}"
Expand All @@ -41,44 +34,8 @@ case "$cmd" in
load_env
exec uv run --project "$ROOT" bubo-mcp "$@"
;;
mcp-upstream)
provider="${1:-}"
[ "$#" -gt 0 ] && shift
load_env
# The candidate names are upstream's (different third-party projects),
# not ours — do not rename them.
case "$provider" in
gitlab)
if command -v mcp-gitlab >/dev/null 2>&1; then
MCP_BIN="$(command -v mcp-gitlab)"
elif command -v gitlab-mcp >/dev/null 2>&1; then
MCP_BIN="$(command -v gitlab-mcp)"
else
echo "mcp-gitlab or gitlab-mcp is required" >&2
exit 127
fi
;;
github)
if command -v github-mcp-server >/dev/null 2>&1; then
MCP_BIN="$(command -v github-mcp-server)"
elif command -v mcp-github >/dev/null 2>&1; then
MCP_BIN="$(command -v mcp-github)"
elif command -v gh-mcp-server >/dev/null 2>&1; then
MCP_BIN="$(command -v gh-mcp-server)"
else
echo "github-mcp-server, mcp-github, or gh-mcp-server is required" >&2
exit 127
fi
;;
*)
echo "usage: bubo mcp-upstream <github|gitlab>" >&2
exit 2
;;
esac
exec "$MCP_BIN" "$@"
;;
*)
echo "usage: bubo <poll|mcp|mcp-upstream <github|gitlab>> [args...]" >&2
echo "usage: bubo <poll|mcp> [args...]" >&2
exit 2
;;
esac
129 changes: 76 additions & 53 deletions config/env.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,35 +59,25 @@ provider = "gitlab"
# ----------------------------------------------------------------------------
[gitlab]

# GitLab web URL the poller reads MRs from.
# GitLab web URL the poller reads MRs from and clones over HTTPS.
#
# Change only for self-hosted GitLab installs (for example,
# "https://gitlab.mycorp.example"). For self-hosted hosts, keep `api_url`
# pointed at the same host or the in-agent MCP tools will read MRs from a
# different server than the poller posts to.
# "https://gitlab.mycorp.example"). Keep `api_url` pointed at the same host.
url = "https://gitlab.com"

# GitLab REST API endpoint used by the GitLab MCP server inside the review
# agent. Usually `<url>/api/v4`. Override only when the API lives on a
# different hostname or path (rare in self-hosted setups).
# GitLab REST API endpoint the poller reads MRs, diffs, and outcomes from.
# Usually `<url>/api/v4`. Override only when the API lives on a different
# hostname or path (rare in self-hosted setups).
api_url = "https://gitlab.com/api/v4"

# GitLab username of the bot account whose name appears on posted review
# threads. Outcome sync uses this to tell bot comments apart from developer
# replies — set it to whatever account you created the token under.
bot_username = "bubo"

# Safety fence applied INSIDE the review agent: GitLab MCP tools whose names
# match this regex are blocked even if the prompt or MCP server exposes
# them. The default blocks anything that could delete or merge an MR or push
# files. Tighten if you trust the agent less; loosen at your own risk.
denied_tools_regex = "^(delete_.*|merge_merge_request|push_files)$"

# GitLab personal-access token, `api` scope required.
#
# The same value is exported to the runtime environment under three names so
# different tools can pick it up: `GITLAB_TOKEN`, `GITLAB_PERSONAL_ACCESS_TOKEN`
# (used by GitLab MCP), and `GLAB_TOKEN` (used by the `glab` CLI).
# GitLab personal-access token, `api` scope required. Used for the REST API and
# as the credential for the HTTPS `git clone` (sent per-call as an auth header,
# never written to the checkout's `.git/config`).
#
# Leave blank for the example template. **Do not commit real values.**
token = ""
Expand All @@ -97,23 +87,23 @@ token = ""
# [github] — GitHub connection (used when provider = "github").
# ----------------------------------------------------------------------------
#
# Required prerequisites for the GitHub provider: the `gh` CLI (checkout) and
# a GitHub MCP server on PATH as `github-mcp-server` / `mcp-github` /
# `gh-mcp-server` (inline review-comment posting; falls back to REST if the
# MCP tool name does not match).
# The GitHub provider needs only `git` on PATH; checkout, diffs, posting, and
# outcome sync all go through the REST API (no extra CLIs).
[github]

# REST API base. Use https://api.github.com for github.com, or
# https://<host>/api/v3 for GitHub Enterprise Server.
# https://<host>/api/v3 for GitHub Enterprise Server. The web host bubo clones
# from is derived from this (api.github.com → github.com; GHES → its own host).
api_url = "https://api.github.com"

# GitHub username of the bot account whose name appears on review comments.
# Outcome sync uses this to separate bot comments from developer replies.
bot_username = "bubo"

# GitHub token (fine-grained or classic) with pull-request read + write
# scope. Exported as GITHUB_TOKEN, GITHUB_PERSONAL_ACCESS_TOKEN (GitHub MCP),
# and GH_TOKEN (the `gh` CLI). Leave blank in the template.
# GitHub token (fine-grained or classic) with pull-request read + write scope.
# Used for the REST API and as the credential for the HTTPS `git clone` (sent
# per-call as an auth header, never written to `.git/config`). Leave blank in
# the template.
token = ""


Expand Down Expand Up @@ -470,40 +460,42 @@ interval_seconds = 900
# only when you have a fork of the prompt and want the agent to use it.
prompt_file = "prompts/00-meta.md"

# Model identifier passed to the review CLI wrapper.
#
# Two things care about this value: (1) the CLI itself (Codex / Claude),
# which must be configured to actually invoke this model, and (2) the
# `model` attribute on OpenTelemetry metrics. If you change the model,
# update the matching pricing rows under `[telemetry]` or the cost-estimate
# metric will be wrong.
# Model identifier for the review. `bubo init` templates this into the agent
# profile (Codex `~/.codex/config.toml`; Claude `~/.claude/settings.json`), so
# it actually drives the model — re-run `bubo init` after changing it. The
# `model` attribute on OpenTelemetry metrics reads this too; keep the
# `[telemetry]` pricing rows aligned or the cost metric will be wrong.
llm_model = "gpt-5.5"

# API key for whatever LLM you run the review with (OpenAI, Anthropic,
# Gemini, a local model, …). Exported as the generic `LLM_API_KEY` plus
# the operator-named variable in `llm_api_key_env` below.
# Reasoning effort for the review ("low" / "medium" / "high"). Higher is more
# thorough but slower and costlier. `bubo init` templates this into the agent
# profile alongside `llm_model`. Start at medium.
llm_model_effort = "medium"

# API key for whatever LLM you review with (OpenAI, Anthropic, Gemini, a local
# OpenAI-compatible endpoint, …). Exported as the generic `LLM_API_KEY`.
#
# How it reaches the agent: by default the review agent authenticates with its
# OWN login (e.g. `codex login --with-api-key`), so the key is NOT injected into
# the agent's environment — the primary anti-exfiltration defense. The one
# exception is a custom `llm_base_url` below (an OpenAI-compatible endpoint reads
# the key from the environment at request time), in which case `LLM_API_KEY` is
# passed through to the agent.
#
# Leave blank in the example. **Do not commit real values.**
llm_api_key = ""

# The environment-variable NAME your LLM CLI / SDK reads the key from.
# Bubo is model-agnostic — it does not guess this from the model name —
# so set it to match the LLM you chose:
#
# OpenAI / Codex -> "OPENAI_API_KEY"
# Anthropic / Claude -> "ANTHROPIC_API_KEY"
# Google Gemini -> "GEMINI_API_KEY"
# …whatever your CLI expects.
#
# The key in `llm_api_key` above is exported under this name (and under
# the generic `LLM_API_KEY`). Leave blank if your CLI reads `LLM_API_KEY`
# directly. Default matches the `gpt-5.5` model + Codex above.
llm_api_key_env = "OPENAI_API_KEY"
# Optional: a custom OpenAI-compatible endpoint (in-house gateway, proxy, local
# server). When set, `bubo init` points the Codex `bubo` profile at it via a
# `[model_providers]` block and the agent reads `LLM_API_KEY` from the
# environment at request time. SECURITY: this is the one mode that re-exposes
# the key to the agent's environment — leave empty unless you need it.
# llm_base_url = "https://llm.internal.example/v1"

# Reasoning effort passed to the agent CLI ("low" / "medium" / "high"). Higher
# values produce more thorough reviews but raise latency and cost. Start at
# medium; only raise once you have outcome data justifying it.
reasoning_effort = "medium"
# DEPRECATED — no longer needed. It named an extra env var to expose the key
# under; the agent now authenticates via its own login. Still honored when set
# so existing configs keep working, but prefer removing it.
# llm_api_key_env = "OPENAI_API_KEY"

# Dry-run hint exported to the review agent as REVIEW_DRY_RUN. Independent
# from `[review].dry_run`, which controls the poller's own posting behavior.
Expand Down Expand Up @@ -605,6 +597,37 @@ output_per_1m = 30.0
cached_input_per_1m = 0.5


# ----------------------------------------------------------------------------
# [analytics] — Help improve Bubo (anonymous usage analytics).
# ----------------------------------------------------------------------------
#
# Bubo is a free, open-source project. The only way we learn what to improve
# is anonymous usage signal from real installs — so this is ON by default.
#
# What we send: NUMBERS ONLY. Counts of reviews and findings, durations,
# lines-of-code reviewed, token totals, your SCM type (gitlab/github), and
# the model name. That's it.
#
# What we NEVER send: your code, file paths, repository or project names,
# review comments, commit SHAs, tokens/credentials, or anything that could
# identify you or your repos. An anonymous random install id lets us count
# distinct installs without knowing who you are. (The allowlist that enforces
# this lives in src/bubo/analytics.py.)
#
# To opt out, do ANY of:
# * uncomment `enabled = false` below, or
# * set the env var BUBO_ANALYTICS=0, or
# * set the standard DO_NOT_TRACK=1.
#
# Thank you for helping make Bubo — and the open-source AI-reviewer
# ecosystem — better. There is genuinely no other way for us to know.
# ----------------------------------------------------------------------------
[analytics]

# Anonymous usage analytics are ON by default. Uncomment to opt out:
# enabled = false


# ----------------------------------------------------------------------------
# [mcp_server] — How `bubo-mcp` exposes itself.
# ----------------------------------------------------------------------------
Expand Down
Loading
Loading