public-ingress
$
npx mdskill add vellum-ai/vellum-assistant/public-ingressYou are setting up and managing a public ingress tunnel so that external services (Telegram webhooks, OAuth callbacks, etc.) can reach the local Vellum gateway. This skill uses ngrok to create a secure tunnel and persists the public URL as `ingress.publicBaseUrl`.
SKILL.md
.github/skills/public-ingressView on GitHub ↗
---
name: public-ingress
description: Set up and manage ngrok-based public ingress for local assistants; do not use this in managed mode when platform callback routing is available
compatibility: "Designed for Vellum personal assistants"
metadata:
emoji: "🌍"
vellum:
category: "system"
display-name: "Public Ingress"
activation-hints:
- "Local assistant needs a public webhook or OAuth callback URL"
- "ngrok tunnel setup for a non-managed assistant"
avoid-when:
- "Running in a platform-managed assistant with platform callback routing available"
---
You are setting up and managing a public ingress tunnel so that external services (Telegram webhooks, OAuth callbacks, etc.) can reach the local Vellum gateway. This skill uses ngrok to create a secure tunnel and persists the public URL as `ingress.publicBaseUrl`.
If managed platform callback routing is available, stop and do not continue with ngrok. In platform-managed deployments, Telegram/Twilio/OAuth callback routing should use the platform callback route flow instead of local public ingress.
## Overview
The Vellum gateway listens locally and needs a publicly reachable URL for:
- Telegram webhook delivery
- Google/Slack OAuth redirect callbacks
- Any other inbound webhook traffic
This skill installs ngrok, configures authentication, starts a tunnel, discovers the public URL, and saves it to the assistant's ingress config.
## Step 0: Reject Managed Callback Environments
Check whether managed platform callback routing is available:
```bash
assistant platform status --json
```
If the result shows `isPlatform: true` and `available: true`, stop here. Tell the user that this assistant should use the platform callback route flow instead of ngrok, and do not install or start ngrok.
## Step 1: Check Current Ingress Status
First, check whether ingress is already configured:
```bash
assistant config get ingress.publicBaseUrl
assistant config get ingress.enabled
```
The local gateway URL is available as the `$INTERNAL_GATEWAY_BASE_URL` environment variable (defaults to `http://127.0.0.1:7830`).
The commands return:
- `ingress.publicBaseUrl` - currently configured public ingress URL (if any)
- `ingress.enabled` - whether ingress is enabled
If `publicBaseUrl` is already set and the tunnel is running (check via `curl -s http://127.0.0.1:4040/api/tunnels`), tell the user the current status and ask if they want to reconfigure or if this is sufficient.
## Step 2: Install ngrok
Check if ngrok is installed:
```bash
ngrok version
```
If not installed, install it:
**macOS (Homebrew):**
```bash
brew install ngrok/ngrok/ngrok
```
**Linux (snap):**
```bash
sudo snap install ngrok
```
**Linux (apt - alternative):**
```bash
curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update && sudo apt install ngrok
```
After installation, verify with `ngrok version`.
## Step 3: Authenticate ngrok
Check if ngrok already has an auth token configured:
```bash
ngrok config check
```
If not authenticated:
1. Tell the user: "You need an ngrok account to create tunnels. If you don't have one, sign up at https://dashboard.ngrok.com/signup - it's free."
2. Once they have an account, use `credential_store` to securely collect their auth token. **Never ask the user to paste the token directly in chat.**
Use `credential_store` with:
- action: `prompt`
- service: `ngrok`
- field: `authtoken`
- label: `ngrok Auth Token`
- description: `Get your auth token from https://dashboard.ngrok.com/get-started/your-authtoken`
- usage_description: `ngrok authentication token for creating public tunnels`
3. Once the credential is stored, retrieve it via `credential_store` and apply it to ngrok:
```bash
credential_store action=get service=ngrok field=authtoken
ngrok config add-authtoken "<authtoken_from_credential_store>"
```
If no value is returned, re-run `credential_store` with `action: "prompt"` and try again.
Verify authentication succeeded by checking `ngrok config check` again.
## Step 4: Start the Tunnel
Before starting, check for an existing ngrok process to avoid duplicates:
```bash
curl -s http://127.0.0.1:4040/api/tunnels 2>/dev/null
```
If a tunnel is already running, check whether it points to the correct local target. If so, skip to Step 5. If it points elsewhere, stop it first:
```bash
pkill -f ngrok || true
sleep 1
```
Start ngrok in the background tunneling to the local gateway URL:
```bash
nohup ngrok http "$INTERNAL_GATEWAY_BASE_URL" --log=stdout > /tmp/ngrok.log 2>&1 &
echo $! > /tmp/ngrok.pid
```
Wait a few seconds for the tunnel to establish:
```bash
sleep 3
```
## Step 4b: Verify Port Alignment
Before discovering the public URL, verify that ngrok is forwarding to the same port the gateway is actually listening on. A mismatch here causes silent failures - webhooks appear to be delivered but never reach the gateway.
Query the ngrok tunnel's target port and the gateway's configured port, then compare them:
```bash
curl -s http://127.0.0.1:4040/api/tunnels | python3 -c "
import sys, json, re
data = json.load(sys.stdin)
tunnels = data.get('tunnels', [])
if not tunnels:
print('ERROR: no active ngrok tunnel found')
sys.exit(1)
addr = tunnels[0].get('config', {}).get('addr', '')
match = re.search(r':(\d+)$', addr)
if not match:
print(f'ERROR: could not extract port from ngrok tunnel addr: {addr}')
sys.exit(1)
print(match.group(1))
"
```
```bash
echo "$INTERNAL_GATEWAY_BASE_URL" | grep -oE '[0-9]+$'
```
Compare the two port numbers. If they differ, warn the user:
> **Port mismatch detected:** ngrok is forwarding to port **X** but the gateway is listening on port **Y**. Webhooks will not reach the gateway. Stop ngrok (`pkill -f ngrok`), then re-run this skill to start ngrok on the correct port.
If the ports match, proceed silently to Step 5.
## Step 5: Discover the Public URL
Query the ngrok local API for the tunnel's public URL:
```bash
curl -s http://127.0.0.1:4040/api/tunnels | python3 -c "
import sys, json
data = json.load(sys.stdin)
tunnels = data.get('tunnels', [])
for t in tunnels:
url = t.get('public_url', '')
if url.startswith('https://'):
print(url)
sys.exit(0)
for t in tunnels:
url = t.get('public_url', '')
if url:
print(url)
sys.exit(0)
print('ERROR: no tunnel found')
sys.exit(1)
"
```
If no tunnel is found, check `/tmp/ngrok.log` for errors and report them to the user.
## Step 6: Persist the Ingress Setting
Save the discovered public URL and enable ingress:
```bash
assistant config set ingress.publicBaseUrl "<public-url>"
assistant config set ingress.enabled true
```
Verify it was saved:
```bash
assistant config get ingress.publicBaseUrl
assistant config get ingress.enabled
```
## Step 7: Report Completion
Summarize the setup:
- **Public URL:** `<the-url>` (this is your `ingress.publicBaseUrl`)
- **Local gateway target:** `$INTERNAL_GATEWAY_BASE_URL`
- **ngrok dashboard:** http://127.0.0.1:4040
Provide useful follow-up commands:
- **Check tunnel status:** `curl -s http://127.0.0.1:4040/api/tunnels | python3 -c "import sys,json; [print(t['public_url']) for t in json.load(sys.stdin)['tunnels']]"`
- **View ngrok logs:** `cat /tmp/ngrok.log`
- **Restart tunnel:** `pkill -f ngrok; sleep 1; nohup ngrok http "$INTERNAL_GATEWAY_BASE_URL" --log=stdout > /tmp/ngrok.log 2>&1 &`
- **Stop tunnel:** `pkill -f ngrok`
- **Rotate URL:** Stop and restart ngrok (free tier assigns a new URL each time; update `ingress.publicBaseUrl` afterward)
**Important:** On ngrok's free tier, the public URL changes every time the tunnel restarts. After restarting, re-run this skill or manually update `ingress.publicBaseUrl` and any registered webhooks (e.g., Telegram).
## Troubleshooting
### ngrok not installed
Run the install commands in Step 2. On macOS, make sure Homebrew is installed first (`brew --version`).
### Auth token invalid or expired
Sign in to https://dashboard.ngrok.com, copy a fresh token from the "Your Authtoken" page, and re-run Step 3.
### ngrok API (port 4040) not responding
The ngrok process may not be running. Check with `ps aux | grep ngrok`. If not running, start it per Step 4. If running but 4040 is unresponsive, check `/tmp/ngrok.log` for errors.
### Gateway not reachable on local target
Re-check the local gateway target with `echo $INTERNAL_GATEWAY_BASE_URL`. Run `curl -s "$INTERNAL_GATEWAY_BASE_URL/healthz"` to verify it is reachable. If the gateway is not running, start the assistant first.
### "Too many connections" or tunnel limit errors
ngrok's free tier allows one tunnel at a time. Stop any other ngrok tunnels before starting a new one.
### ngrok port doesn't match gateway port
**Symptom:** Webhooks return connection refused or timeouts even though both ngrok and the gateway appear to be running.
**Cause:** ngrok is forwarding to a different port than the gateway is listening on. This can happen if the gateway port was changed after ngrok was started, or if ngrok was started manually with a hardcoded port.
**Fix:** Stop ngrok (`pkill -f ngrok`), verify the gateway URL with `echo $INTERNAL_GATEWAY_BASE_URL`, then re-run this skill to start ngrok on the correct port.
### ngrok automatically restarts with wrong port
If after killing the ngrok process, it automatically re-spawns and is still attached to the incorrect port, check to see if there is a launch agent process configured to auto-restart it. This might exist at `~/Library/LaunchAgents/com.ngrok.tunnel.plist`. If so, it needs to be either removed or updated.