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();