Skip to content

msc_pubFosGit #8

Description

@Phovos

MSCFRQ project: MscFosGit

pyproject.toml: implied

#!/usr/bin/env -S uv run
# -*- coding: utf-8 -*-
# /* script
# requires-python = ">=3.14"
# dependencies = [
#     "uv==*.*",
# ]
#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────────────
# serve.sh SCM Fossil Router Quine ::SCMFRQ::
#
# Boolean (IS_PUBLIC) determines the execution topology:
#
#   IS_PUBLIC=false  →  STATE A: local dev, full admin bypass, HTTP
#   IS_PUBLIC=true   →  STATE B: production, SCGI, cloudflared tunnel
#
# Usage:
#   IS_PUBLIC=false ./serve.sh                  # development
#   IS_PUBLIC=true TUNNEL_NAME=mywiki ./serve.sh  # production (named tunnel)
#   IS_PUBLIC=true ./serve.sh                   # production (ephemeral quick tunnel)
#
# Dependencies:
#   STATE A: fossil
#   STATE B: fossil, python3 (stdlib only, no pip), cloudflared
#
# Traffic topology (STATE B):
#
#   Internet → Cloudflare Edge (TLS, Worker, cache)
#     → cloudflared (outbound tunnel, HTTP/2)
#       → Python adapter (HTTP → SCGI, $HTTP_PORT)
#         → Fossil SCGI server ($SCGI_PORT, loopback only)
#
# To sync local state to a remote backup:
#   fossil push https://remote-fossil-server.com/repo
#
# The CGI/SCGI environment dictionary is IDENTICAL in both states.
# What runs in STATE A is what runs in STATE B (same binary, same env keys),
# different auth policy and network exposure. No CI/CD pipeline needed.
# ──────────────────────────────────────────────────────────────────────────────

set -euo pipefail

# ── Configuration ──────────────────────────────────────────────────────────────
# All of these can be overridden via environment variables.

: "${IS_PUBLIC:=false}"
: "${FOSSIL_REPO:=./repo.fossil}"     # path to your .fossil database file
: "${HTTP_PORT:=8080}"                # HTTP port: local server OR adapter listener
: "${SCGI_PORT:=9000}"                # SCGI port: Fossil in production mode
: "${TUNNEL_NAME:=}"                  # cloudflared named tunnel (empty = quick tunnel)
: "${OPEN_BROWSER:=true}"             # auto-open browser in STATE A

