mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-10 18:22:20 +02:00
* fix(mcp): stop assuming http://localhost:7000 for the OAuth callback The MCP OAuth callback origin is wrong on any install not reached at http://localhost:7000, and on Docker it cannot be corrected at all. Three sites, one assumption: - The redirect base fell back to a fixed port 7000. The app binds APP_PORT natively (app.py, launcher.py) and the macOS launcher defaults to 7860, where 7000 is AirPlay Receiver, so the callback lands on another service entirely. The fallback now follows APP_PORT. The hostname stays localhost rather than internal_api_base()'s 127.0.0.1: this URI is registered with the authorization server, so changing the host would invalidate the registrations that already exist. - The paste-back form hardcoded an http:// action. Serving the page over HTTPS, Chrome raises its insecure-form interstitial, and overriding that posts plain HTTP at a TLS port, which fails too. Either way the authorization code never reaches Odysseus. The action now carries the scheme the request arrived on. - OAUTH_REDIRECT_BASE_URL is the only fix available to a Docker install, because the container listens on 7000 and cannot see the host port map, but compose never forwarded it and nothing documented it. Both fixed. * fix(mcp): make the paste-back form action relative and export APP_PORT Answers the review on #6032. Three of the fixes did not survive contact with the deployments they targeted. - The form action derived its scheme from request.url.scheme. uvicorn only honours X-Forwarded-Proto from a peer inside --forwarded-allow-ips, which defaults to 127.0.0.1; the Dockerfile CMD sets no override, so a proxy arriving over the Docker bridge is untrusted and the scheme stays http. That is mixed content on exactly the HTTPS installs paste-back exists for. A relative action is resolved by the browser against the origin the page came from, which is right under every proxy setup, and it drops the Host header from the page entirely. - The APP_PORT fallback never fired for the shipped launchers. start-macos.sh, the generated .app launcher and launch-windows.ps1 all pass --port to uvicorn without putting the value in the environment, so the motivating case, macOS on 7860, still registered localhost:7000. Each now exports it. internal_api_base() and companion pairing read APP_PORT too and were wrong in the same way. - .env.example pointed Google MCP servers at OAUTH_REDIRECT_BASE_URL. add_server writes Desktop App credentials, and Google only accepts loopback redirects for that client type, so a public origin comes back as redirect_uri_mismatch. The variable is for the DCR flow; Google stays on the loopback default and finishes remotely through paste-back. The Host header is no longer reflected into the page, so the escaping regression test asserts its absence instead of its escaping.
178 lines
6.2 KiB
Bash
Executable File
178 lines
6.2 KiB
Bash
Executable File
#!/bin/bash
|
|
# Build a downloadable macOS launcher app + .dmg for Odysseus.
|
|
#
|
|
# ./build-macos-app.sh
|
|
#
|
|
# Produces:
|
|
# dist/Odysseus.app — double-click: starts the local server (using this
|
|
# repo's venv) and opens the UI in an app-style window.
|
|
# dist/Odysseus.dmg — drag-to-Applications disk image (the downloadable).
|
|
#
|
|
# This is a *launcher* wrapper: it drives the venv we set up in this repo, it
|
|
# does not bundle Python. The install path is baked into the app at build time,
|
|
# so rebuild if you move the repo. Override the port with ODYSSEUS_PORT.
|
|
set -e
|
|
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
APP_NAME="Odysseus"
|
|
INSTALL_DIR="$REPO_DIR"
|
|
PORT="${ODYSSEUS_PORT:-7860}"
|
|
DIST="$REPO_DIR/dist"
|
|
APP="$DIST/$APP_NAME.app"
|
|
|
|
echo "Building $APP_NAME.app"
|
|
echo " install dir: $INSTALL_DIR"
|
|
echo " port: $PORT"
|
|
|
|
rm -rf "$APP"
|
|
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
|
|
|
# ── Icon (best effort) — center-crop docs/odysseus.jpg to a square .icns ──
|
|
if [ -f "$REPO_DIR/docs/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then
|
|
TMPIMG="$(mktemp -d)"
|
|
# Center-crop to a square, scale to 512 (sips' icns encoder caps at 512), and
|
|
# let sips emit the .icns directly — more robust across macOS versions than
|
|
# building an .iconset by hand.
|
|
sips -c 720 720 "$REPO_DIR/docs/odysseus.jpg" --out "$TMPIMG/sq.png" >/dev/null 2>&1 || cp "$REPO_DIR/docs/odysseus.jpg" "$TMPIMG/sq.png"
|
|
sips -z 512 512 "$TMPIMG/sq.png" --out "$TMPIMG/icon.png" >/dev/null 2>&1
|
|
if sips -s format icns "$TMPIMG/icon.png" --out "$APP/Contents/Resources/odysseus.icns" >/dev/null 2>&1; then
|
|
echo " icon: odysseus.icns"
|
|
else
|
|
echo " icon: (skipped — conversion failed)"
|
|
fi
|
|
rm -rf "$TMPIMG"
|
|
else
|
|
echo " icon: (skipped — no docs/odysseus.jpg)"
|
|
fi
|
|
|
|
# ── Info.plist ──
|
|
cat > "$APP/Contents/Info.plist" <<PLIST
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>CFBundleName</key> <string>$APP_NAME</string>
|
|
<key>CFBundleDisplayName</key> <string>$APP_NAME</string>
|
|
<key>CFBundleIdentifier</key> <string>com.odysseus.launcher</string>
|
|
<key>CFBundleVersion</key> <string>1.0</string>
|
|
<key>CFBundleShortVersionString</key><string>1.0</string>
|
|
<key>CFBundlePackageType</key> <string>APPL</string>
|
|
<key>CFBundleExecutable</key> <string>$APP_NAME</string>
|
|
<key>CFBundleIconFile</key> <string>odysseus</string>
|
|
<key>LSMinimumSystemVersion</key> <string>11.0</string>
|
|
<key>NSHighResolutionCapable</key> <true/>
|
|
<key>LSUIElement</key> <false/>
|
|
</dict>
|
|
</plist>
|
|
PLIST
|
|
|
|
# ── Launcher executable (placeholders filled below) ──
|
|
cat > "$APP/Contents/MacOS/$APP_NAME.tmpl" <<'LAUNCHER'
|
|
#!/bin/bash
|
|
# Odysseus.app — start the local server and open the UI in an app window.
|
|
INSTALL_DIR="__INSTALL_DIR__"
|
|
PORT="__PORT__"
|
|
URL="http://127.0.0.1:${PORT}"
|
|
# uvicorn is started with --port below, but APP_PORT is what the app itself
|
|
# reads when it needs to build a URL for this instance (internal_api_base(),
|
|
# companion pairing, the MCP OAuth callback), so export it as well.
|
|
export APP_PORT="$PORT"
|
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
|
|
|
|
UVICORN="$INSTALL_DIR/venv/bin/uvicorn"
|
|
LOG="$INSTALL_DIR/logs/odysseus-app.log"
|
|
|
|
notify() { /usr/bin/osascript -e "display notification \"$1\" with title \"Odysseus\"" >/dev/null 2>&1; }
|
|
die_gui() {
|
|
/usr/bin/osascript -e "display dialog \"$1\" with title \"Odysseus\" buttons {\"OK\"} default button 1 with icon stop" >/dev/null 2>&1
|
|
exit 1
|
|
}
|
|
|
|
[ -x "$UVICORN" ] || die_gui "Odysseus isn't set up yet. Open Terminal and run:
|
|
|
|
cd $INSTALL_DIR
|
|
python3.11 -m venv venv
|
|
./venv/bin/pip install -r requirements.txt
|
|
./venv/bin/python setup.py"
|
|
|
|
# Open the UI in a chrome-less app window (Chromium browsers), else default browser.
|
|
open_ui() {
|
|
local b base exe bin
|
|
for b in "Google Chrome" "Microsoft Edge" "Brave Browser" "Chromium"; do
|
|
for base in "/Applications" "$HOME/Applications"; do
|
|
if [ -d "$base/$b.app" ]; then
|
|
exe="$(/usr/bin/defaults read "$base/$b.app/Contents/Info" CFBundleExecutable 2>/dev/null)"
|
|
bin="$base/$b.app/Contents/MacOS/$exe"
|
|
if [ -x "$bin" ]; then
|
|
"$bin" --app="$URL" --new-window >/dev/null 2>&1 &
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
done
|
|
/usr/bin/open "$URL"
|
|
}
|
|
|
|
mkdir -p "$INSTALL_DIR/logs"
|
|
|
|
# Already running? Just open the UI.
|
|
if /usr/bin/curl -s -o /dev/null --max-time 2 "$URL"; then
|
|
open_ui
|
|
exit 0
|
|
fi
|
|
|
|
notify "Starting…"
|
|
cd "$INSTALL_DIR" || die_gui "Install folder not found: $INSTALL_DIR"
|
|
if [ "$(uname -m)" = "arm64" ]; then
|
|
arch -arm64 "$UVICORN" app:app --host 127.0.0.1 --port "$PORT" >>"$LOG" 2>&1 &
|
|
else
|
|
"$UVICORN" app:app --host 127.0.0.1 --port "$PORT" >>"$LOG" 2>&1 &
|
|
fi
|
|
SERVER_PID=$!
|
|
|
|
# Quitting the app stops the server it started.
|
|
trap 'kill $SERVER_PID 2>/dev/null; exit 0' TERM INT
|
|
|
|
# Wait for readiness (first run downloads an embedding model — allow ~2 min).
|
|
READY=0
|
|
for i in $(seq 1 120); do
|
|
/usr/bin/curl -s -o /dev/null --max-time 2 "$URL" && { READY=1; break; }
|
|
kill -0 "$SERVER_PID" 2>/dev/null || die_gui "Odysseus failed to start. Log:
|
|
$LOG"
|
|
sleep 1
|
|
done
|
|
|
|
if [ "$READY" = "1" ]; then
|
|
open_ui
|
|
else
|
|
notify "Odysseus is taking a while — open $URL once it finishes starting."
|
|
fi
|
|
wait "$SERVER_PID"
|
|
LAUNCHER
|
|
|
|
sed -e "s|__INSTALL_DIR__|$INSTALL_DIR|g" -e "s|__PORT__|$PORT|g" \
|
|
"$APP/Contents/MacOS/$APP_NAME.tmpl" > "$APP/Contents/MacOS/$APP_NAME"
|
|
rm -f "$APP/Contents/MacOS/$APP_NAME.tmpl"
|
|
chmod +x "$APP/Contents/MacOS/$APP_NAME"
|
|
|
|
# Refresh Finder's icon cache for the new bundle.
|
|
touch "$APP"
|
|
|
|
# ── .dmg (drag-to-Applications) ──
|
|
echo "Packaging dist/$APP_NAME.dmg"
|
|
STAGE="$(mktemp -d)/dmg"
|
|
mkdir -p "$STAGE"
|
|
cp -R "$APP" "$STAGE/"
|
|
ln -s /Applications "$STAGE/Applications"
|
|
rm -f "$DIST/$APP_NAME.dmg"
|
|
hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO "$DIST/$APP_NAME.dmg" >/dev/null
|
|
rm -rf "$STAGE"
|
|
|
|
echo ""
|
|
echo "Done:"
|
|
echo " $APP"
|
|
echo " $DIST/$APP_NAME.dmg"
|
|
echo ""
|
|
echo "Run it: open '$APP'"
|
|
echo "Install: open '$DIST/$APP_NAME.dmg' (drag Odysseus to Applications)"
|