Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Product: Cloud AI API Gateway for LLM Cost Optimization and Security Auditing. Architecture: 100% Cloud SaaS on Fly.io. No local stdio transport. No OS-level sandbox.
119 changes: 114 additions & 5 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,120 @@
# Phase 53 — production .dockerignore.
#
# Anything that is NOT required to run the Phase 53 multi-stage
# build (installer + builder + runner) is excluded so:
#
# 1. The build context stays small (faster `docker build`).
# 2. Source files / tests / docs CANNOT accidentally leak into
# the final runtime layer if a future Dockerfile change
# forgets a narrow COPY directive.
# 3. Real secrets (`.env`, `.env.production`) cannot leak into
# an image push.
#
# The runner stage in the Dockerfile uses narrow `COPY --from=builder`
# directives, which is the primary defence; this .dockerignore is
# defence-in-depth.

# ─────────────────────────────────────────────────────────────────────
# Build / dev artefacts
# ─────────────────────────────────────────────────────────────────────
node_modules
ui/node_modules
**/node_modules
dist
**/dist
ui/node_modules
ui/dist
.mcp-cache
**/.mcp-cache
coverage
**/coverage
.nyc_output
*.tsbuildinfo
**/*.tsbuildinfo

# ─────────────────────────────────────────────────────────────────────
# VCS, editor, OS metadata
# ─────────────────────────────────────────────────────────────────────
.git
.gitignore
.gitattributes
.github
.vscode
.idea
.cursorrules
.kiro
.antigravitycli
*.swp
*.swo
.DS_Store
Thumbs.db

# ─────────────────────────────────────────────────────────────────────
# Tests + test fixtures (NEVER ship to runtime)
# ─────────────────────────────────────────────────────────────────────
tests
**/tests
**/__tests__
**/*.test.ts
**/*.spec.ts
jest.config.js
jest.config.ts
.jest

# ─────────────────────────────────────────────────────────────────────
# Documentation + non-runtime markdown
# ─────────────────────────────────────────────────────────────────────
docs
*.md
!README.md
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
SECURITY.md
SUPPORT.md
BUSINESS_OPERATIONS.md

# ─────────────────────────────────────────────────────────────────────
# Examples, playgrounds, sample data
# ─────────────────────────────────────────────────────────────────────
examples
**/examples
models

# ─────────────────────────────────────────────────────────────────────
# Container / orchestration manifests (the build doesn't need them)
# ─────────────────────────────────────────────────────────────────────
Dockerfile
Dockerfile.*
docker-compose.yml
docker-compose.*.yml
compose.yml
compose.*.yml
.dockerignore

# ─────────────────────────────────────────────────────────────────────
# Logs / runtime spillover
# ─────────────────────────────────────────────────────────────────────
*.log
audit.log
compile_out.log
npm-debug.log*

# ─────────────────────────────────────────────────────────────────────
# Secrets — must never enter an image
# ─────────────────────────────────────────────────────────────────────
.env
.env.*
!.env.example

# ─────────────────────────────────────────────────────────────────────
# Monitoring / infra configs (mounted at runtime by compose, not
# baked into the image)
# ─────────────────────────────────────────────────────────────────────
monitoring

# ─────────────────────────────────────────────────────────────────────
# Misc dev tooling
# ─────────────────────────────────────────────────────────────────────
scripts
fly.toml
.mcp-cache
examples/.mcp-cache
examples/__pycache__
coverage
npm-debug.log
240 changes: 236 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,243 @@
PROXY_AUTH_TOKEN=12345678901234567890123456789012
# =============================================================================
# Toolwall .env.example
# =============================================================================
#
# !! SECURITY WARNING !!
#
# This file is COMMITTED TO VERSION CONTROL. NEVER place real production
# secrets, API keys, or tokens here. The values below are documentation
# placeholders only — they must be replaced before the gateway is run
# in any environment that handles real traffic.
#
# Required steps before running in production:
# 1. Copy this file to `.env` (which IS in .gitignore).
# 2. Replace every `CHANGEME_*` placeholder with a strong, unique value
# generated by your secret manager / CI vault.
# 3. NEVER commit the resulting `.env` to git.
# 4. Prefer injecting secrets via the environment (CI secret store,
# Docker/Kubernetes Secrets, AWS Secrets Manager, GCP Secret
# Manager, HashiCorp Vault) rather than a file on disk.
#
# Generate strong tokens with, e.g.:
# openssl rand -hex 32
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
#
# Tokens MUST be at least 32 bytes of entropy. The gateway will refuse
# any value shorter than that and will fail to start.
# =============================================================================