# ── Terminal colors (suppressed if not a TTY) ──────────────────────────────────
if [ -t 1 ]; then
    R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m' B='\033[0;34m'
    BOLD='\033[1m' DIM='\033[2m' RST='\033[0m'
else
    R='' G='' Y='' B='' BOLD='' DIM='' RST=''
fi

# ── Error helper ───────────────────────────────────────────────────────────────
die() { echo -e "${R}ERROR:${RST} $*" >&2; exit 1; }

# ── Dependency checks ──────────────────────────────────────────────────────────
command -v fossil &>/dev/null || die \
    "fossil not found in PATH.\n  Install: https://fossil-scm.org/home/uv/download.html"

if [[ ! -f "$FOSSIL_REPO" ]]; then
    echo -e "${Y}Repo not found: creating new Fossil repo at $FOSSIL_REPO${RST}"
    fossil init "$FOSSIL_REPO"
    echo ""
fi

# ── Process tracking and cleanup ───────────────────────────────────────────────
declare -a PIDS=()

cleanup() {
    echo ""
    echo -e "${Y}Shutting down...${RST}"
    for pid in "${PIDS[@]:-}"; do
        if kill -0 "$pid" 2>/dev/null; then
            kill "$pid" 2>/dev/null || true
        fi
    done
    wait 2>/dev/null || true
    echo -e "${G}All processes stopped.${RST}"
}

trap cleanup EXIT INT TERM

# ── Browser opener ─────────────────────────────────────────────────────────────
open_browser() {
    [[ "$OPEN_BROWSER" != "true" ]] && return
    local url="$1"
    { sleep 0.8
      if   command -v xdg-open &>/dev/null; then xdg-open "$url"
      elif command -v open     &>/dev/null; then open     "$url"
      fi
    } &
}

# ══════════════════════════════════════════════════════════════════════════════
# STATE A — Local Development
# ══════════════════════════════════════════════════════════════════════════════
#
# fossil server --localauth
#
# --localauth is Fossil's own designed development idiom, not a workaround.
# It grants admin privileges to any connection from 127.0.0.1, which means
# zero auth friction during local development. It is not active in production.
#
# --localhost ensures the port is bound to the loopback interface only.
# A local attacker who can reach 127.0.0.1 already owns your machine.
# Network-level exposure in dev is the actual risk, not loopback admin access.
# ══════════════════════════════════════════════════════════════════════════════

run_local() {
    echo ""
    echo -e "${BOLD}══════════════════════════════════════════${RST}"
    echo -e "${BOLD} STATE A — Local Development${RST}"
    echo -e "${BOLD}══════════════════════════════════════════${RST}"
    echo -e "  Repo   : ${B}$FOSSIL_REPO${RST}"
    echo -e "  URL    : ${G}http://localhost:$HTTP_PORT${RST}"
    echo -e "  Auth   : ${Y}--localauth  (admin bypass for 127.0.0.1)${RST}"
    echo -e "  Scope  : ${Y}loopback only — no LAN or internet exposure${RST}"
    echo ""

    fossil server           \
        --port      "$HTTP_PORT"    \
        --localhost             \
        --localauth             \
        "$FOSSIL_REPO"          &

    local fossil_pid=$!
    PIDS+=("$fossil_pid")

    echo -e "${G}✓${RST} Fossil HTTP server started (PID $fossil_pid)"
    echo ""
    echo -e "${DIM}  Common paths:${RST}"
    echo -e "    Timeline  http://localhost:$HTTP_PORT/timeline"
    echo -e "    Wiki      http://localhost:$HTTP_PORT/wiki"
    echo -e "    Files     http://localhost:$HTTP_PORT/dir"
    echo -e "    Admin     http://localhost:$HTTP_PORT/setup"
    echo -e "    Raw CGI   source ./mock_env.conf && fossil_cgi_get /timeline"
    echo ""
    echo -e "${Y}Ctrl+C to stop${RST}"

    open_browser "http://localhost:$HTTP_PORT"

    wait "$fossil_pid"
}

# ══════════════════════════════════════════════════════════════════════════════
# STATE B Production
# ══════════════════════════════════════════════════════════════════════════════
#
# Three processes, in order of dependency:
#
#   1. Fossil SCGI server: loopback only, no --localauth
#   2. Python HTTP→SCGI adapter: pure stdlib, converts HTTP to SCGI protocol
#   3. cloudflared tunnel: outbound only, no inbound firewall rules needed
#
# Why a Python adapter instead of nginx?
#   nginx adds configuration surface area, a separate process to maintain,
#   and a dependency the user has to install. The Python adapter below is
#   ~100 lines of stdlib code that does exactly one thing and nothing else.
#   It is inspectable, auditable, and disposable.
#
# Why SCGI instead of HTTP for Fossil?
#   Fossil's HTTP mode forks a new process per request (CGI model under the hood).
#   SCGI mode keeps one persistent Fossil process, which is faster and uses
#   less memory under concurrent load. The adapter handles the protocol
#   translation so nothing else in the stack needs to know about SCGI.
# ══════════════════════════════════════════════════════════════════════════════

# The Python HTTP→SCGI adapter, written to a temp file to avoid
# heredoc quoting conflicts with Python f-strings and $ signs.
# Uses only Python stdlib. No pip. No virtualenv.
write_adapter_script() {
    local outfile="$1"
    cat > "$outfile" <<'PYEOF'
"""
fossil_adapter.py — Minimal HTTP → SCGI bridge for Fossil SCM.
Reads SCGI_PORT and HTTP_PORT from environment variables.
Pure stdlib. No dependencies.
"""

import os
import sys
import socket
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler

SCGI_HOST   = "127.0.0.1"
SCGI_PORT   = int(os.environ["FOSSIL_SCGI_PORT"])
HTTP_PORT   = int(os.environ["FOSSIL_HTTP_PORT"])


# ── SCGI protocol encoder ──────────────────────────────────────────────────────
# SCGI request format:
#   netstring_header + body
#   netstring = LENGTH ':' KEY NUL VALUE NUL ... ','
#
# Mandatory constraints (from SCGI spec):
#   - CONTENT_LENGTH must be the FIRST variable
#   - SCGI=1 must be present
def build_scgi_request(env: dict, body: bytes) -> bytes:
    pairs = []

    # CONTENT_LENGTH first — required by spec
    pairs.append(b"CONTENT_LENGTH\x00" + str(len(body)).encode() + b"\x00")

    # SCGI version flag — required by spec
    pairs.append(b"SCGI\x001\x00")

    # All other environment variables
    for key, val in env.items():
        if key in ("CONTENT_LENGTH", "SCGI"):
            continue
        pairs.append(
            key.encode("utf-8", "replace") + b"\x00"
            + val.encode("utf-8", "replace") + b"\x00"
        )

    header = b"".join(pairs)
    netstring = str(len(header)).encode() + b":" + header + b","
    return netstring + body


# ── SCGI client ────────────────────────────────────────────────────────────────
def forward_to_fossil(env: dict, body: bytes) -> bytes:
    """Send an SCGI request to Fossil and return the raw CGI-style response."""
    request_bytes = build_scgi_request(env, body)

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.settimeout(30)
        sock.connect((SCGI_HOST, SCGI_PORT))
        sock.sendall(request_bytes)

        chunks = []
        while True:
            chunk = sock.recv(65536)
            if not chunk:
                break
            chunks.append(chunk)

    return b"".join(chunks)


# ── CGI response parser ────────────────────────────────────────────────────────
# Fossil responds with CGI-format output:
#   Status: 200 OK\r\n
#   Content-Type: text/html\r\n
#   \r\n
#   <body bytes>
def parse_cgi_response(raw: bytes):
    """Returns (http_status: int, headers: list[tuple], body: bytes)."""
    for sep in (b"\r\n\r\n", b"\n\n"):
        pos = raw.find(sep)
        if pos != -1:
            header_section = raw[:pos].decode("utf-8", "replace")
            body = raw[pos + len(sep):]
            break
    else:
        # Malformed: return 502 with raw body for debugging
        return 502, [("Content-Type", "text/plain")], raw

    http_status = 200
    headers = []

    for line in header_section.splitlines():
        line = line.strip()
        if not line or ":" not in line:
            continue
        name, _, value = line.partition(":")
        name, value = name.strip(), value.strip()

        if name.lower() == "status":
            # "Status: 302 Found" → 302
            try:
                http_status = int(value.split()[0])
            except (ValueError, IndexError):
                pass
        else:
            headers.append((name, value))

    return http_status, headers, body


# ── HTTP request handler ────────────────────────────────────────────────────────
class FossilAdapter(BaseHTTPRequestHandler):
    server_version  = "fossil-scgi-adapter/1.0"
    protocol_version = "HTTP/1.1"

    def log_message(self, fmt, *args):
        sys.stderr.write(f"[adapter] {self.client_address[0]} - {fmt % args}\n")
        sys.stderr.flush()

    def build_env(self, body_len: int) -> dict:
        """
        Map incoming HTTP headers to Fossil's CGI environment dictionary.

        Key mapping rules:
          HTTP headers → HTTP_* env vars (e.g., Accept → HTTP_ACCEPT)
          Fossil reads PATH_INFO for routing, not the full URL.
          REMOTE_ADDR from X-Forwarded-For (set by Cloudflare Worker).
          HTTPS="on" tells Fossil to generate https:// self-links.
        """
        raw_path = self.path
        path_info, _, query_string = raw_path.partition("?")

        host = self.headers.get("Host", "localhost")
        server_name = host.split(":")[0]
        server_port = host.split(":")[-1] if ":" in host else "443"

        # Cloudflare Worker sets X-Forwarded-For to the real client IP.
        # Split on comma in case of proxy chain ("real, proxy1, proxy2").
        forwarded_for = self.headers.get("X-Forwarded-For", self.client_address[0])
        remote_addr = forwarded_for.split(",")[0].strip()

        forwarded_proto = self.headers.get("X-Forwarded-Proto", "https")
        https_flag = "on" if forwarded_proto.lower() == "https" else "off"

        env = {
            # ── Mandatory CGI variables ──────────────────────────────────────
            "GATEWAY_INTERFACE": "CGI/1.1",
            "SERVER_PROTOCOL":   "HTTP/1.1",
            "SERVER_SOFTWARE":   "fossil-scgi-adapter",
            "REQUEST_METHOD":    self.command,
            "SCRIPT_NAME":       "",           # Fossil at the root path
            "PATH_INFO":         path_info,
            "QUERY_STRING":      query_string,
            "SERVER_NAME":       server_name,
            "SERVER_PORT":       server_port,
            "REMOTE_ADDR":       remote_addr,
            "CONTENT_TYPE":      self.headers.get("Content-Type", ""),
            "CONTENT_LENGTH":    str(body_len),
            # ── TLS flag ─────────────────────────────────────────────────────
            # Fossil uses HTTPS=on to generate self-referential https:// URLs.
            # Without this, wiki links and redirects use http:// even on TLS.
            "HTTPS":             https_flag,
            # ── Host for Fossil's URL self-assembly ──────────────────────────
            "HTTP_HOST":         host,
        }

        # Map all remaining HTTP headers to HTTP_* env vars.
        # Skip headers we've already handled explicitly above.
        skip_as_http = {"host", "content-type", "content-length"}
        for key, val in self.headers.items():
            if key.lower() in skip_as_http:
                continue
            env_key = "HTTP_" + key.upper().replace("-", "_")
            env.setdefault(env_key, val)

        return env

    def handle_request(self):
        body_len = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(body_len) if body_len > 0 else b""
        env = self.build_env(body_len)

        try:
            raw = forward_to_fossil(env, body)
            status, headers, body_out = parse_cgi_response(raw)

            self.send_response(status)
            for name, value in headers:
                self.send_header(name, value)
            self.send_header("Content-Length", str(len(body_out)))
            self.end_headers()
            if self.command != "HEAD":
                self.wfile.write(body_out)

        except ConnectionRefusedError:
            msg = b"Fossil SCGI server refused connection"
            self.send_response(502)
            self.send_header("Content-Type", "text/plain")
            self.send_header("Content-Length", str(len(msg)))
            self.end_headers()
            self.wfile.write(msg)

        except socket.timeout:
            msg = b"Fossil SCGI server timed out"
            self.send_response(504)
            self.send_header("Content-Type", "text/plain")
            self.send_header("Content-Length", str(len(msg)))
            self.end_headers()
            self.wfile.write(msg)

        except Exception as exc:
            msg = f"Adapter error: {exc}".encode()
            self.send_response(500)
            self.send_header("Content-Type", "text/plain")
            self.send_header("Content-Length", str(len(msg)))
            self.end_headers()
            self.wfile.write(msg)

    do_GET     = handle_request
    do_POST    = handle_request
    do_HEAD    = handle_request
    do_PUT     = handle_request
    do_DELETE  = handle_request
    do_OPTIONS = handle_request


# ── Threaded server ────────────────────────────────────────────────────────────
class ThreadedHTTPServer(HTTPServer):
    """Process each request in its own daemon thread."""
    def process_request(self, request, client_address):
        t = threading.Thread(
            target=self._handle_in_thread,
            args=(request, client_address),
            daemon=True,
        )
        t.start()

    def _handle_in_thread(self, request, client_address):
        try:
            self.finish_request(request, client_address)
        except Exception:
            self.handle_error(request, client_address)
        finally:
            self.shutdown_request(request)


# ── Entry point ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    server = ThreadedHTTPServer(("127.0.0.1", HTTP_PORT), FossilAdapter)
    print(
        f"[adapter] HTTP→SCGI  127.0.0.1:{HTTP_PORT} → SCGI 127.0.0.1:{SCGI_PORT}",
        flush=True,
    )
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
PYEOF
}

run_production() {
    command -v python3 &>/dev/null || die \
        "python3 not found. Required for HTTP→SCGI adapter."
    command -v cloudflared &>/dev/null || die \
        "cloudflared not found.\n  Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"

    echo ""
    echo -e "${BOLD}══════════════════════════════════════════${RST}"
    echo -e "${BOLD} STATE B — Production${RST}"
    echo -e "${BOLD}══════════════════════════════════════════${RST}"
    echo -e "  Repo    : ${B}$FOSSIL_REPO${RST}"
    echo -e "  SCGI    : ${B}127.0.0.1:$SCGI_PORT${RST}  (Fossil, loopback only)"
    echo -e "  Adapter : ${B}127.0.0.1:$HTTP_PORT${RST}  (HTTP→SCGI bridge)"
    echo -e "  Tunnel  : ${B}${TUNNEL_NAME:-quick (ephemeral trycloudflare.com URL)}${RST}"
    echo ""

    # ── Step 1: Fossil in SCGI mode ────────────────────────────────────────────
    # No --localauth. Auth is Fossil's built-in user table.
    # --localhost: SCGI port bound to loopback only.
    # Even if something bypasses cloudflared, it cannot reach Fossil
    # from the network — only from the local machine.

    fossil server       \
        --scgi          \
        --port "$SCGI_PORT" \
        --localhost     \
        "$FOSSIL_REPO"  &

    local fossil_pid=$!
    PIDS+=("$fossil_pid")
    echo -e "${G}✓${RST} Fossil SCGI on 127.0.0.1:$SCGI_PORT (PID $fossil_pid)"

    sleep 0.3  # give Fossil time to bind its port

    # ── Step 2: HTTP→SCGI adapter ──────────────────────────────────────────────
    local adapter_script
    adapter_script=$(mktemp /tmp/fossil_adapter_XXXXXX.py)

    write_adapter_script "$adapter_script"

    FOSSIL_SCGI_PORT="$SCGI_PORT" \
    FOSSIL_HTTP_PORT="$HTTP_PORT" \
    python3 "$adapter_script" &

    local adapter_pid=$!
    PIDS+=("$adapter_pid")
    # Temp file can be removed now — the process has already loaded it
    rm -f "$adapter_script"
    echo -e "${G}✓${RST} HTTP→SCGI adapter on 127.0.0.1:$HTTP_PORT (PID $adapter_pid)"

    sleep 0.3  # give adapter time to bind

    # ── Step 3: cloudflared tunnel ─────────────────────────────────────────────
    # Outbound-only tunnel to Cloudflare's edge.
    # No inbound firewall rules, no port forwarding, no static IP required.
    #
    # Named tunnel (TUNNEL_NAME set):
    #   Requires: cloudflared tunnel create NAME
    #   Requires: ~/.cloudflared/config.yml mapping tunnel → http://localhost:HTTP_PORT
    #   URL is stable across restarts.
    #
    # Quick tunnel (TUNNEL_NAME empty):
    #   URL is ephemeral (*.trycloudflare.com), changes on restart.
    #   Good for testing the full stack without tunnel setup.

    if [[ -n "$TUNNEL_NAME" ]]; then
        cloudflared tunnel run "$TUNNEL_NAME" &
    else
        echo -e "${Y}  Quick tunnel — URL will appear below (changes on restart)${RST}"
        cloudflared tunnel --url "http://127.0.0.1:$HTTP_PORT" &
    fi

    local cf_pid=$!
    PIDS+=("$cf_pid")
    echo -e "${G}✓${RST} cloudflared tunnel started (PID $cf_pid)"
    echo ""
    echo -e "${Y}Ctrl+C to stop all three processes${RST}"
    echo ""

    wait
}

# ── Entry point ────────────────────────────────────────────────────────────────

case "${IS_PUBLIC,,}" in
    true|1|yes)
        run_production
        ;;
    false|0|no|"")
        run_local
        ;;
    *)
        die "IS_PUBLIC must be true or false (got: '${IS_PUBLIC}')"
        ;;
