From fd5cde27194035318684d1139959e13998fcfca2 Mon Sep 17 00:00:00 2001 From: "brynary-fabro[bot]" <265161896+brynary-fabro[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:39:39 -0400 Subject: [PATCH] Fix: Add OAuth callback URLs to CLI-generated GitHub App manifest (#105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes GitHub issue #97 where `fabro install` creates a GitHub App that passes `fabro doctor` but fails during OAuth login in `fabro-web` because the CLI-generated manifest is missing `callback_urls` and `setup_url` fields. Without these fields, GitHub rejects the OAuth flow with a "must be configured with a callback URL" error, even though the web setup flow (`setup.tsx`) already includes them correctly. The fix extracts manifest construction into a standalone `build_github_app_manifest` helper and adds the missing `callback_urls` and `setup_url` fields, mirroring what the web setup flow already provides. A new `--web-url` flag (defaulting to `http://localhost:5173`) is added to the `Install` command so users can specify their web UI base URL, which is then threaded through `run_install` and `setup_github_app` to construct the correct OAuth callback endpoints. A unit test is included to verify that the generated manifest contains the expected `callback_urls` and `setup_url` values for a given `web_url`. ### Fabro Details
Ran 9 stages in 17m 47s for $1.56 | Stage | Duration | Cost | Retries | |---|---|---|---| | start | 0s | – | 0 | | toolchain | 0s | – | 0 | | preflight_compile | 1m 11s | – | 0 | | preflight_lint | 14s | – | 0 | | implement | 6m 32s | $0.64 | 1 | | simplify_opus | 0s | – | 3 | | simplify_gpt | 5m 42s | $0.92 | 0 | | verify | 1m 21s | – | 0 | | fmt | 1s | – | 0 | | **Total** | **17m 47s** | **$1.56** | **4** |
Ran ImplementAndSimplify.fabro (12 nodes and 15 edges) ```dot digraph ImplementAndSimplify { graph [ goal="Implement and simplify", model_stylesheet=" * { backend: api; model: claude-opus-4-6;} " ] rankdir=LR start [shape=Mdiamond, label="Start"] exit [shape=Msquare, label="Exit"] toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0] preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0] preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0] fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3] implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."] simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"] simplify_gpt [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"] verify [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"] fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3] fmt [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", max_retries=0] start -> toolchain toolchain -> preflight_compile [condition="outcome=success"] toolchain -> exit preflight_compile -> preflight_lint [condition="outcome=success"] preflight_compile -> exit preflight_lint -> implement [condition="outcome=success"] preflight_lint -> fix_lints fix_lints -> preflight_lint implement -> simplify_opus -> simplify_gpt -> verify verify -> fmt [condition="outcome=success"] verify -> fixup fixup -> verify fmt -> exit } ```
⚒️ Generated with [Fabro](https://fabro.sh) --------- Co-authored-by: Fabro Co-authored-by: Bryan Helmkamp Co-authored-by: Claude Opus 4.6 (1M context) --- lib/crates/fabro-cli/src/install.rs | 63 ++++++++++++++++++++--------- lib/crates/fabro-cli/src/main.rs | 14 ++++--- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/lib/crates/fabro-cli/src/install.rs b/lib/crates/fabro-cli/src/install.rs index fcf51a0c2..a401a0aa3 100644 --- a/lib/crates/fabro-cli/src/install.rs +++ b/lib/crates/fabro-cli/src/install.rs @@ -258,9 +258,33 @@ struct CallbackParams { code: String, } +fn build_github_app_manifest(app_name: &str, port: u16, web_url: &str) -> serde_json::Value { + serde_json::json!({ + "name": app_name, + "url": "https://github.com/apps/arc", + "redirect_url": format!("http://127.0.0.1:{port}/callback"), + "callback_urls": [format!("{web_url}/auth/callback")], + "setup_url": format!("{web_url}/setup/callback"), + "public": false, + "default_permissions": { + "contents": "write", + "metadata": "read", + "pull_requests": "write", + "checks": "write", + "issues": "write", + "emails": "read" + }, + "default_events": [] + }) +} + /// Run the GitHub App manifest registration flow via a temporary local server. /// Returns env var pairs (key, value) for secrets to merge into `.env`. -async fn setup_github_app(arc_dir: &Path, s: &Styles) -> Result> { +async fn setup_github_app( + arc_dir: &Path, + s: &Styles, + web_url: &str, +) -> Result> { // Random suffix so app names don't collide let mut rng = rand::thread_rng(); let suffix: String = (0..6) @@ -275,21 +299,7 @@ async fn setup_github_app(arc_dir: &Path, s: &Styles) -> Result Result Result<()> { +pub async fn run_install(web_url: &str) -> Result<()> { let s = Styles::detect_stderr(); let emoji = console::Emoji("⚒️ ", ""); @@ -640,7 +650,7 @@ pub async fn run_install() -> Result<()> { .await??; if setup_github { - let github_env_pairs = setup_github_app(&arc_dir, &s).await?; + let github_env_pairs = setup_github_app(&arc_dir, &s, web_url).await?; let slug = { let cli_toml_path = arc_dir.join("cli.toml"); let toml_content = std::fs::read_to_string(&cli_toml_path).unwrap_or_default(); @@ -951,4 +961,21 @@ mod tests { assert_eq!(tls.key, PathBuf::from("~/.fabro/certs/server.key")); assert_eq!(tls.ca, PathBuf::from("~/.fabro/certs/ca.crt")); } + + // -- GitHub App manifest -- + + #[test] + fn manifest_includes_callback_urls_and_setup_url() { + let web_url = "https://app.example.com"; + let manifest = build_github_app_manifest("Arc-test", 12345, web_url); + + assert_eq!( + manifest["callback_urls"], + serde_json::json!(["https://app.example.com/auth/callback"]), + ); + assert_eq!( + manifest["setup_url"], + serde_json::json!("https://app.example.com/setup/callback"), + ); + } } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 543ffe2b9..8eef1ab42 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -119,7 +119,11 @@ enum Command { #[command(hide = true)] Init, /// Set up the Fabro environment (LLMs, certs, GitHub) - Install, + Install { + /// Base URL for the web UI (used for OAuth callback URLs) + #[arg(long, default_value = "http://localhost:5173")] + web_url: String, + }, /// List workflow runs #[command(hide = true)] Ps(commands::runs::RunsListArgs), @@ -490,7 +494,7 @@ async fn main_inner() -> (String, Result<()>) { RepoCommand::Deinit => "repo deinit", }, Command::Init => "init", - Command::Install => "install", + Command::Install { .. } => "install", Command::Ps(_) => "ps", Command::Rm(_) => "rm", Command::Pr { command } => match command { @@ -572,7 +576,7 @@ async fn main_inner() -> (String, Result<()>) { | Command::Exec(_) | Command::Repo { .. } | Command::Init - | Command::Install + | Command::Install { .. } ) { upgrade::spawn_upgrade_check(cli.no_upgrade_check, upgrade_check_enabled) } else { @@ -845,8 +849,8 @@ async fn main_inner() -> (String, Result<()>) { ); init::run_init().await?; } - Command::Install => { - install::run_install().await?; + Command::Install { web_url } => { + install::run_install(&web_url).await?; } Command::Ps(args) => { let styles = fabro_util::terminal::Styles::detect_stdout();