diff --git a/bin/disinto b/bin/disinto index 18179df..bf8edc4 100755 --- a/bin/disinto +++ b/bin/disinto @@ -1488,28 +1488,15 @@ p.write_text(text) touch "${FACTORY_ROOT}/.env" fi - # Configure Forgejo and Woodpecker URLs when EDGE_TUNNEL_FQDN is set. - # In subdomain mode, uses per-service FQDNs at root path instead of subpath URLs. + # Configure Forgejo and Woodpecker subpath URLs when EDGE_TUNNEL_FQDN is set if [ -n "${EDGE_TUNNEL_FQDN:-}" ]; then - local routing_mode="${EDGE_ROUTING_MODE:-subpath}" - if [ "$routing_mode" = "subdomain" ]; then - # Subdomain mode: Forgejo at forge..disinto.ai (root path) - if ! grep -q '^FORGEJO_ROOT_URL=' "${FACTORY_ROOT}/.env" 2>/dev/null; then - echo "FORGEJO_ROOT_URL=https://${EDGE_TUNNEL_FQDN_FORGE:-forge.${EDGE_TUNNEL_FQDN}}/" >> "${FACTORY_ROOT}/.env" - fi - # Subdomain mode: Woodpecker at ci..disinto.ai (root path) - if ! grep -q '^WOODPECKER_HOST=' "${FACTORY_ROOT}/.env" 2>/dev/null; then - echo "WOODPECKER_HOST=https://${EDGE_TUNNEL_FQDN_CI:-ci.${EDGE_TUNNEL_FQDN}}" >> "${FACTORY_ROOT}/.env" - fi - else - # Subpath mode: Forgejo ROOT_URL with /forge/ subpath (trailing slash required) - if ! grep -q '^FORGEJO_ROOT_URL=' "${FACTORY_ROOT}/.env" 2>/dev/null; then - echo "FORGEJO_ROOT_URL=https://${EDGE_TUNNEL_FQDN}/forge/" >> "${FACTORY_ROOT}/.env" - fi - # Subpath mode: Woodpecker WOODPECKER_HOST with /ci subpath (no trailing slash for v3) - if ! grep -q '^WOODPECKER_HOST=' "${FACTORY_ROOT}/.env" 2>/dev/null; then - echo "WOODPECKER_HOST=https://${EDGE_TUNNEL_FQDN}/ci" >> "${FACTORY_ROOT}/.env" - fi + # Forgejo ROOT_URL with /forge/ subpath (note trailing slash - Forgejo needs it) + if ! grep -q '^FORGEJO_ROOT_URL=' "${FACTORY_ROOT}/.env" 2>/dev/null; then + echo "FORGEJO_ROOT_URL=https://${EDGE_TUNNEL_FQDN}/forge/" >> "${FACTORY_ROOT}/.env" + fi + # Woodpecker WOODPECKER_HOST with /ci subpath (no trailing slash for v3) + if ! grep -q '^WOODPECKER_HOST=' "${FACTORY_ROOT}/.env" 2>/dev/null; then + echo "WOODPECKER_HOST=https://${EDGE_TUNNEL_FQDN}/ci" >> "${FACTORY_ROOT}/.env" fi fi @@ -1616,15 +1603,9 @@ p.write_text(text) create_woodpecker_oauth "$forge_url" "$forge_repo" # Create OAuth2 app on Forgejo for disinto-chat (#708) - # In subdomain mode, callback is at chat. root instead of /chat/ subpath. local chat_redirect_uri if [ -n "${EDGE_TUNNEL_FQDN:-}" ]; then - local chat_routing_mode="${EDGE_ROUTING_MODE:-subpath}" - if [ "$chat_routing_mode" = "subdomain" ]; then - chat_redirect_uri="https://${EDGE_TUNNEL_FQDN_CHAT:-chat.${EDGE_TUNNEL_FQDN}}/oauth/callback" - else - chat_redirect_uri="https://${EDGE_TUNNEL_FQDN}/chat/oauth/callback" - fi + chat_redirect_uri="https://${EDGE_TUNNEL_FQDN}/chat/oauth/callback" else chat_redirect_uri="http://localhost/chat/oauth/callback" fi @@ -2824,29 +2805,15 @@ disinto_edge() { # Write to .env (replace existing entries to avoid duplicates) local tmp_env tmp_env=$(mktemp) - grep -Ev "^EDGE_TUNNEL_(HOST|PORT|FQDN|FQDN_FORGE|FQDN_CI|FQDN_CHAT)=" "$env_file" > "$tmp_env" 2>/dev/null || true + grep -Ev "^EDGE_TUNNEL_(HOST|PORT|FQDN)=" "$env_file" > "$tmp_env" 2>/dev/null || true mv "$tmp_env" "$env_file" echo "EDGE_TUNNEL_HOST=${edge_host}" >> "$env_file" echo "EDGE_TUNNEL_PORT=${port}" >> "$env_file" echo "EDGE_TUNNEL_FQDN=${fqdn}" >> "$env_file" - # Subdomain mode: write per-service FQDNs (#1028) - local reg_routing_mode="${EDGE_ROUTING_MODE:-subpath}" - if [ "$reg_routing_mode" = "subdomain" ]; then - echo "EDGE_TUNNEL_FQDN_FORGE=forge.${fqdn}" >> "$env_file" - echo "EDGE_TUNNEL_FQDN_CI=ci.${fqdn}" >> "$env_file" - echo "EDGE_TUNNEL_FQDN_CHAT=chat.${fqdn}" >> "$env_file" - fi - echo "Registered: ${project}" echo " Port: ${port}" echo " FQDN: ${fqdn}" - if [ "$reg_routing_mode" = "subdomain" ]; then - echo " Mode: subdomain" - echo " Forge: forge.${fqdn}" - echo " CI: ci.${fqdn}" - echo " Chat: chat.${fqdn}" - fi echo " Saved to: ${env_file}" ;; diff --git a/docker/chat/server.py b/docker/chat/server.py index 0623955..de65d16 100644 --- a/docker/chat/server.py +++ b/docker/chat/server.py @@ -52,8 +52,6 @@ FORGE_URL = os.environ.get("FORGE_URL", "http://localhost:3000") CHAT_OAUTH_CLIENT_ID = os.environ.get("CHAT_OAUTH_CLIENT_ID", "") CHAT_OAUTH_CLIENT_SECRET = os.environ.get("CHAT_OAUTH_CLIENT_SECRET", "") EDGE_TUNNEL_FQDN = os.environ.get("EDGE_TUNNEL_FQDN", "") -EDGE_TUNNEL_FQDN_CHAT = os.environ.get("EDGE_TUNNEL_FQDN_CHAT", "") -EDGE_ROUTING_MODE = os.environ.get("EDGE_ROUTING_MODE", "subpath") # Shared secret for Caddy forward_auth verify endpoint (#709). # When set, only requests carrying this value in X-Forward-Auth-Secret are @@ -126,8 +124,6 @@ OPCODE_PONG = 0xA def _build_callback_uri(): """Build the OAuth callback URI based on tunnel configuration.""" - if EDGE_ROUTING_MODE == "subdomain" and EDGE_TUNNEL_FQDN_CHAT: - return f"https://{EDGE_TUNNEL_FQDN_CHAT}/oauth/callback" if EDGE_TUNNEL_FQDN: return f"https://{EDGE_TUNNEL_FQDN}/chat/oauth/callback" return "http://localhost/chat/oauth/callback" @@ -335,14 +331,47 @@ class _WebSocketHandler: self.message_queue = message_queue self.closed = False - async def accept_connection(self, sec_websocket_key, sec_websocket_protocol=None): - """Accept the WebSocket handshake. + async def accept_connection(self): + """Accept the WebSocket handshake.""" + # Read the HTTP request + request_line = await self._read_line() + if not request_line.startswith("GET "): + self._close_connection() + return False + + # Parse the request + headers = {} + while True: + line = await self._read_line() + if line == "": + break + if ":" in line: + key, value = line.split(":", 1) + headers[key.strip().lower()] = value.strip() + + # Validate WebSocket upgrade + if headers.get("upgrade", "").lower() != "websocket": + self._send_http_error(400, "Bad Request", "WebSocket upgrade required") + self._close_connection() + return False + + if headers.get("connection", "").lower() != "upgrade": + self._send_http_error(400, "Bad Request", "Connection upgrade required") + self._close_connection() + return False + + # Get Sec-WebSocket-Key + sec_key = headers.get("sec-websocket-key", "") + if not sec_key: + self._send_http_error(400, "Bad Request", "Missing Sec-WebSocket-Key") + self._close_connection() + return False + + # Get Sec-WebSocket-Protocol if provided + sec_protocol = headers.get("sec-websocket-protocol", "") - The HTTP request has already been parsed by BaseHTTPRequestHandler, - so we use the provided key and protocol instead of re-reading from socket. - """ # Validate subprotocol - if sec_websocket_protocol and sec_websocket_protocol != WEBSOCKET_SUBPROTOCOL: + if sec_protocol and sec_protocol != WEBSOCKET_SUBPROTOCOL: self._send_http_error( 400, "Bad Request", @@ -352,7 +381,7 @@ class _WebSocketHandler: return False # Generate accept key - accept_key = self._generate_accept_key(sec_websocket_key) + accept_key = self._generate_accept_key(sec_key) # Send handshake response response = ( @@ -362,8 +391,8 @@ class _WebSocketHandler: f"Sec-WebSocket-Accept: {accept_key}\r\n" ) - if sec_websocket_protocol: - response += f"Sec-WebSocket-Protocol: {sec_websocket_protocol}\r\n" + if sec_protocol: + response += f"Sec-WebSocket-Protocol: {sec_protocol}\r\n" response += "\r\n" self.writer.write(response.encode("utf-8")) @@ -458,8 +487,10 @@ class _WebSocketHandler: async def _decode_frame(self): """Decode a WebSocket frame. Returns (opcode, payload).""" try: - # Read first two bytes (use readexactly for guaranteed length) - header = await self.reader.readexactly(2) + # Read first two bytes + header = await self.reader.read(2) + if len(header) < 2: + return None, None fin = (header[0] >> 7) & 1 opcode = header[0] & 0x0F @@ -468,18 +499,18 @@ class _WebSocketHandler: # Extended payload length if length == 126: - ext = await self.reader.readexactly(2) + ext = await self.reader.read(2) length = struct.unpack(">H", ext)[0] elif length == 127: - ext = await self.reader.readexactly(8) + ext = await self.reader.read(8) length = struct.unpack(">Q", ext)[0] # Masking key if masked: - mask_key = await self.reader.readexactly(4) + mask_key = await self.reader.read(4) # Payload - payload = await self.reader.readexactly(length) + payload = await self.reader.read(length) # Unmask if needed if masked: @@ -499,22 +530,15 @@ class _WebSocketHandler: break if opcode == OPCODE_CLOSE: - await self._send_close() + self._send_close() break elif opcode == OPCODE_PING: - await self._send_pong(payload) + self._send_pong(payload) elif opcode == OPCODE_PONG: pass # Ignore pong elif opcode in (OPCODE_TEXT, OPCODE_BINARY): - # Handle text messages from client (e.g., chat_request) - try: - msg = payload.decode("utf-8") - data = json.loads(msg) - if data.get("type") == "chat_request": - # Invoke Claude with the message - await self._handle_chat_request(data.get("message", "")) - except (json.JSONDecodeError, UnicodeDecodeError): - pass + # Handle text messages from client (e.g., heartbeat ack) + pass # Check if we should stop waiting for messages if self.closed: @@ -524,103 +548,25 @@ class _WebSocketHandler: print(f"WebSocket connection error: {e}", file=sys.stderr) finally: self._close_connection() - # Clean up the message queue on disconnect - if self.user in _websocket_queues: - del _websocket_queues[self.user] - async def _send_close(self): + def _send_close(self): """Send a close frame.""" try: - # Close code 1000 = normal closure - frame = self._encode_frame(OPCODE_CLOSE, struct.pack(">H", 1000)) + frame = self._encode_frame(OPCODE_CLOSE, b"\x03\x00") self.writer.write(frame) - await self.writer.drain() + self.writer.drain() except Exception: pass - async def _send_pong(self, payload): + def _send_pong(self, payload): """Send a pong frame.""" try: frame = self._encode_frame(OPCODE_PONG, payload) self.writer.write(frame) - await self.writer.drain() + self.writer.drain() except Exception: pass - async def _handle_chat_request(self, message): - """Handle a chat_request WebSocket frame by invoking Claude.""" - if not message: - return - - # Validate Claude binary exists - if not os.path.exists(CLAUDE_BIN): - await self.send_text(json.dumps({ - "type": "error", - "message": "Claude CLI not found", - })) - return - - try: - # Spawn claude --print with stream-json for streaming output - proc = subprocess.Popen( - [CLAUDE_BIN, "--print", "--output-format", "stream-json", message], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, - ) - - # Stream output line by line - for line in iter(proc.stdout.readline, ""): - line = line.strip() - if not line: - continue - try: - event = json.loads(line) - etype = event.get("type", "") - - # Extract text content from content_block_delta events - if etype == "content_block_delta": - delta = event.get("delta", {}) - if delta.get("type") == "text_delta": - text = delta.get("text", "") - if text: - # Send tokens to client - await self.send_text(text) - - # Check for usage event to know when complete - if etype == "result": - pass # Will send complete after loop - - except json.JSONDecodeError: - pass - - # Wait for process to complete - proc.wait() - - if proc.returncode != 0: - await self.send_text(json.dumps({ - "type": "error", - "message": f"Claude CLI failed with exit code {proc.returncode}", - })) - return - - # Send complete signal - await self.send_text(json.dumps({ - "type": "complete", - })) - - except FileNotFoundError: - await self.send_text(json.dumps({ - "type": "error", - "message": "Claude CLI not found", - })) - except Exception as e: - await self.send_text(json.dumps({ - "type": "error", - "message": str(e), - })) - # ============================================================================= # Conversation History Functions (#710) @@ -1309,30 +1255,28 @@ class ChatHandler(BaseHTTPRequestHandler): # Create message queue for this user _websocket_queues[user] = asyncio.Queue() - # Get WebSocket upgrade headers from the HTTP request - sec_websocket_key = self.headers.get("Sec-WebSocket-Key", "") - sec_websocket_protocol = self.headers.get("Sec-WebSocket-Protocol", "") - - # Validate Sec-WebSocket-Key - if not sec_websocket_key: - self.send_error_page(400, "Bad Request", "Missing Sec-WebSocket-Key") - return - # Get the socket from the connection sock = self.connection sock.setblocking(False) + reader = asyncio.StreamReader() + protocol = asyncio.StreamReaderProtocol(reader) # Create async server to handle the connection async def handle_ws(): try: - # Wrap the socket in asyncio streams using open_connection - reader, writer = await asyncio.open_connection(sock=sock) + # Wrap the socket in asyncio streams + transport, _ = await asyncio.get_event_loop().create_connection( + lambda: protocol, + sock=sock, + ) + ws_reader = protocol._stream_reader + ws_writer = transport # Create WebSocket handler - ws_handler = _WebSocketHandler(reader, writer, user, _websocket_queues[user]) + ws_handler = _WebSocketHandler(ws_reader, ws_writer, user, _websocket_queues[user]) - # Accept the connection (pass headers from HTTP request) - if not await ws_handler.accept_connection(sec_websocket_key, sec_websocket_protocol): + # Accept the connection + if not await ws_handler.accept_connection(): return # Start a task to read from the queue and send to client @@ -1345,8 +1289,8 @@ class ChatHandler(BaseHTTPRequestHandler): # Send ping to keep connection alive try: frame = ws_handler._encode_frame(OPCODE_PING, b"") - writer.write(frame) - await writer.drain() + ws_writer.write(frame) + await ws_writer.drain() except Exception: break except Exception as e: @@ -1370,8 +1314,8 @@ class ChatHandler(BaseHTTPRequestHandler): print(f"WebSocket handler error: {e}", file=sys.stderr) finally: try: - writer.close() - await writer.wait_closed() + ws_writer.close() + await ws_writer.wait_closed() except Exception: pass diff --git a/lib/ci-setup.sh b/lib/ci-setup.sh index 507affb..319e83e 100644 --- a/lib/ci-setup.sh +++ b/lib/ci-setup.sh @@ -142,7 +142,6 @@ _create_forgejo_oauth_app() { # Set up Woodpecker CI to use Forgejo as its forge backend. # Creates an OAuth2 app on Forgejo for Woodpecker, activates the repo. -# Respects EDGE_ROUTING_MODE: in subdomain mode, uses EDGE_TUNNEL_FQDN_CI for redirect URI. # Usage: create_woodpecker_oauth _create_woodpecker_oauth_impl() { local forge_url="$1" @@ -151,13 +150,7 @@ _create_woodpecker_oauth_impl() { echo "" echo "── Woodpecker OAuth2 setup ────────────────────────────" - local wp_redirect_uri="http://localhost:8000/authorize" - local routing_mode="${EDGE_ROUTING_MODE:-subpath}" - if [ "$routing_mode" = "subdomain" ] && [ -n "${EDGE_TUNNEL_FQDN_CI:-}" ]; then - wp_redirect_uri="https://${EDGE_TUNNEL_FQDN_CI}/authorize" - fi - - _create_forgejo_oauth_app "woodpecker-ci" "$wp_redirect_uri" || return 0 + _create_forgejo_oauth_app "woodpecker-ci" "http://localhost:8000/authorize" || return 0 local client_id="${_OAUTH_CLIENT_ID}" local client_secret="${_OAUTH_CLIENT_SECRET}" @@ -165,15 +158,10 @@ _create_woodpecker_oauth_impl() { # WP_FORGEJO_CLIENT/SECRET match the docker-compose.yml variable references # WOODPECKER_HOST must be host-accessible URL to match OAuth2 redirect_uri local env_file="${FACTORY_ROOT}/.env" - local wp_host="http://localhost:8000" - if [ "$routing_mode" = "subdomain" ] && [ -n "${EDGE_TUNNEL_FQDN_CI:-}" ]; then - wp_host="https://${EDGE_TUNNEL_FQDN_CI}" - fi - local wp_vars=( "WOODPECKER_FORGEJO=true" "WOODPECKER_FORGEJO_URL=${forge_url}" - "WOODPECKER_HOST=${wp_host}" + "WOODPECKER_HOST=http://localhost:8000" ) if [ -n "${client_id:-}" ]; then wp_vars+=("WP_FORGEJO_CLIENT=${client_id}") diff --git a/lib/generators.sh b/lib/generators.sh index 739ca50..eb223e8 100644 --- a/lib/generators.sh +++ b/lib/generators.sh @@ -607,12 +607,9 @@ COMPOSEEOF - EDGE_TUNNEL_USER=${EDGE_TUNNEL_USER:-tunnel} - EDGE_TUNNEL_PORT=${EDGE_TUNNEL_PORT:-} - EDGE_TUNNEL_FQDN=${EDGE_TUNNEL_FQDN:-} - # Subdomain fallback (#1028): per-service FQDNs for subdomain routing mode. - # Set EDGE_ROUTING_MODE=subdomain to activate. See docs/edge-routing-fallback.md. - - EDGE_ROUTING_MODE=${EDGE_ROUTING_MODE:-subpath} - - EDGE_TUNNEL_FQDN_FORGE=${EDGE_TUNNEL_FQDN_FORGE:-} - - EDGE_TUNNEL_FQDN_CI=${EDGE_TUNNEL_FQDN_CI:-} - - EDGE_TUNNEL_FQDN_CHAT=${EDGE_TUNNEL_FQDN_CHAT:-} + # Subdomain fallback (#713): if subpath routing (#704/#708) fails, add: + # EDGE_TUNNEL_FQDN_FORGE, EDGE_TUNNEL_FQDN_CI, EDGE_TUNNEL_FQDN_CHAT + # See docs/edge-routing-fallback.md for the full pivot plan. # Shared secret for Caddy ↔ chat forward_auth (#709) - FORWARD_AUTH_SECRET=${FORWARD_AUTH_SECRET:-} volumes: @@ -703,8 +700,6 @@ COMPOSEEOF CHAT_OAUTH_CLIENT_ID: ${CHAT_OAUTH_CLIENT_ID:-} CHAT_OAUTH_CLIENT_SECRET: ${CHAT_OAUTH_CLIENT_SECRET:-} EDGE_TUNNEL_FQDN: ${EDGE_TUNNEL_FQDN:-} - EDGE_TUNNEL_FQDN_CHAT: ${EDGE_TUNNEL_FQDN_CHAT:-} - EDGE_ROUTING_MODE: ${EDGE_ROUTING_MODE:-subpath} DISINTO_CHAT_ALLOWED_USERS: ${DISINTO_CHAT_ALLOWED_USERS:-} # Shared secret for Caddy forward_auth verify endpoint (#709) FORWARD_AUTH_SECRET: ${FORWARD_AUTH_SECRET:-} @@ -810,11 +805,6 @@ _generate_agent_docker_impl() { # Output path: ${FACTORY_ROOT}/docker/Caddyfile (gitignored — generated artifact). # The edge compose service mounts this path as /etc/caddy/Caddyfile. # On a fresh clone, `disinto init` calls generate_caddyfile before first `disinto up`. -# -# Routing mode (EDGE_ROUTING_MODE env var): -# subpath — (default) all services under .disinto.ai/{forge,ci,chat,staging} -# subdomain — per-service subdomains: forge., ci., chat. -# See docs/edge-routing-fallback.md for the full pivot plan. _generate_caddyfile_impl() { local docker_dir="${FACTORY_ROOT}/docker" local caddyfile="${docker_dir}/Caddyfile" @@ -824,22 +814,8 @@ _generate_caddyfile_impl() { return fi - local routing_mode="${EDGE_ROUTING_MODE:-subpath}" - - if [ "$routing_mode" = "subdomain" ]; then - _generate_caddyfile_subdomain "$caddyfile" - else - _generate_caddyfile_subpath "$caddyfile" - fi - - echo "Created: ${caddyfile} (routing_mode=${routing_mode})" -} - -# Subpath Caddyfile: all services under a single :80 block with path-based routing. -_generate_caddyfile_subpath() { - local caddyfile="$1" cat > "$caddyfile" <<'CADDYFILEEOF' -# Caddyfile — edge proxy configuration (subpath mode) +# Caddyfile — edge proxy configuration # IP-only binding at bootstrap; domain + TLS added later via vault resource request :80 { @@ -882,50 +858,8 @@ _generate_caddyfile_subpath() { } } CADDYFILEEOF -} -# Subdomain Caddyfile: four host blocks per docs/edge-routing-fallback.md. -# Uses env vars EDGE_TUNNEL_FQDN_FORGE, EDGE_TUNNEL_FQDN_CI, EDGE_TUNNEL_FQDN_CHAT, -# and EDGE_TUNNEL_FQDN (main project domain → staging). -_generate_caddyfile_subdomain() { - local caddyfile="$1" - cat > "$caddyfile" <<'CADDYFILEEOF' -# Caddyfile — edge proxy configuration (subdomain mode) -# Per-service subdomains; see docs/edge-routing-fallback.md - -# Main project domain — staging / landing -{$EDGE_TUNNEL_FQDN} { - reverse_proxy staging:80 -} - -# Forgejo — root path, no subpath rewrite needed -{$EDGE_TUNNEL_FQDN_FORGE} { - reverse_proxy forgejo:3000 -} - -# Woodpecker CI — root path -{$EDGE_TUNNEL_FQDN_CI} { - reverse_proxy woodpecker:8000 -} - -# Chat — with forward_auth (#709, on its own host) -{$EDGE_TUNNEL_FQDN_CHAT} { - handle /login { - reverse_proxy chat:8080 - } - handle /oauth/callback { - reverse_proxy chat:8080 - } - handle /* { - forward_auth chat:8080 { - uri /auth/verify - copy_headers X-Forwarded-User - header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET} - } - reverse_proxy chat:8080 - } -} -CADDYFILEEOF + echo "Created: ${caddyfile}" } # Generate docker/index.html default page. diff --git a/projects/disinto.toml.example b/projects/disinto.toml.example index 34eacae..ebe6eed 100644 --- a/projects/disinto.toml.example +++ b/projects/disinto.toml.example @@ -59,23 +59,6 @@ check_pipeline_stall = false # compact_pct = 60 # poll_interval = 60 -# Edge routing mode (default: subpath) -# -# Controls how services are exposed through the edge proxy. -# subpath — all services under .disinto.ai/{forge,ci,chat,staging} -# subdomain — per-service subdomains: forge., ci., chat. -# -# Set to "subdomain" if subpath routing causes unfixable issues (redirect loops, -# OAuth callback mismatches, cookie collisions). See docs/edge-routing-fallback.md. -# -# Set in .env (not TOML) since it's consumed by docker-compose and shell scripts: -# EDGE_ROUTING_MODE=subdomain -# -# In subdomain mode, `disinto edge register` also writes: -# EDGE_TUNNEL_FQDN_FORGE=forge..disinto.ai -# EDGE_TUNNEL_FQDN_CI=ci..disinto.ai -# EDGE_TUNNEL_FQDN_CHAT=chat..disinto.ai - # [mirrors] # github = "git@github.com:johba/disinto.git" # codeberg = "git@codeberg.org:johba/disinto.git" diff --git a/tools/edge-control/register.sh b/tools/edge-control/register.sh index ee12ef7..3ac0d09 100755 --- a/tools/edge-control/register.sh +++ b/tools/edge-control/register.sh @@ -39,10 +39,13 @@ EOF exit 1 } +# TODO(#713): Subdomain fallback — if subpath routing (#704/#708) fails, this +# function would need to register additional routes for forge., +# ci., chat. subdomains (or accept a --subdomain parameter). +# See docs/edge-routing-fallback.md for the full pivot plan. + # Register a new tunnel # Usage: do_register -# When EDGE_ROUTING_MODE=subdomain, also registers forge., ci., -# and chat. subdomain routes (see docs/edge-routing-fallback.md). do_register() { local project="$1" local pubkey="$2" @@ -76,32 +79,17 @@ do_register() { local port port=$(allocate_port "$project" "$full_pubkey" "${project}.${DOMAIN_SUFFIX}") - # Add Caddy route for main project domain + # Add Caddy route add_route "$project" "$port" - # Subdomain mode: register additional routes for per-service subdomains - local routing_mode="${EDGE_ROUTING_MODE:-subpath}" - if [ "$routing_mode" = "subdomain" ]; then - local subdomain - for subdomain in forge ci chat; do - add_route "${subdomain}.${project}" "$port" - done - fi - # Rebuild authorized_keys for tunnel user rebuild_authorized_keys # Reload Caddy reload_caddy - # Build JSON response - local response="{\"port\":${port},\"fqdn\":\"${project}.${DOMAIN_SUFFIX}\"" - if [ "$routing_mode" = "subdomain" ]; then - response="${response},\"routing_mode\":\"subdomain\"" - response="${response},\"subdomains\":{\"forge\":\"forge.${project}.${DOMAIN_SUFFIX}\",\"ci\":\"ci.${project}.${DOMAIN_SUFFIX}\",\"chat\":\"chat.${project}.${DOMAIN_SUFFIX}\"}" - fi - response="${response}}" - echo "$response" + # Return JSON response + echo "{\"port\":${port},\"fqdn\":\"${project}.${DOMAIN_SUFFIX}\"}" } # Deregister a tunnel @@ -121,18 +109,9 @@ do_deregister() { # Remove from registry free_port "$project" >/dev/null - # Remove Caddy route for main project domain + # Remove Caddy route remove_route "$project" - # Subdomain mode: also remove per-service subdomain routes - local routing_mode="${EDGE_ROUTING_MODE:-subpath}" - if [ "$routing_mode" = "subdomain" ]; then - local subdomain - for subdomain in forge ci chat; do - remove_route "${subdomain}.${project}" - done - fi - # Rebuild authorized_keys for tunnel user rebuild_authorized_keys