# --- Proxy authentication (required when running outside stdio mode) ---
# 32+ byte random hex string. Required by the HTTP /mcp endpoint.
PROXY_AUTH_TOKEN=<GENERATE_A_RANDOM_32_CHAR_TOKEN_HERE>

# --- Admin API authentication (required when MCP_ADMIN_ENABLED=true) ---
# 32+ byte random hex string. DIFFERENT from PROXY_AUTH_TOKEN.
ADMIN_TOKEN=<GENERATE_A_RANDOM_32_CHAR_TOKEN_HERE>

# --- Cache (L2 SQLite) ---
MCP_CACHE_DIR=.mcp-cache
MCP_CACHE_TTL_SECONDS=300

MCP_ADMIN_ENABLED=true
# --- PID lifecycle (embedded MCP server) ---
# Directory where the embedded standalone MCP server records its PID
# for local health-checks. Defaults to `<cwd>/.data` (which is in
# .gitignore). Override to point at a writable volume in container or
# Kubernetes deployments. Both absolute and relative paths are honoured;
# relative paths resolve against the current working directory.
MCP_GATEWAY_PID_DIR=.data

# --- Admin API ---
MCP_ADMIN_ENABLED=false
MCP_ADMIN_PORT=9090
MCP_ADMIN_CORS_ORIGIN=*
ADMIN_TOKEN=abcdefghijklmnopqrstuvwxyz123456
# CORS origin for the admin web UI. If unset, defaults strictly to http://127.0.0.1
# to prevent cross-origin resource leaks.
MCP_ADMIN_CORS_ORIGIN=http://127.0.0.1

# --- HTTP server ---
MCP_PORT=3000
MCP_SERVER_ID=default

# --- Phase 40: Global edge deployment ---
# Set to the Fly.io region code of the primary writer DB
# (e.g. "iad", "ams", "hkg"). The runtime stamps this onto every
# audit event when the request didn't carry a Fly-Region / X-Fly-Region
# header (single-region deployments, local dev). Multi-region Fly
# deployments override per-request via the edge header.
PRIMARY_REGION=iad

# --- Phase 40: Postgres read-replica routing ---
# DATABASE_URL points at the NEAREST replica (or the only DB in
# single-region deployments). Used for read-heavy paths: L2 cache
# `get`, semantic-cache ANN lookup, dashboard time-series.
DATABASE_URL=postgres://<DB_USER>:<DB_PASSWORD>@localhost:5432/<DB_NAME>
# MASTER_DATABASE_URL points at the PRIMARY writer endpoint. All
# writes, transactions, and the auth-path `isTenantActive` read route
# here explicitly so a replica-lag window cannot let a just-revoked
# tenant authenticate. Leave UNSET for single-region deployments —
# the writer transparently falls back to DATABASE_URL.
# MASTER_DATABASE_URL=postgres://<DB_USER>:<DB_PASSWORD>@primary.example/<DB_NAME>

# Pool sizing — separate caps for writer and reader so chatty
# dashboard reads can't starve the auth path on the writer.
PGPOOL_WRITER_MAX=10
PGPOOL_READER_MAX=10