esac

build

That said, there isn't NO build stage, it is just a Quineic build stage. Out of scope to some extent for the time being, here is a ghost/artifact:

version: 2.0
# =============================================================================
# build.yaml - Hermitian DevOps;
# <https://github.com/Phovos/Morphological-Source-Code> • MSC: Morphological Source Code © 2026 by Phovos
#   Bijection harness outside C3 linearization MRO;
#   CICD, publishing, documentation, etc. harness
#   'conjugation [of verbs, nouns]' in RPN one(self)-to-many(readers)
#       fps: 'Future Participle Syntax'
#       rpn: 'Reverse Polish Notation'
#       msc: 'Morphological Source Code'
#       qsd: 'Quineic Statistical Dynamics'
# Cloud-Compute: None (all local via self-hosted agent)
# ┌─────────────────────────────────────────────────────────┐
# │  Git Cloud (Sync Signal)                                │
# │  ┌───────────────┐         ┌──────────────┐             │
# │  │ DevOps Flow   │──────》 │  OSS Bucket  │             │
# │  │ (build.yaml)  │         │ (checkpoints)│             │
# │  └───────┬───────┘         └──────────────┘             │
# │          │ HTTP POST                                    │
# └──────────┼──────────────────────────────────────────────┘
#            │ Sync Pulse (timestamp, build_id)
#            ▼ Lamport/GADT timestamp
# ┌────────────────────────────────────────────────────────┐
# │  Local Machine                                         │
# │  ┌─────────────────────────────────────────────────┐   │
# │  │ morphological_source_code.py (monolith)         │   │
# │  │                                                 │   │
# │  │  ┌──────────────────────────────────────────┐   │   │
# │  │  │ GitSyncServer (port 8282)                │   │   │
# │  │  │ - Receives sync pulses                   │   │   │
# │  │  │ - Updates LAST_SYNC_TIME                 │   │   │
# │  │  │ - Triggers checkpoints                   │   │   │
# │  │  └──────────────────────────────────────────┘   │   │
# │  │                                                 │   │
# │  │  ┌──────────────────────────────────────────┐   │   │
# │  │  │ HolographicBoundary (main runtime)       │   │   │
# │  │  │ - ByteWord algebra and Quine populations │   │   │
# │  │  │ - Multi-interpreter holography           │   │   │
# │  │  │ - All computation happens HERE           │   │   │
# │  │  └──────────────────────────────────────────┘   │   │
# │  │                                                 │   │
# │  └─────────────────────────────────────────────────┘   │
# └────────────────────────────────────────────────────────┘
# =============================================================================

# =============================================================================
# Notification Hooks (Optional)
# =============================================================================
notifications:
  - type: webhook
    on: success
    url: "${BUILD_ENDPOINT}/notifications/success"
  - type: webhook
    on: failure
    url: "${BUILD_ENDPOINT}/notifications/failure"
  - type: webhook
    on: always
    url: "${BUILD_ENDPOINT}/notifications/always"
  - type: githook
    on: success
    url: "${BUILD_ENDPOINT}/notifications/github/success"
  - type: fossilhook
    on: success
    url: "${BUILD_ENDPOINT}/notifications/fossil/success"
  - type: fossilhook
    on: failure
    url: "${BUILD_ENDPOINT}/notifications/fossil/failure"
  - type: fossilhook
    on: always
    url: "${BUILD_ENDPOINT}/notifications/fossil/always"

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions