solar-gateway
HTTP webhook and WebSocket entry point for external integrations (n8n, Telegram). Delegates to solar-router. Not Solar App UI (`solar-app` on :9000).
SKILL.mdcore/skills/solar-gateway/SKILL.md
GitHubSolar Gateway
Purpose
Provide a reusable local transport layer for Solar:
- receive inbound messages over WebSocket,
- process request/response loop in one place,
- keep channel adapters decoupled from runtime transport,
- manage Telegram webhook operations from this same skill.
Scope
- Run local WebSocket server for bidirectional messaging.
- Run local HTTP webhook bridge with channel routes (
/webhook/<channel>). - Define stable message contract for channel adapters.
- Keep implementation lightweight and deterministic.
Required MCP
None
Dependencies
- solar-router: This skill depends on
solar-routerfor AI provider execution. Ensuresolar-routeris configured first:bash core/skills/solar-router/scripts/onboard_router_env.sh bash core/skills/solar-router/scripts/diagnose_router.sh
Validation commands
# One-command setup (recommended)
bash core/skills/solar-gateway/scripts/setup_transport_gateway.sh
# Restart after .env changes (stop owned runtime + start; runs preflight first)
bash core/skills/solar-gateway/scripts/setup_transport_gateway.sh --restart
# Dry-run stop (ownership report; no kill)
bash core/skills/solar-gateway/scripts/stop_transport_gateway.sh --dry-run
# Stop owned processes (--force kills foreign blockers; --tunnel-only skips bridges)
bash core/skills/solar-gateway/scripts/stop_transport_gateway.sh
bash core/skills/solar-gateway/scripts/stop_transport_gateway.sh --tunnel-only
# Validate skill quality and structure
python3 core/skills/solar-skill-creator/scripts/package_skill.py core/skills/solar-gateway /tmp
# Bootstrap .env block for this skill
bash core/skills/solar-gateway/scripts/onboard_websocket_env.sh
# Validate runtime prerequisites
bash core/skills/solar-gateway/scripts/validate_websocket_bridge.sh
# Preflight AI providers
bash core/skills/solar-router/scripts/diagnose_router.sh --dry-run
bash core/skills/solar-router/scripts/diagnose_router.sh
bash core/skills/solar-router/scripts/list_supported_providers.sh
# Check runtime health — local HTTP + named-tunnel connector /ready (exit 0/1/2).
# Public hostname curl from the origin host is informational (hairpin).
bash core/skills/solar-gateway/scripts/check_transport_gateway.sh
# Ensure gateway is healthy, recover if not (used by solar-system orchestrator)
# Drift-first: env stamp mismatch → preflight → setup --restart
# Partial without drift → tunnel-only; partial with drift → full restart
bash core/skills/solar-gateway/scripts/ensure_transport_gateway.sh
# Smoke: priority change → ensure → new process env (restores .env afterward)
bash core/tests/skills/solar-gateway/smoke_priority_ensure.sh
# Unit lifecycle tests (OWNER, fingerprint, preflight, rollback)
bash core/tests/skills/solar-gateway/test_gateway_lifecycle.sh
# n8n auth / async poll tests
uv run --project core/tests python -m pytest core/tests/skills/solar-gateway/test_http_n8n_auth.py -q
# Register and verify Telegram webhook (OWNER=solar only; fails if OWNER=external)
bash core/skills/solar-gateway/scripts/set_telegram_webhook.sh
bash core/skills/solar-gateway/scripts/verify_telegram_webhook.sh
# Configure stable named tunnel (recommended for production)
bash core/skills/solar-gateway/scripts/configure_named_tunnel.sh
# Direct runtime check without local .venv
uv run --with websockets==12.0 python3 -c "import websockets; print(websockets.__version__)"
# Sync core changes to local clients
solar client sync
Lifecycle (stop / setup / ensure)
- No listener reuse: setup fails if WS/HTTP ports are busy unless
--restart. - Stop is the only kill path: ownership via bridge cmdline signatures + port/pid file; tunnel via
cloudflared.pid+ cmdline (no global cloudflared scan). - Env stamp lives at
$SOLAR_WORKSPACE/<runtime root>/gateway/env.stamp(fingerprint of an allowlisted key set — not.envmtime). Missing stamp with live Solar bridges counts as drift. - Fingerprint includes
SOLAR_GATEWAY_CLAIM_TELEGRAMand a derivedSOLAR_N8N_WEBHOOK_SECRET_SHA256(never the secret in plaintext). Rotating the secret or changing the claim flag triggers drift →ensurerestart. - HTTP channels (
SOLAR_HTTP_WEBHOOK_BASE/<channel>):n8nalways;telegramwhenTELEGRAM_BOT_TOKENis set. Distinct from Bot API claim. - Telegram claim (
SOLAR_GATEWAY_CLAIM_TELEGRAM=true|false, absent = do not claim):falseor absent: setup/ensure skipsetWebhook(exit 0). OutboundsendMessageis still allowed.true: register Solar's URL only whengetWebhookInfois empty or already Solar. A foreign URL is never claimed; setup continues (no rollback).- No
SOLAR_TELEGRAM_WEBHOOK/OWNERfallback.
- n8n auth (
SOLAR_N8N_WEBHOOK_SECRET): required.POST /webhook/n8nusesAuthorization: Bearer. Unset secret → fail-closed401. Missing header →401; wrong token →403(constant-time compare). Generate withopenssl rand -base64 32. HTTP 202 /GET /webhook/n8n/resultare not part of the production contract. - Drift / restart runs a non-destructive preflight before stopping a healthy runtime. Preflight failure writes
env.failand leaves processes running. Provider tokens are validated against solar-routerPROVIDERS(vialist_supported_providers.sh), not a duplicated list. InvalidSOLAR_GATEWAY_CLAIM_TELEGRAM(nottrue|falseor absent) fails preflight. - Backoff: repeated failures with the same fingerprint are throttled via
env.fail(exponential, capped atGATEWAY_BACKOFF_CAP_SEC, default 15 min). Configuration failures (preflight_failed*,setup_failed,stop_failed,ports_busy_after_stop) count towardGATEWAY_FAIL_ATTEMPTS_CAP(default 5). After that many with the same fingerprint, ensure stops retrying until the fingerprint changes (fix.envor removeenv.fail). A fingerprint change resets that backoff.tunnel_recovery_faileddoes not count toward the cap and never takes that hard stop: it only uses the capped backoff. An olderenv.failalready exhausted with that reason is retried on the next ensure when preflight passes, without deleting the file by hand. - Tunnel grace: while
cloudflaredis still running and only the connector is down, ensure leaves it to reconnect forGATEWAY_TUNNEL_RESTART_GRACE_SEC(default 300 s) before restarting it. A healthy check clears that clock, so the next outage of the same process gets a fresh grace. - mkdir-lock (portable, no
flock) at<runtime root>/gateway/lock/serializes ensure/setup/stamp writes. Distinct from the solar-system orchestrator lock. Dead or recycled lock PIDs are reclaimed.
Runtime requirements
uv- Python dependency resolved at runtime by
uv:websockets==12.0 - At least one AI client CLI in
PATH:codex,claude,agy, oragent
- Local runtime write access for conversation memory (default:
<runtime root>/router/)
System activation (via solar-system)
For host-level orchestration through one LaunchAgent, enable this feature in:
# [solar-system] required environment
SOLAR_SYSTEM_FEATURES=transport-gateway
Telegram / n8n gateway (examples):
# n8n owns the Telegram webhook (this host). Absent or false: do not claim.
# SOLAR_GATEWAY_CLAIM_TELEGRAM=false
SOLAR_N8N_WEBHOOK_SECRET=<openssl rand -base64 32>
# Solar registers /webhook/telegram only when getWebhookInfo is empty or already Solar:
# SOLAR_GATEWAY_CLAIM_TELEGRAM=true
Or combined with async tasks:
SOLAR_SYSTEM_FEATURES=async-tasks,transport-gateway
Then install/update Solar LaunchAgent:
bash core/skills/solar-system/scripts/install_launchagent_macos.sh
Laptop runtime note (optional)
- This skill can expose long-running local runtime endpoints (webhook/bridge/server/tunnel).
- If the active host is a laptop, host sleep can stop the runtime and break reachability.
- This is a host operations concern, not a mandatory dependency of the skill.
- If multiple laptops are used, only one active host should serve the same public webhook route at a time.
Workflow
- Run
setup_transport_gateway.shas default end-to-end flow (writesenv.stampon success; printshttp_channels=andtelegram_claim=). - After editing watched
.envkeys (includingSOLAR_GATEWAY_CLAIM_TELEGRAMor n8n secret), either wait for the nextensure_transport_gateway.shtick or runsetup_transport_gateway.sh --restart. - Preview stop candidates with
stop_transport_gateway.sh --dry-runbefore a manual restart. - If needed, run
setup_transport_gateway.sh --prepare-onlyto stop before long-running services. - For stable DNS, configure named tunnel with
configure_named_tunnel.shand setSOLAR_TUNNEL_MODE=named. - With
SOLAR_GATEWAY_CLAIM_TELEGRAMabsent/false(or a foreign URL already present), setup does not register the Telegram webhook. - All AI execution and routing policy is delegated to solar-router (
core/skills/solar-router/scripts/run_router.py). This skill does not select providers or implement fallback. - Use individual scripts only for troubleshooting or partial reconfiguration.
Dependency policy
- This skill must not create or rely on an in-repo
.venv. - Runtime Python dependencies are executed directly with
uv run --with .... - Install
uvonce on the host, for examplebrew install uv.
Conversation continuity
Managed entirely by solar-router. See skill solar-router for details.
Message contract (v3)
This skill is a pure delegate to solar-router. No provider selection, no fallback, no async policy here.
Inbound request (WS bridge):
type:requestrequest_id: unique id (channel:conversation_id:message_idwhen composed by the HTTP bridge)session_id: continuity key (channel:conversation_id); the router keys history by it, falling back touser_idonly when emptyuser_id: user identifiertext: user messagechannel:telegram|n8n|async-task|other(set by HTTP bridge before forwarding)mode:auto|direct_only|async_only(set by HTTP bridge based on caller)provider: optional — if set, strict mode in router (no fallback)
Outbound response (WS bridge — router v3 JSON + envelope):
type:responserequest_id: mirrors inbound idstatus:success|failedprovider_used: provider that respondedreply_text: generated reply textdecision.kind:direct_reply|async_draft_created|async_activation_needed|async_draft_proposaldecision.task_id: task id if async draft was createderror_code: optional, for consumer routingerror: human-readable error detail
HTTP bridge channel mapping:
- Telegram inbound →
channel=telegram,mode=auto(only when Solar registered the webhook) - n8n inbound →
channel=n8n,mode=auto; requireAuthorization: Bearer(SOLAR_N8N_WEBHOOK_SECRET). Unset secret → fail-closed401 - n8n message identity: n8n sends
channel(platform, e.g.telegram),conversation_idandmessage_id; the bridge composessession_idandrequest_idand never splits them. Partial parts, a malformedchannelor a conflictingsession_idare rejected before the replay ledger and not stored. The legacy body (request_id,session_id, optionalchat_id) is still accepted. Seereferences/message-contract.md - Telegram direct inbound composes
request_id = telegram:<chat>:<message_id> - n8n production contract: one synchronous
POST /webhook/n8n. HTTP 202,SOLAR_N8N_DEFAULT_ASYNC, andGET /webhook/n8n/resultreturnstatus: failed(no poll) - n8n response: router v3 JSON exposed directly (no legacy double-wrapper). Replay uses
$SOLAR_GATEWAY_RUN_DIR/n8n-jobs/
References
references/message-contract.md- Routing policy:
core/skills/solar-router/references/routing-policy.md references/telegram-webhook-flow.md