# --- vNext: Postgres TLS certificate verification (SECURITY_AUDIT F-01) ---
#
# Production NEVER uses an unverified TLS connection. The resolver in
# src/database/postgres-pool.ts (resolvePostgresTls) enforces:
# - NODE_ENV=production + non-local DB -> TLS REQUIRED, certificate
# VERIFIED (rejectUnauthorized:true). Boot fails closed on
# sslmode=disable or PG_TLS_INSECURE.
# - dev/test + localhost -> no TLS (the common
# docker-compose / CI case) unless a TLS flag is set.
#
# CA configuration (optional — falls back to the Node/system CA store
# with verification still ON):
# PG_CA_CERT — inline PEM of the provider/self-managed root CA.
# PGSSLROOTCERT — filesystem path to the root CA PEM (inline wins).
# Neon / Supabase / Fly managed Postgres are verifiable against the
# system CA store, so most operators leave both unset.
# PG_CA_CERT="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
# PGSSLROOTCERT=/etc/ssl/certs/pg-root-ca.pem
#
# PG_FORCE_TLS=true forces verified TLS even when the URL doesn't
# advertise sslmode=require (e.g. a managed host without the param).
# PG_FORCE_TLS=true
#
# PG_TLS_INSECURE=true is a DEV-ONLY escape hatch for self-signed
# local proxies. It is REJECTED at boot in production. Do not set it
# in any environment that handles real traffic.
# PG_TLS_INSECURE=false

# --- vNext: Reverse-proxy / client-IP trust (SECURITY_AUDIT F-02) ---
#
# MCP_TRUST_PROXY maps to Express `trust proxy`. Behind Fly/edge/LB the
# gateway must know the proxy topology so req.ip is the real client IP
# (correct rate-limit attribution + audit + color-boundary isolation).
#
# REQUIRED in production — boot FAILS LOUD if unset (we refuse to guess).
# Accepted values:
# "1" -> trust 1 proxy hop (typical single Fly edge).
# "2" -> two hops (edge + internal LB), etc.
# "false" -> direct bind, no proxy (req.ip = socket peer).
# "loopback" -> trust only 127.0.0.1/::1.
# "10.0.0.0/8,127.0.0.1" -> explicit trusted proxy IP/CIDR allowlist.
# "true" -> trust ALL proxies. REJECTED in production
# (enables X-Forwarded-For spoofing).
# For Fly.io single-region behind the edge, "1" is the safe default.
MCP_TRUST_PROXY=1

# --- Phase 47: Production-tuned pool timeouts for high-throughput
# PGBouncer (transaction pooling) deployments. ---
#
# All four are env-driven so an operator running against a beefy
# single-tenant Postgres can relax them without a rebuild.
#
# PGPOOL_STATEMENT_TIMEOUT_MS — server-side hard cap on any
# single query. Postgres KILLS queries that exceed this.
# Default: 5000 (5 s).
# PGPOOL_QUERY_TIMEOUT_MS — client-side timeout. node-postgres
# cancels and rejects the awaiting promise after this many
# ms. Set HIGHER than statement_timeout so the server's
# server-side kill is the primary mechanism. Default: 10000
# (10 s).
# PGPOOL_CONNECT_TIMEOUT_MS — how long Pool.connect() waits
# for an available client. Short so a saturated DB fails
# fast (request returns 503) rather than queueing. Default:
# 2000 (2 s).
# PGPOOL_IDLE_TIMEOUT_MS — how long a checked-in client can
# sit idle before being closed. Default: 30000 (30 s).
PGPOOL_STATEMENT_TIMEOUT_MS=5000
PGPOOL_QUERY_TIMEOUT_MS=10000
PGPOOL_CONNECT_TIMEOUT_MS=2000
PGPOOL_IDLE_TIMEOUT_MS=30000

# --- Phase 47: cross-region LISTEN/NOTIFY adapter ---
#
# LISTENER_DATABASE_URL points the policy-notify adapter at a
# DIRECT Postgres connection that bypasses any PGBouncer
# transaction-mode pooler in front of the writer. PGBouncer's
# transaction pooling is incompatible with persistent LISTEN —
# the bouncer multiplexes the upstream session across clients
# between transactions, silently dropping notifications. When
# unset, the adapter falls back to MASTER_DATABASE_URL /
# DATABASE_URL.
# LISTENER_DATABASE_URL=postgres://<DB_USER>:<DB_PASSWORD>@primary.example:5432/<DB_NAME>

# Keepalive cadence for the LISTEN client. PGBouncer and many
# managed-Postgres firewalls reap connections that go idle for
# too long; the adapter fires `SELECT 1` at this cadence so the
# connection's TCP-level activity counter never lets the
# firewall mark it idle. Set to 0 to disable (use only when the
# upstream tolerates idle LISTEN connections).
MCP_LISTENER_KEEPALIVE_MS=30000

# --- Phase 56: AI-based jailbreak / prompt-injection detection ---
#
# Disabled by default. When enabled, every `tools/call` whose
# arguments contain user-supplied text is checked against an
# external classifier (Llama Guard sidecar, NeMo Guardrails,
# OpenAI moderation, self-hosted PromptGuard, etc.) BEFORE the
# request reaches the upstream provider.
#
# FAIL-CLOSED contract:
#
# - Disabled (`MCP_AI_SECURITY_ENABLED=false`) → guard is a
# no-op; zero overhead, zero risk.
# - Enabled + classifier reachable → POST aggregated text,
# accept verdict `{safe: boolean, ...}`. Unsafe → 403
# `J_B_BLOCKED`; safe → proceed.
# - Enabled + classifier UNREACHABLE (timeout, 5xx, network
# drop, malformed response, no URL configured) → 503
# `JAILBREAK_CLASSIFIER_FAILED`. The gateway refuses traffic
# during a classifier outage rather than silently bypassing
# the guard. Operators who require the looser fail-open
# posture leave `MCP_AI_SECURITY_ENABLED=false`.
#
# Aggregated input: the entire request payload is recursively
# walked and every string fragment is joined into one
# newline-delimited block (`extractAllStrings` in
# src/middleware/ai-security-guard.ts). Capped at
# `MCP_SECURITY_CLASSIFIER_MAX_BYTES` (default 64 KB) so a giant
# payload cannot turn the classifier call into a bandwidth
# amplifier.
#
# Strict timeout: enforced via `AbortController` against the
# value below (default 800 ms).
#
# Positive detection emits `JAILBREAK_DETECTED` audit; outage
# emits `AI_SECURITY_CHECK_FAILED`; missing classifier with the
# flag on emits `AI_SECURITY_NOT_CONFIGURED`.
MCP_AI_SECURITY_ENABLED=false
MCP_SECURITY_CLASSIFIER_URL=
MCP_SECURITY_CLASSIFIER_TIMEOUT_MS=800
# MCP_SECURITY_CLASSIFIER_MAX_BYTES=65536

# MCP_WEBHOOK_URL=https://your-siem.example.com/ingest
# MCP_WEBHOOK_AUTH_TOKEN=<GENERATE_A_RANDOM_32_CHAR_TOKEN_HERE>

# --- Phase 43: Prometheus metrics scrape endpoint ---
# 32+ byte random hex token. The gateway exposes
# `GET /metrics` on the main app port and gates it with this
# bearer token (Authorization: Bearer <token>). Without the token
# configured the endpoint fails closed with 503 — we never expose
# raw metrics to anyone who can reach the port. Configure the same
# value in Fly's [metrics] block in fly.toml so the built-in
# Prometheus scraper can poll the endpoint.
PROMETHEUS_SCRAPE_TOKEN=<GENERATE_A_RANDOM_32_CHAR_TOKEN_HERE>

# --- Phase 41/42: Read-your-writes consistency guard ---
# Internal-only shared secret. When a request carries
# `X-Force-Master: true` AND `X-Internal-Secret: <this value>`,
# the consistency middleware pins the request to the writer pool
# regardless of operation type — used by post-revocation re-auth
# probes, just-issued-key smoke checks, and the billing sync
# worker's reconciliation reads. Public clients without the secret
# get the standard replica-routed behaviour. 32+ byte random hex.
# INTERNAL_FORCE_MASTER_SECRET=<GENERATE_A_RANDOM_32_CHAR_TOKEN_HERE>

# Phase 38: legacy LemonSqueezy local-license validation has been
# removed. Commercial enforcement is now exclusively the Stripe
# webhook + SQLite key-registry pipeline (see STRIPE_SECRET_KEY,
# STRIPE_PRICE_PRO, BILLING_WEBHOOK_SECRET above).
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 5

- package-ecosystem: "cargo"
directory: "/src-tauri"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
Loading
Loading