portal-expose
Expose, preview, protect with x402 payments, or keep a local web app, static site, HTTP route set, or explicitly requested TCP/UDP service reachable through Portal, then verify the public endpoint and report its lifecycle. Use when the user asks to deploy, publish, share, tunnel,
Install
npx skills add https://github.com/gosuda/portal-tunnel/tree/main/plugins/portal-deploy/skills/portal-expose
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install gosuda-portal-tunnel@llmmart
git clone https://github.com/gosuda/portal-tunnel.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole gosuda/portal-tunnel collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Expose an App with Portal
Portal publishes a service that is already running on the user's machine. It does not build the app or move it to a cloud host. Treat a successful tunnel as dependent on both the local app and the Portal process or agent remaining available. This is the app side of Portal: every relay website offers the same three steps (paste what to share, run one install-and-expose command built from the app settings, open the public URL), and this skill performs those steps for the app with verification around them. Any other program that reaches the published URL needs nothing from Portal.
Run the workflow in order. Open a reference only when that branch is taken: references/x402.md for a paid route, references/safety-and-verification.md for an authenticated, sensitive/high-risk, or non-HTTP service, references/game-hosting.md for a game server, references/portal-cli.md when choosing persistent-agent configuration or checking a flag, references/sdk-embed.md only for a Go app that should run the tunnel inside its own process. Use the installed portal CLI for flags.
Choose the Mode
Use the smallest mode that satisfies the request:
- Temporary web preview:
portal expose <target>. - Connect the app to a relay the user picked, as its website's quick start does: install Portal from the official GitHub installer if it is missing, then
portal expose <target> --name <name> --relays <relay-url>, with--discovery=falsewhen only that relay should carry the app. - Trusted static directory or HTML entry:
portal expose --serve <path>. - Multiple local HTTP services under one URL: repeat
--http-route. - Paid HTTP path: routed HTTP with an explicit x402 payment contract; never enable payment implicitly.
- Durable tunnel that should survive terminal or login restarts: an explicit
portal agentconfig and managed service. - Session-owned durable tunnel without an OS service:
portal agent run --foreground. - Game server (Minecraft, Terraria, Palworld, or any dedicated game server): always start from
references/game-hosting.md— raw TCP/UDP transport has different prerequisites and verification than HTTP. - Go app that should carry the tunnel in-process instead of running the CLI next to it:
references/sdk-embed.md. The CLI stays the default even for Go; embedding is for users who ask for it.
Default to a temporary preview when the user says only "share", "preview", or "deploy locally". Do not install an OS service unless the user asks for a persistent, managed, or restart-surviving tunnel and accepts that portal agent run without --foreground installs a per-user launchd or systemd unit.
Workflow
1. Inspect the Project
- Read the applicable repository instructions before running or changing anything.
- Determine the app directory, start command, expected protocol, loopback target, and a meaningful health path.
- Prefer declared scripts and documented ports over guessing from process lists.
- Do not expose a port merely because it is listening. Tie it to the requested app.
- If the project is already running, preserve its process. If it is not running and deployment was requested, start it with the project's normal command and retain the terminal/session handle.
Ask one concise question only when the target, desired lifetime, or transport cannot be discovered safely. An explicit request to deploy, publish, expose, tunnel, or share authorizes creating the public tunnel for the named app; it does not authorize exposing adjacent services.
For x402, do not guess the protected path, payment methods, amount, network, recipient, or network-specific asset. Collect any missing consequential value before building the command or config. Treat an omitted method list as charging every method on the route and confirm that scope when it was not explicit.
2. Verify the Local Service
- Wait for the app's real readiness signal, not only for the process to exist.
- Make a bounded local request to the selected target. For HTTP, record the URL and status. For TCP/UDP, use a protocol-appropriate check that does not mutate application data.
- Stop before opening a tunnel if the local health check fails.
- Warn and require explicit direction before exposing databases, container daemons, debug consoles, unauthenticated admin panels, or services containing sensitive data.
- Before opening the tunnel, say that the public hostname is enumerable through each participating relay's
GET /api/stateunless the user asked for--hide, and that relays which enable their directory page show it there too.
3. Check Portal
- Run
portal versionwhenportalis available. - If Portal is missing, present the official GitHub installer and request approval before running it because installation writes outside the project. A relay also serves an installer at
<relay>/api/install.sh(install.ps1on Windows), but the relay then supplies the script, the binary, and the checksum together, so nothing in that download is verified independently of the relay. Use it only when the user explicitly prefers it, and before running the installed binary compare its SHA-256 with thechecksums.txtor.sha256asset of the matching GitHub release, fetched from github.com rather than from the relay. Never run an installer from a relay the user did not choose or from a third-party URL. - Do not assume a hard-coded latest release or stale flags. Use the installed Portal version as the compatibility baseline.
4. Build the Command or Agent Config
- Use loopback targets such as
127.0.0.1:<port>unless the project explicitly needs another address. - Use the user's requested name. Otherwise derive a DNS-label-safe name from the app and pass it explicitly, so the public URL
https://<name>.<relay-host>/is known before the tunnel starts; the relay website always emits--namefor the same reason.--nameapplies only when the identity file is created. An existing--identity-pathkeeps its saved name and silently ignores the flag, so use one identity file per public name. - For
portal expose, always pass an absolute--identity-pathoutside the repository. The CLI default isidentity.jsonin the process working directory and that file contains private key material. Forportal agent, omitidentity_pathso the agent stores identity under its state directory; if you set the field, use an absolute path outside the repository. - Never print or commit identity JSON, control tokens, facilitator tokens, or wallet secrets.
- With a user-selected relay on
portal expose, pass--relays <https-url> --discovery=false. In persistent mode those flags are not accepted onportal agent run; putrelays = ["https://..."]anddiscovery = falseon the[[tunnels]]entry instead. - Map the app's settings onto the command the way the relay quick-start form does: a port or
host:portis the positional target; anhttp(s)://URL contributes its host and port; a directory or HTML file becomes--serve <path>; a thumbnail URL, description, tags, or owner that the app or user defines go to--thumbnail,--description,--tags,--owner, all of which are public metadata; a datagram service adds--udp, with--udp-addrwhen the UDP port differs from the target. - The MITM self-probe runs against every relay whose tenant TLS stack exports keying material. Without
--ban-mitm/ban_mitm = true, a suspected TLS termination is only logged and the tunnel keeps serving; do not claim the default path blocks a relay. Add--ban-mitmonly when the user wants fail-closed handling; it then refuses relays that cannot export keying material instead of serving unprotected.--cacheopts out of the probe entirely because it deliberately lets the relay terminate TLS, and it cannot be combined with--ban-mitm. - Never add TCP, UDP, payment,
--cache,--cache-ttl, or--overlayflags that the user did not request;--cachehands the relay the static files and browser TLS termination. Public metadata flags are fine when their values come from the app's own settings or from the user; do not invent them.--hideis the exception for listing: mention the default public listing, then add--hideorhide = trueonly when the user wants the tunnel unlisted. - For a paid route, follow
references/x402.md. Keep payment policy on the smallest requested path, use an explicit network, and never place wallet or facilitator secrets in a command, log, committed file, or final response.
Before executing, show the exact public target and any important exposure consequence when it is not already obvious from the user's request.
5. Start and Observe the Tunnel
- Run a temporary
portal exposein a foreground PTY or managed long-running command session. Do not hide it behind an untrackednohupprocess. - For persistent mode, inspect any existing agent config and running service first.
run,restart, andstopare service-wide: they affect every[[tunnels]]entry that the selected service owns. Reuse and merge the existing config when the same agent should keep other tunnels. An isolated second agent needs its own config,service_name,state_dir, and loopbackcontrol_addr. Changing onlyservice_namestill shares the default state directory and127.0.0.1:4018. Do not stop or replace an agent that already owns unrelated tunnels. - Create or update only the selected agent config, then start it with
portal agent run --config <path>after the user accepts OS-service installation (portal agent runrequires the file to exist; it never creates one), orportal agent run --foreground --config <path>when the current session should own the process.--foregroundopens the interactive dashboard when stdin and stdout are TTYs. Run that command in a non-TTY managed session so logs stay capturable and the TUI does not start. - Do not run
portal agent dashboard. It is an interactive TUI. Give the user that command in the handoff. - Capture bounded output. Redact tokens, identity material, signed payloads, and credentials.
- HTTP tunnels are ready on a log event with field
public_url. The message still starts withservice ready at. Relayhttps://values inlistener_relays/added_relaysare not ready. Raw TCP/UDP tunnels lograw transport endpoints allocatedwithtcp_addrand/orudp_addrinstead. Do not wait for an HTTPS URL on a raw transport. A later line startingrelay no longer active forretracts a URL; re-verify before handoff if you see one.
6. Verify the Public Endpoint
- Confirm the lease the way the relay website does:
GET <relay>/api/statelists the hostname withready > 0once the tunnel can serve;readyat zero means registered but not yet connected. - For HTTP, make a bounded HTTPS request to every public URL being handed off. A deliberately authenticated app may return
401or403; explain that as reachable but protected. Treat unexpected5xx, TLS errors, or a Portal error page as a failed deployment. - For each paid route, make an unpaid request with a protected method and require
402 Payment Requiredplus a payment-requirements header. Compare the returned network, asset, recipient, amount, and resource with the requested policy. Verify the method scope by requesting an intentionally unprotected method when one exists. Never spend funds merely to verify configuration. - For raw TCP or UDP, protocol-probe the allocated
tcp_addr/udp_addrwithout mutating application data. A successful local port open is not enough. - When a browser-capable tool is available and the app has UI, load the primary page and check for an obvious render or runtime failure. Do not log in or submit data unless the user requested it.
- Re-check the local health endpoint if the public request fails so the handoff distinguishes app failure from tunnel or relay failure.
7. Hand Off the Result
Report:
- Deployment mode and exact local target.
- Public URL or allocated raw endpoint, and the verified status.
- Whether the tunnel is listed on public relays or hidden with
--hide. - Whether MITM handling is detect-only or
--ban-mitm. - For x402, the protected paths and methods, human amount, network, public recipient, facilitator mode, and whether the unpaid
402challenge was verified. State explicitly when settlement was not tested. - The identity path and that it must stay out of version control.
- The app and Portal process/session or OS-service ownership.
- The exact stop or restart command, and whether that command affects other tunnels on the same agent.
- Anything that remains temporary, unavailable, or unverified.
Do not call the result permanent when the local machine, app process, or foreground tunnel must remain running.
If Portal-specific friction materially affected the task, report one sanitized sentence (command, expected versus actual). Do not initiate GitHub feedback handling, write feedback files, or query extra relays unless the user explicitly requests that follow-up.
Loopback Relay Variant
Use this variant only when the user asks to expose through a relay running on this machine. The relay must already be running; the client never starts one. Do not consult or fall back to the public registry in this mode.
- Run the client and the relay from the same Portal checkout or release. This pairing is for local development and test harnesses only; production users expose through their relay's public deployment. A mixed pair (for example an installed release against a worktree relay) can register hostnames the relay's SNI router never matches, and the tunnel can stall while the client retries.
- Point the client at the relay's canonical SNI origin:
portal expose <loopback-target> --name <name> --identity-path <absolute-path-outside-repo> --relays https://127.0.0.1:<sni-port> --discovery=false. When port 443 is unavailable, start the same-tree relay withrelay-server --portal-url https://127.0.0.1:<port>;SNI_PORTfollows thePORTAL_URLport, so pass--sni-portonly when the bind port differs from the public one. The SNI router is the relay's single ingress: it serves the root host's Admin/API handler in-process. - Treat the tunnel as ready only when the log prints the line starting
service ready atcarryingpublic_url. Listener or added-relayhttps://URLs in the same output describe relay listeners, not tenant readiness. - Verify the emitted
public_urlitself with one bounded request.*.localhostoften resolves to::1first, so usecurl -sk --ipv4 --connect-timeout 5 --max-time 15 -o /dev/null -w '%{http_code}' <public-url>and accept the app's real status (401 or 403 means reachable and protected). - Stop and report instead of improvising when a required fact is missing: no relay admin URL or port, no identity path outside the repository, or no
service ready atline within a bounded wait. Do not substitute registry relays or start extra relays to unblock the run.
Failure Rules
- Local app unhealthy: stop before exposing it and report the failing check.
- Portal absent and installation not approved: provide the official command without executing it.
- No ready public URL or allocated raw endpoint: keep the bounded diagnostic output and report the relay/tunnel failure.
- Paid route returns anything other than the expected
402challenge: do not describe it as protected or hand it off as ready. Stop only the tunnel created by this workflow, preserve bounded diagnostics, and report the policy mismatch. - MITM self-probe warning without
--ban-mitm: report the warning and offer--ban-mitm; do not claim the relay was blocked. Aself-probe timed outorself-probe failedline is not a detection; report passthrough as unverified. - Requested name unavailable: offer an auto-generated or alternative name; do not silently hijack another identity.
--namehad no effect: the identity file already existed and supplied its saved name. Point--identity-pathat a new file for a new name.- Existing agent owns other tunnels: do not stop or replace it to publish this app.
- Cancellation: stop only processes started by this workflow, unless the user explicitly asks to stop an existing app or agent.
Files (portal-tunnel)
-
agents
-
openai.yaml 215 B
interface: display_name: "Expose App with Portal" short_description: "Expose local apps via Portal" default_prompt: "Use $portal-expose to publish this local app safely with Portal and verify its public URL."
-
-
references
-
game-hosting.md 5.6 KB
# Game Server Hosting Reference Read this when the user asks to host, publish, or share a game server (Minecraft, Terraria, Palworld, Valheim, Rust, or any game with a dedicated server). Game hosting uses Portal's raw TCP/UDP transport, not HTTP — the workflow, prerequisites, and verification differ fundamentally from web exposure. > **Sync notice**: the Minecraft Java, Terraria, Palworld, Valheim, and Rust rows and the relay-setup facts in this file mirror `docs/src/routes/game-server-hosting/+page.md`; the Minecraft Bedrock row and the Transport limits section mirror the Limitations in `docs/src/routes/tcp-udp-tunneling/+page.md` and have no docs-table counterpart. When Portal's transport capabilities change (new game support, multi-port allocation, etc.), update both files together. The docs page is the human-facing source; this reference is the agent-facing copy. ## Game quick-reference | Game | Transport | Ports | Status | Notes | |---|---|---|---|---| | Minecraft Java | TCP | 25565 | Tested | Single TCP port. | | Minecraft Bedrock | UDP | 19132 | Untested | Portal caps UDP datagrams at 1350 bytes and Bedrock's default MTU is larger; do not promise Bedrock support. | | Terraria | TCP | 7777 | Compatible | Single TCP port. | | Palworld | UDP | 8211 | Experimental | Verify real gameplay before sharing. | | Valheim | UDP pair | 2456–2457 | Not supported yet | Requires a base port and the next; Portal allocates one UDP port per lease. | | Rust | UDP game + query | separate | Not supported yet | Requires separate public game and query ports. | | Custom | Ask user | user-specified | — | Identify the protocol (TCP or UDP) and every port before exposing. | ## Hard rule: the relay must support raw transport Game hosting fails silently if the relay does not have TCP/UDP allocation enabled. Most public relays do **not** — check before promising the user anything: - Relay must set `TCP_ENABLED` and/or `UDP_ENABLED` with a `MIN_PORT`–`MAX_PORT` range. - The relay must publish those ports (`MIN_PORT-MAX_PORT:MIN_PORT-MAX_PORT/tcp` and `/udp` in its compose). - For UDP, the relay must additionally publish `443/udp`, the QUIC backhaul on the public `PORTAL_URL` port, not only the lease range. The bundled `docker-compose.yml` ships that line commented out. - The relay's cloud firewall must allow the same ports. If no participating relay has raw transport enabled, tell the user they need a relay that does (self-hosted relay with TCP/UDP enabled, or a community relay that supports it). Do not attempt the tunnel — it will fail without a clear error. ## Transport limits - UDP datagrams above 1350 bytes are dropped by the relay; check the game's packet size or MTU settings before promising UDP support. - UDP flows idle for 5 minutes are forgotten by the relay; protocols that can go quiet need keepalives. - One lease may carry both `--tcp` and `--udp` (`portal expose --tcp --udp --udp-addr localhost:19132 localhost:25565`); both ports come from the same `MIN_PORT`–`MAX_PORT` range. - A relay serves at most `MAX_PORT - MIN_PORT + 1` concurrent leases per protocol; size the range for the expected number of simultaneous tunnels. - Raw TCP/UDP adds no Portal tenant TLS, so game traffic is visible to the relay; rely on the game's own password or allowlist. ## Workflow ### 1. Identify the game and its requirements Look up the game in the table above, or ask the user for the protocol and port(s). If the game needs multiple related UDP ports (Valheim, Rust), report that Portal cannot support it yet rather than partially exposing. ### 2. Check relay support Confirm the chosen relay (or relay pool) has the needed transport enabled. `portal expose` with `--tcp` or `--udp` will fail or hang if the relay cannot allocate a port. ### 3. Start the local game server The game server must be running and listening on its expected port before the tunnel opens. Verify with a local connection (e.g., `nc -z 127.0.0.1 25565` for TCP, or a protocol-appropriate UDP probe). ### 4. Expose with the correct transport ```sh portal expose <game-port> --udp --name <name> ``` UDP-only Palworld example; `--udp-addr` is needed only when the UDP port differs from the positional target: ```sh portal expose 127.0.0.1:8211 --udp --name <name> ``` or for TCP: ```sh portal expose <game-port> --tcp --name <name> ``` Game servers are long-lived — recommend the persistent agent config for anything beyond a one-off session. ### 5. Verify the raw transport endpoint Unlike HTTP tunnels, raw transports log `raw transport endpoints allocated` with `tcp_addr` and/or `udp_addr` — **not** a `service ready at <URL>` line. Do not wait for an HTTPS URL. Verify by connecting through the public endpoint: - TCP: attempt a connection to the `tcp_addr` (e.g., `nc -z <host> <port>`) - UDP: send a protocol-appropriate packet and expect a response (game-dependent; a no-response UDP probe proves nothing) A successful local port check is not sufficient — verify through the public endpoint. ### 6. Hand off Report: the `tcp_addr` or `udp_addr` for players to connect to, which game and version is hosted, and the tunnel lifecycle (persistent agent or foreground). Players connect directly to `host:port` — they do not install Portal. ## Failure rules - Game needs a port group Portal cannot allocate: report the limitation, do not partially expose. - No relay with raw transport available: report before attempting, suggest a self-hosted relay. - Public endpoint unreachable while local game server works: check the relay's port publishing and firewall first. - UDP probe gets no response: some game servers do not respond to empty probes — try connecting with the actual game client before declaring failure. -
portal-cli.md 11.3 KB
# Portal CLI reference for app deployment Last checked against `gosuda/portal-tunnel` main commit `d38001ad` on 2026-09-22. Prefer the behavior of the installed Portal version and the repository's current `docs/src/routes/cli-reference/+page.md` and `docs/src/routes/portal-agent/+page.md` when they differ from this snapshot. ## Installation Check first: ```sh command -v portal portal version ``` Official Unix installer: ```sh curl -fsSL https://github.com/gosuda/portal-tunnel/releases/latest/download/install.sh | bash ``` Official PowerShell installer: ```powershell $ProgressPreference = 'SilentlyContinue' irm https://github.com/gosuda/portal-tunnel/releases/latest/download/install.ps1 | iex ``` Installation changes user or system paths. Obtain approval before running it. A relay also serves an installer at `<relay>/api/install.sh` and `<relay>/api/install.ps1`, which is what its website's quick start shows. Prefer the official GitHub installer. If the user insists on the relay-hosted one, verify the installed binary against the `checksums.txt` or `.sha256` asset of the matching GitHub release, fetched from github.com, because the relay serves the script, the binary, and the checksum together and a relay-provided checksum authenticates nothing on its own. ## Temporary exposure `portal expose` defaults `--identity-path` to `identity.json` in the process working directory. That file is created automatically and contains private key material. Always pass an absolute path outside the repository. The `IDENTITY_PATH` environment variable sets the same flag, and the same variable name means the identity directory for `relay-server`; on a host that runs both, prefer the explicit flag. An existing identity file supplies the saved public name as well as the key. `--name` applies only when a new identity is created and never renames an existing one, so use one identity file per public name. Web app: ```sh portal expose 3000 --identity-path "$HOME/Library/Application Support/Portal Tunnel/identities/preview.json" portal expose 127.0.0.1:3000 --name my-app --identity-path /absolute/path/to/identity.json ``` On Linux, prefer `$XDG_CONFIG_HOME/portal-tunnel/identities/` or `~/.config/portal-tunnel/identities/`. Create the parent directory if needed. Trusted static site or HTML entry: ```sh portal expose --serve ./site --name my-app --identity-path /absolute/path/to/identity.json portal expose --serve ./site/index.html --name my-app --identity-path /absolute/path/to/identity.json ``` Static serving follows symlinks inside the selected directory. Expose only a directory whose complete contents and symlink targets are intended to be public. Multiple HTTP services: ```sh portal expose --name my-app \ --identity-path /absolute/path/to/identity.json \ --http-route /api=http://127.0.0.1:3001 \ --http-route /=http://127.0.0.1:5173 ``` Specific relay only: ```sh portal expose 3000 \ --identity-path /absolute/path/to/identity.json \ --relays https://portal.example.com \ --discovery=false ``` Unlisted on relay screens: ```sh portal expose 3000 --hide --identity-path /absolute/path/to/identity.json ``` Fail-closed MITM handling: ```sh portal expose 3000 --ban-mitm --identity-path /absolute/path/to/identity.json ``` Raw transport, only on explicit request: ```sh portal expose 127.0.0.1:25565 --name game --tcp --identity-path /absolute/path/to/identity.json portal expose 127.0.0.1:19132 --name game --udp --identity-path /absolute/path/to/identity.json ``` `--udp` adds a UDP relay on the default stream lease for the positional target. For a UDP-only request, the positional target must be that UDP service. `--udp-addr` is only for an explicit combined stream-plus-UDP request, for example `portal expose 127.0.0.1:8080 --udp --udp-addr 127.0.0.1:19132`. Do not point the positional target at an unrelated HTTP app just to attach UDP. `--tcp` and `--udp` require relay-side support. `--http-route` cannot be combined with `--udp`; `--serve` cannot be combined with a target, HTTP routes, TCP, or UDP. `--cache` requires `--serve` and cannot be combined with `--ban-mitm`; `--cache-ttl` requires `--cache` and must be `0` or between `1s` and `8760h`. ### Flags The flag is `--description`, not `--desc`. Environment variables in the third column set the same value. | Flag | Default | Env | Meaning | |------|---------|-----|---------| | `--relays` | | | Additional relay API URLs, comma-separated; a missing scheme means https | | `--discovery` | `true` | | Include bootstrap relays and discover more | | `--overlay` | `false` | `OVERLAY_ENABLED` | Prefer IVNP overlay transport when available | | `--ban-mitm` | `false` | `BAN_MITM` | Ban a relay when the self-probe detects TLS termination | | `--identity-path` | `identity.json` | `IDENTITY_PATH` | Identity file, created when missing | | `--identity-json` | | `IDENTITY_JSON` | In-memory identity JSON; wins over the file | | `--name` | generated | | Public hostname prefix, one DNS label; applies only to a new identity | | `--description`, `--tags`, `--owner`, `--thumbnail` | | | Public listing metadata; `--tags` is comma-separated | | `--hide` | `false` | | Keep the service out of relay listings | | `--http-route` | | | `PATH=UPSTREAM [METHOD[,METHOD...]:AMOUNT]`, repeatable | | `--serve` | | | Serve a directory, or an HTML file as the SPA entry of its folder | | `--cache` | `false` | | Let selected relays store `--serve` content and terminate browser TLS | | `--cache-ttl` | `0` | | Requested offline cache lifetime; `0` uses relay policy | | `--udp` | `false` | `UDP_ENABLED` | Add a public UDP relay | | `--udp-addr` | the target | `UDP_ADDR` | Local UDP target when it differs from the positional target | | `--tcp` | `false` | `TCP_ENABLED` | Request a dedicated raw TCP port, no TLS | | `--max-active-relays` | `3` | `MAX_ACTIVE_RELAYS` | Cap on auto-selected relays kept connected | | `--metrics-addr` | | | Serve Prometheus `/metrics` on `host:port` | | `--x402-pay-to`, `--x402-network`, `--x402-asset`, `--x402-endpoint`, `--x402-testnet`, `--x402-facilitator-token` | | `CSPR_CLOUD_API_KEY` for the token | Paid routes; see `x402.md` | `portal list` accepts `--relays` and `--default-relays` (default `true`) and prints one `RELAY VERSION` row per resolved relay. It never lists services. ## Persistent agent `portal agent run` without `--foreground` installs and starts a per-user OS service (launchd LaunchAgent or systemd user unit). Obtain approval before that path. `--foreground` keeps the agent in the current process and skips service installation. `run`, `restart`, and `stop` are service-wide. They apply to every `[[tunnels]]` entry owned by that `service_name`. Do not point a second config at the default `portal-agent` service if an existing agent already has unrelated tunnels. An isolated agent also needs its own `state_dir` and loopback `control_addr`; those otherwise default to the shared data directory and `127.0.0.1:4018`. `--foreground` skips OS-service installation, but if stdin and stdout are TTYs it then opens `portal agent dashboard`. Run it from a non-TTY session when the agent should stay in the background and emit logs. `portal agent run` does not accept `--relays` or `--discovery`. Put those values on the tunnel entry. A tunnel entry is one of three modes: `target`, `http_routes`, or `serve` for a static site. Omit `identity_path` unless you have an absolute path outside the repository; an empty value stores identity under the agent state directory. `portal agent dashboard` is an interactive TUI that owns the terminal until Ctrl+C. Do not run it from an agent session. Give the user the command. Minimal config: ```toml [agent] control_addr = "127.0.0.1:4018" service_name = "portal-agent" [[tunnels]] id = "web" name = "my-app" target = "127.0.0.1:3000" discovery = true description = "Managed web tunnel" tags = ["web"] ``` Isolated second agent: ```toml [agent] control_addr = "127.0.0.1:4019" service_name = "portal-agent-my-app" state_dir = "/absolute/path/to/portal-agent-my-app" [[tunnels]] id = "web" name = "my-app" target = "127.0.0.1:3000" ``` User-selected relay, unlisted, fail-closed MITM: ```toml [[tunnels]] id = "web" name = "my-app" target = "127.0.0.1:3000" relays = ["https://portal.example.com"] discovery = false hide = true ban_mitm = true ``` Commands: ```sh portal agent run --config /absolute/path/to/config.toml portal agent run --foreground --config /absolute/path/to/config.toml portal agent restart --config /absolute/path/to/config.toml portal agent stop --config /absolute/path/to/config.toml ``` Handoff-only, do not execute in the agent session: ```sh portal agent dashboard --state-dir /absolute/path/to/state ``` Keep `control_addr` on loopback. Default config locations: - Linux: `$XDG_CONFIG_HOME/portal-tunnel/agent/config.toml` or `~/.config/portal-tunnel/agent/config.toml`; as root, `/etc/portal-tunnel/agent/config.toml` - macOS: `~/Library/Application Support/Portal Tunnel/Agent/config.toml`; as root, `/Library/Application Support/Portal Tunnel/Agent/config.toml` - Windows: `%ProgramData%\Portal Tunnel\Agent\config.toml` ### Config keys `[agent]`: `state_dir` (defaults to the platform data directory), `control_addr` (default `127.0.0.1:4018`), `service_name` (default `portal-agent`), `allowed_wallets` (wallet addresses allowed to sign in to the dashboard). `[[tunnels]]`: `id`, `name`, `target`, `serve` (static site directory or HTML file, relative to the config file's directory; cannot be combined with `target`, `http_routes`, `tcp`, or `udp`; the relay cache options have no TOML equivalent), `http_routes` (entries with `prefix`, `upstream`, `methods`, `amount`), `relays`, `discovery`, `overlay`, `identity_path`, `identity_json`, `udp`, `udp_addr`, `tcp`, `ban_mitm`, `max_active_relays` (default `3`), `description`, `tags`, `owner`, `thumbnail`, `hide`, `x402_pay_to`, `x402_testnet`, `x402_network`, `x402_asset`, `x402_endpoints`, `x402_facilitator_token`. The keys mirror the `portal expose` flags one to one. ## Output and readiness - A Portal process can start while relay discovery, lease registration, and reconnects continue in the background. - HTTP readiness log: `service ready at <https-url>`. - Raw TCP/UDP readiness log: `raw transport endpoints allocated` with `tcp_addr` and/or `udp_addr`. Do not wait for an HTTPS URL in that mode. - Multiple relays can produce multiple public URLs. Report and verify each URL or raw endpoint that is handed off. - Tenant TLS terminates locally through a keyless TLS 1.3 server: the relay signs each handshake with its certificate key but never receives the session keys. The hostname is still publicly reachable. Without `--hide`, anyone can enumerate the lease through `GET <relay>/api/state`, and relays that enable their directory page show it there as well. Portal does not add application authentication. - The MITM self-probe runs against every relay whose tenant TLS stack exports keying material. `--ban-mitm` defaults to false. Without it, a mismatch is a warning and the tunnel keeps serving. With it, Portal bans the relay and closes that listener, and refuses at registration a relay that cannot export keying material. `--cache` disables the probe because it deliberately permits relay TLS termination; the two flags cannot be combined. A `self-probe timed out` or `self-probe failed` line is not a detection. - A later log line starting `relay no longer active for <url>` retracts a previously ready URL. -
safety-and-verification.md 4.7 KB
# Exposure safety and verification ## Public-exposure check Before creating a tunnel, identify the exact target and confirm that it belongs to the requested app. Treat these as high-risk targets that require explicit user direction: - Databases and caches such as PostgreSQL, MySQL, Redis, or MongoDB. - Docker/container sockets and daemon APIs. - Debuggers, profilers, development consoles, internal dashboards, and admin panels. - Services without authentication that can read files, execute code, change configuration, or mutate production data. - Local development servers that expose source trees, environment-derived data, directory listings, or unrestricted filesystem paths. End-to-end transport encryption does not make an unauthenticated service private. The public hostname can still be reached by anyone who learns it. Unless `--hide` / `hide = true` is set, every participating relay lists the lease in `GET /api/state`, so anyone can enumerate it. The relay's landing-page directory shows that same list only when the operator opts in (`LANDING_PAGE_ENABLED`, default `false`, changeable later from the admin dashboard). Listed services can also receive public up/down reputation votes (`POST /api/reputation/vote`) on relays running the current release. ## Secret handling - `portal expose` defaults `--identity-path` to `./identity.json` and writes `private_key`, `mnemonic`, and `token_secret` there. Always override it with an absolute path outside the repository. For `portal agent`, omit `identity_path` so identity stays under the agent state directory. - Never print or commit Portal identity JSON, private keys, access tokens, agent endpoint tokens, wallet material, payment facilitator tokens, or installer credentials. - Keep persistent identity and agent state outside the repository. If the user intentionally keeps state under the project, verify that its directory is ignored before creating files. - Pass secrets through the environment or an existing secret manager. Do not place them directly in commands, logs, skill output, or TOML unless the upstream workflow has no secret reference mechanism and the user explicitly accepts the storage risk. - Avoid environment dumps, `ps eww`, or other diagnostics that reveal process environments. - `IDENTITY_PATH`, `TCP_ENABLED`, and `UDP_ENABLED` are read by both `portal expose` and `relay-server` with different meanings (an identity file versus a state directory; a per-tunnel port request versus relay-wide transport); on a host that runs both, an environment variable set for the relay silently reconfigures `portal expose`. Prefer explicit flags. ## Verification matrix For HTTP services: 1. Request a local readiness path with a short timeout. 2. Start Portal and wait for `service ready at <https-url>`. 3. Request the public URL with a short timeout and normal certificate verification. 4. Accept `2xx` or expected redirects as healthy. Treat an intentional `401` or `403` as reachable-but-protected and say so. Investigate unexpected `4xx`, any `5xx`, TLS errors, or Portal error pages. 5. If public validation fails, repeat the local request to separate app failure from tunnel or relay failure. For UI apps, use a browser-capable tool when available to check the primary page for a blank screen, obvious runtime error, failed asset loading, or redirect loop. Avoid login and state-changing interactions unless requested. For raw TCP or UDP, wait for `raw transport endpoints allocated` and protocol-probe the logged `tcp_addr`/`udp_addr`. A successful local port open alone may not prove application readiness. Do not require an HTTPS URL. ## Lifecycle handoff The final response must distinguish: - App process started by this workflow versus an existing process. - Temporary foreground `portal expose` versus `portal agent --foreground` versus a managed OS service. - Whether `portal agent stop`/`restart` would also take down other tunnels on that service. - Verified public URLs or raw endpoints versus values merely printed in logs. - Listed versus `--hide` visibility, and detect-only MITM versus `--ban-mitm`. - Whether `--cache` was used: cached responses are served by the relay with relay-terminated browser TLS and can remain available on the relay after the tunnel stops, bounded by `--cache-ttl` and the relay's `CACHE_MAX_TTL` (default `24h`). - Stop command for the tunnel and whether stopping the tunnel also stops the app. Do not promise availability after the local machine sleeps, disconnects from the network, shuts down, or stops the application; with `--cache`, say instead that the relay may keep serving the cached static files until its bounded deadline and that cache misses still need the local server. Do not run `portal agent dashboard` yourself; it is an interactive TUI. -
sdk-embed.md 8.3 KB
# Embedding the Portal SDK in a Go app Use this reference only when the app is written in Go and the user wants the tunnel to live inside the app process instead of running the `portal` CLI next to it. For every other language, and for most Go apps, the CLI path in `SKILL.md` is the right answer: it is what the relay websites, the docs, and the agent config all assume. Last checked against `gosuda/portal-tunnel` main commit `d38001ad` on 2026-09-22 (module `github.com/gosuda/portal-tunnel/v2`, release `v2.5.0`, `go 1.27`). The Go API is described in `docs/architecture.md` and by the code's doc comments; the published docs site covers the CLI and the relay wire protocol, so read `sdk/expose.go`, `sdk/http.go`, `sdk/proxy.go`, `cmd/demo-app/main.go`, and `cmd/payment-app/` when something here does not match. ## Smallest working program ```go package main import ( "log" "net/http" "os" "github.com/gosuda/portal-tunnel/v2/portal/identity" "github.com/gosuda/portal-tunnel/v2/sdk" "github.com/gosuda/portal-tunnel/v2/types" "github.com/gosuda/portal-tunnel/v2/utils" ) func main() { ctx, stop := utils.SignalContext() // cancels on SIGINT/SIGTERM defer stop() // name applies only when the identity file does not exist yet. id, err := identity.LoadOrCreate("my-app", "", os.Getenv("PORTAL_IDENTITY_PATH"), os.Getenv("IDENTITY_JSON")) if err != nil { log.Fatal(err) } exposure, err := sdk.Expose(ctx, id, nil, sdk.WithDiscovery(3), // bootstrap relays plus discovery; nil relays is allowed only with this option sdk.WithMetadata(types.LeaseMetadata{Description: "my app"}), ) if err != nil { log.Fatal(err) } defer exposure.Close() // RunHTTP does not close the exposure for you go func() { if relays, err := exposure.WaitReady(ctx); err == nil { log.Printf("public url: %s", relays[0].PublicURL) } }() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("hello")) }) if err := sdk.RunHTTP(ctx, exposure, mux, ""); err != nil { // "" = no extra local listener log.Fatal(err) } } ``` ``` module example.com/myapp go 1.27.0 require github.com/gosuda/portal-tunnel/v2 v2.5.0 ``` `sdk.Exposure` is a `net.Listener`, so `http.Serve(exposure, handler)` also works; `sdk.RunHTTP` adds header and idle timeouts, an optional second local listener, and a five-second graceful shutdown. ## Identity - `identity.LoadOrCreate(name, target, path, rawJSON)`: an in-memory JSON payload wins, then an existing file at `path` is parsed as-is, and only when neither exists is a new identity generated with `name` and written to `path` with mode `0600`. An existing file supplies the public name; `name` never renames it. One identity file per public name. - The file holds private key material (`private_key` or `mnemonic` plus `derivation_path`). Keep it outside the repository and out of images and logs. Empty `path` keeps the identity in memory only, which means a new hostname on every start. - `sdk.Expose` requires a fully resolved identity with name, address, and keys. Always go through `identity.LoadOrCreate`, `identity.Parse`, or `identity.Generate`; a hand-built struct is rejected. ## Relays and readiness - Explicit relays are always kept. `sdk.WithDiscovery(n)` adds the bootstrap set from `registry.json` and keeps at most `n` auto-selected relays connected (default 3 when `n <= 0`). Without the option at least one explicit relay is required, and nothing else is ever contacted, which keeps tests hermetic. - `sdk.WithOverlay()` is the `--overlay` equivalent. - Read the public URL from `exposure.WaitReady(ctx)`, from `exposure.Relays()` (sorted snapshot), or from `exposure.Updates()` (buffered channel of size one; slow readers miss intermediate states and should re-read `Relays()`). The SDK also logs `service ready at <url>` with field `public_url`, exactly like the CLI. - `RelayStatus` carries `RelayURL`, `PublicURL`, `TCPAddr`, `UDPAddr`, `Version`, `State` (`connecting`, `ready`, `failed`), and `Failure` (`runtime`, `terminal`, `mitm`). One failed relay does not stop an exposure that has healthy ones. - Lease renewal, re-registration after a relay restart, and reverse-session pooling are handled inside the SDK. The app does nothing for them. ## What the handler sees - Tenant TLS is terminated inside the SDK using the relay's certificate through the keyless signer; session keys never leave the process, and the handler receives plain HTTP. This is the same end-to-end property the CLI gives. - `r.TLS` is always nil, because the terminated connection is not a `crypto/tls` conn. Do not gate Secure cookies or scheme detection on it; the public scheme is always `https` for the tunnel hostname. - `r.Host` is the real public hostname. `r.RemoteAddr` is the relay end of the SDK's own outbound connection, never the browser. Portal injects no `X-Forwarded-*` headers on tunneled traffic, so the real client IP is not available in-process. - WebSockets and other upgrades work; the tunnel is a byte-transparent `net.Conn`. - To proxy to local services instead of handling in-process: `sdk.NewHTTPRoutes([]sdk.HTTPRouteConfig{{Prefix: "/", Upstream: "127.0.0.1:3000"}})` returns an `http.Handler`; `StaticRoot` serves a directory as an SPA. `sdk.Proxy(ctx, exposure, "127.0.0.1:3000")` forwards raw streams and closes the exposure when it returns, so do not also `defer exposure.Close()` on that path. ## Raw TCP and UDP - `sdk.WithTCP()` requests a public TCP port; `exposure.WaitTCPReady(ctx)` returns snapshots with `TCPAddr`. Raw connections arrive on `exposure.Accept()` untouched, with no TLS. - `sdk.WithUDP()` enables datagrams; `exposure.WaitDatagramReady(ctx)` returns `UDPAddr`. The app receives `types.DatagramFrame` values from `exposure.AcceptDatagram()` and must echo the same frame's `FlowID`, `RelayURL`, and `Address` into `exposure.SendDatagram` for replies. `sdk.ProxyUDP` does this against a local UDP target. Flows idle for five minutes are dropped; datagrams above 1350 bytes are dropped. - Both wait functions error when the matching option was not set. ## Options and their limits - `sdk.WithMetadata(types.LeaseMetadata{Description, Owner, Thumbnail, Tags, Hide})` sets the listing entry; `exposure.UpdateMetadata` changes it at the next renewal. `Hide: true` keeps the service out of `/api/state` and landing pages. - `sdk.WithStaticRelayCache(path, ttl)` lets the selected relays store a static site and terminate browser TLS for it. It cannot be combined with TCP, UDP, or MITM blocking, and it changes the trust boundary in the same way `--cache` does. - `sdk.WithMITMProtection(true)` is `--ban-mitm`: the SDK self-probes its public URL and bans a relay whose exported TLS keying material does not match. It fails at start against a relay whose tenant TLS stack cannot export keying material. ## Shutdown and errors - Cancel the context or call `exposure.Close()`; each listener unregisters its lease with a five-second budget, so hostnames free up on clean exit. - The SDK exports no sentinel errors. Match `net.ErrClosed` and `context.Canceled` as normal shutdown, and relay API failures with `errors.Is(err, &types.APIRequestError{Code: types.APIErrorCodeHostnameConflict})` and the other codes in `types/error.go`. `hostname_conflict`, `feature_unavailable`, `transport_mismatch`, `udp_disabled`, and `tcp_port_disabled` are terminal for that relay; `lease_not_found`, `unauthorized`, `rate_limited`, and network errors are retried for you. ## Paid routes The SDK is payment-agnostic. The x402 gateway lives in `cmd/portal-tunnel/agent`, which is CLI composition, not an SDK contract; do not import it into an app. An app that needs x402 wires `github.com/gosuda/x402-facilitator` directly, the way `cmd/payment-app/handler.go` does: build `PaymentRequirements`, create the Sui facilitator, wrap only the paid handler with `x402http.New(...).Wrap(...)`, and mount `suihttp.ClientHandler()` at `/x402/client.js` and `suihttp.NewPrepareHandler(...)` at `/x402/prepare` so browser and native clients can pay. On the consumer side, `portal-connect` recognizes and reports a `402`; paying it is a planned separate skill. ## Verify like the CLI Whatever the app logs, verify from outside exactly as `SKILL.md` step 6 says: one bounded request to the `public_url`, `401` or `403` counted as reachable, and the lease visible in `GET <relay>/api/state` with `ready > 0`. -
x402.md 5.8 KB
# x402 paid routes Use this reference only when the user explicitly requests x402 payment protection or a paid route. Portal applies payment policy in the local tunnel process; relay-level `/api/x402/*` configuration is separate and does not configure tunnel routes. ## Required decisions Collect these values before starting the tunnel: - Public route prefix and loopback upstream. - Protected HTTP methods, or explicit confirmation that every method on the route is paid. - Positive human-readable amount. Sui USDC supports up to 6 decimals; Casper wCSPR supports up to 9. - Exact network: `sui:mainnet`, `sui:testnet`, `casper:casper`, or `casper:casper-test`. - Payment recipient for that network. - Casper only: the wCSPR CEP-18 contract hash and facilitator choice. Do not invent or transform a recipient, asset contract, network, or amount. Prefer a dedicated paid prefix such as `/paid` over protecting `/` unless the user explicitly wants the whole app paywalled. A route amount without methods protects every HTTP method on that route. ## Temporary tunnel The route form is `PATH=UPSTREAM [METHOD[,METHOD...]:AMOUNT]`. Quote every paid route so the shell preserves the space. Sui USDC: ```sh portal expose --name paid-app \ --identity-path /absolute/path/to/identity.json \ --http-route "/paid=http://127.0.0.1:3001 GET:0.01" \ --http-route /=http://127.0.0.1:5173 \ --x402-network sui:testnet \ --x402-pay-to 0x... ``` Use `sui:mainnet` only when explicitly requested. `--x402-testnet` selects Sui testnet only when `--x402-network` is omitted; prefer the explicit network in agent-generated commands. Repeat `--x402-endpoint` only for user-selected Sui RPC fallbacks: the browser prepare flow receives the full endpoint list, while the settlement facilitator and all Casper flows use only the first endpoint. On Sui the asset is fixed to the network's allowlisted gasless USDC type; `--x402-asset` applies only to Casper. Casper wCSPR: ```sh portal expose --name paid-app \ --identity-path /absolute/path/to/identity.json \ --http-route "/paid=http://127.0.0.1:3001 GET:0.01" \ --http-route /=http://127.0.0.1:5173 \ --x402-network casper:casper-test \ --x402-asset hash-... \ --x402-pay-to account-hash-... \ --x402-endpoint https://x402-facilitator.cspr.cloud ``` The default hosted CSPR.cloud facilitator requires `CSPR_CLOUD_API_KEY`; without it the tunnel fails at startup with `CSPR.cloud x402 facilitator requires an authorization token`. Supply it through the process environment, never inline in the command. A token is refused when the facilitator endpoint is plaintext `http://` on a non-loopback host (`sends the authorization token over plaintext http; use https or a loopback endpoint`), so a custom authenticated facilitator must be `https://` or local. A user-selected unauthenticated facilitator can omit the token. Casper clients sign externally and retry with `PAYMENT-SIGNATURE` or `X-PAYMENT`; Portal verifies and settles through the facilitator. Use an `https://` facilitator for every non-loopback endpoint even when it needs no token; the verify and settle requests carry the signed payment payload. ## Persistent agent config Merge payment fields into the selected existing `[[tunnels]]` entry; do not replace unrelated tunnels. Keep the recipient and route policy in TOML, but prefer the service environment for facilitator credentials. ```toml [[tunnels]] id = "paid-app" name = "paid-app" x402_network = "sui:testnet" x402_pay_to = "0x..." [[tunnels.http_routes]] prefix = "/paid" upstream = "http://127.0.0.1:3001" methods = ["GET"] amount = "0.01" [[tunnels.http_routes]] prefix = "/" upstream = "http://127.0.0.1:5173" ``` For Casper, set `x402_network`, `x402_asset`, `x402_pay_to`, and optionally `x402_endpoints`. The hosted CSPR.cloud facilitator also needs `CSPR_CLOUD_API_KEY` in the OS-service environment. Set `x402_facilitator_token` in TOML only when no service-safe environment mechanism exists and the user explicitly accepts secret persistence; keep that config outside the repository with restrictive permissions. After changing payment routes or network fields, restart the selected tunnel or its owning agent. Remember that `portal agent restart` is service-wide and can affect unrelated tunnels. ## Verification 1. Verify each local upstream directly before opening the tunnel. 2. Request the public paid path with a protected method and no payment header. 3. Require HTTP `402` with both `PAYMENT-REQUIRED` and `X-PAYMENT-REQUIRED` carrying the same base64 requirements; every challenge sets both, so a missing header means the request did not reach the paywall. 4. Decode or inspect the JSON requirements and compare the network, asset, recipient, atomic amount, and resource URL with the requested policy. 5. Verify each intentionally free route. If only selected methods are paid, verify the method scope with a safe unprotected method only when the upstream supports it without mutation. 6. For Sui browser integration, verify that `/x402/client.js` is reachable. `/x402/prepare` is POST-only and expects a JSON body with `path` (optional `method`, default `GET`, and `sender`); a GET returns `405`, which confirms the handler is mounted without spending anything. Test a real prepare only when a valid public sender address is available and the user requested wallet-flow integration. Do not send `PAYMENT-SIGNATURE` or `X-PAYMENT`, sign wallet payloads, or spend funds as a routine health check. End-to-end settlement is a separate financial action requiring an explicit request, a user-approved wallet flow, a stated amount and network, and a handoff that reports the transaction result without exposing wallet material. If an unpaid protected request returns `2xx`, an unexpected redirect, `404`, or `5xx`, treat payment protection as unverified. Do not silently remove the amount or expose the upstream without the requested paywall.
-
-
SKILL.md 16.9 KB
--- name: portal-expose description: Expose, preview, protect with x402 payments, or keep a local web app, static site, HTTP route set, or explicitly requested TCP/UDP service reachable through Portal, then verify the public endpoint and report its lifecycle. This is the app side of Portal, the same flow as the quick-start form on a relay website (install, run one portal expose command built from the app settings, open the public URL). Use when the user asks to deploy, publish, share, tunnel, expose, or connect a local app to Portal or to a specific relay, create a public preview, add a paid route, configure x402, needs an HTTPS URL on a phone or another device because a browser feature such as microphone, camera, geolocation, or PWA install refuses to work over a LAN http address, or wants a Go app to embed the Portal SDK. Do not use for deploying a Portal relay, generic cloud hosting, publishing this plugin, or reaching a service that someone else already published (portal-connect). license: MIT --- # Expose an App with Portal Portal publishes a service that is already running on the user's machine. It does not build the app or move it to a cloud host. Treat a successful tunnel as dependent on both the local app and the Portal process or agent remaining available. This is the app side of Portal: every relay website offers the same three steps (paste what to share, run one install-and-expose command built from the app settings, open the public URL), and this skill performs those steps for the app with verification around them. Any other program that reaches the published URL needs nothing from Portal. Run the workflow in order. Open a reference only when that branch is taken: `references/x402.md` for a paid route, `references/safety-and-verification.md` for an authenticated, sensitive/high-risk, or non-HTTP service, `references/game-hosting.md` for a game server, `references/portal-cli.md` when choosing persistent-agent configuration or checking a flag, `references/sdk-embed.md` only for a Go app that should run the tunnel inside its own process. Use the installed `portal` CLI for flags. ## Choose the Mode Use the smallest mode that satisfies the request: - Temporary web preview: `portal expose <target>`. - Connect the app to a relay the user picked, as its website's quick start does: install Portal from the official GitHub installer if it is missing, then `portal expose <target> --name <name> --relays <relay-url>`, with `--discovery=false` when only that relay should carry the app. - Trusted static directory or HTML entry: `portal expose --serve <path>`. - Multiple local HTTP services under one URL: repeat `--http-route`. - Paid HTTP path: routed HTTP with an explicit x402 payment contract; never enable payment implicitly. - Durable tunnel that should survive terminal or login restarts: an explicit `portal agent` config and managed service. - Session-owned durable tunnel without an OS service: `portal agent run --foreground`. - Game server (Minecraft, Terraria, Palworld, or any dedicated game server): always start from `references/game-hosting.md` — raw TCP/UDP transport has different prerequisites and verification than HTTP. - Go app that should carry the tunnel in-process instead of running the CLI next to it: `references/sdk-embed.md`. The CLI stays the default even for Go; embedding is for users who ask for it. Default to a temporary preview when the user says only "share", "preview", or "deploy locally". Do not install an OS service unless the user asks for a persistent, managed, or restart-surviving tunnel and accepts that `portal agent run` without `--foreground` installs a per-user launchd or systemd unit. ## Workflow ### 1. Inspect the Project - Read the applicable repository instructions before running or changing anything. - Determine the app directory, start command, expected protocol, loopback target, and a meaningful health path. - Prefer declared scripts and documented ports over guessing from process lists. - Do not expose a port merely because it is listening. Tie it to the requested app. - If the project is already running, preserve its process. If it is not running and deployment was requested, start it with the project's normal command and retain the terminal/session handle. Ask one concise question only when the target, desired lifetime, or transport cannot be discovered safely. An explicit request to deploy, publish, expose, tunnel, or share authorizes creating the public tunnel for the named app; it does not authorize exposing adjacent services. For x402, do not guess the protected path, payment methods, amount, network, recipient, or network-specific asset. Collect any missing consequential value before building the command or config. Treat an omitted method list as charging every method on the route and confirm that scope when it was not explicit. ### 2. Verify the Local Service - Wait for the app's real readiness signal, not only for the process to exist. - Make a bounded local request to the selected target. For HTTP, record the URL and status. For TCP/UDP, use a protocol-appropriate check that does not mutate application data. - Stop before opening a tunnel if the local health check fails. - Warn and require explicit direction before exposing databases, container daemons, debug consoles, unauthenticated admin panels, or services containing sensitive data. - Before opening the tunnel, say that the public hostname is enumerable through each participating relay's `GET /api/state` unless the user asked for `--hide`, and that relays which enable their directory page show it there too. ### 3. Check Portal - Run `portal version` when `portal` is available. - If Portal is missing, present the official GitHub installer and request approval before running it because installation writes outside the project. A relay also serves an installer at `<relay>/api/install.sh` (`install.ps1` on Windows), but the relay then supplies the script, the binary, and the checksum together, so nothing in that download is verified independently of the relay. Use it only when the user explicitly prefers it, and before running the installed binary compare its SHA-256 with the `checksums.txt` or `.sha256` asset of the matching GitHub release, fetched from github.com rather than from the relay. Never run an installer from a relay the user did not choose or from a third-party URL. - Do not assume a hard-coded latest release or stale flags. Use the installed Portal version as the compatibility baseline. ### 4. Build the Command or Agent Config - Use loopback targets such as `127.0.0.1:<port>` unless the project explicitly needs another address. - Use the user's requested name. Otherwise derive a DNS-label-safe name from the app and pass it explicitly, so the public URL `https://<name>.<relay-host>/` is known before the tunnel starts; the relay website always emits `--name` for the same reason. `--name` applies only when the identity file is created. An existing `--identity-path` keeps its saved name and silently ignores the flag, so use one identity file per public name. - For `portal expose`, always pass an absolute `--identity-path` outside the repository. The CLI default is `identity.json` in the process working directory and that file contains private key material. For `portal agent`, omit `identity_path` so the agent stores identity under its state directory; if you set the field, use an absolute path outside the repository. - Never print or commit identity JSON, control tokens, facilitator tokens, or wallet secrets. - With a user-selected relay on `portal expose`, pass `--relays <https-url> --discovery=false`. In persistent mode those flags are not accepted on `portal agent run`; put `relays = ["https://..."]` and `discovery = false` on the `[[tunnels]]` entry instead. - Map the app's settings onto the command the way the relay quick-start form does: a port or `host:port` is the positional target; an `http(s)://` URL contributes its host and port; a directory or HTML file becomes `--serve <path>`; a thumbnail URL, description, tags, or owner that the app or user defines go to `--thumbnail`, `--description`, `--tags`, `--owner`, all of which are public metadata; a datagram service adds `--udp`, with `--udp-addr` when the UDP port differs from the target. - The MITM self-probe runs against every relay whose tenant TLS stack exports keying material. Without `--ban-mitm` / `ban_mitm = true`, a suspected TLS termination is only logged and the tunnel keeps serving; do not claim the default path blocks a relay. Add `--ban-mitm` only when the user wants fail-closed handling; it then refuses relays that cannot export keying material instead of serving unprotected. `--cache` opts out of the probe entirely because it deliberately lets the relay terminate TLS, and it cannot be combined with `--ban-mitm`. - Never add TCP, UDP, payment, `--cache`, `--cache-ttl`, or `--overlay` flags that the user did not request; `--cache` hands the relay the static files and browser TLS termination. Public metadata flags are fine when their values come from the app's own settings or from the user; do not invent them. `--hide` is the exception for listing: mention the default public listing, then add `--hide` or `hide = true` only when the user wants the tunnel unlisted. - For a paid route, follow `references/x402.md`. Keep payment policy on the smallest requested path, use an explicit network, and never place wallet or facilitator secrets in a command, log, committed file, or final response. Before executing, show the exact public target and any important exposure consequence when it is not already obvious from the user's request. ### 5. Start and Observe the Tunnel - Run a temporary `portal expose` in a foreground PTY or managed long-running command session. Do not hide it behind an untracked `nohup` process. - For persistent mode, inspect any existing agent config and running service first. `run`, `restart`, and `stop` are service-wide: they affect every `[[tunnels]]` entry that the selected service owns. Reuse and merge the existing config when the same agent should keep other tunnels. An isolated second agent needs its own config, `service_name`, `state_dir`, and loopback `control_addr`. Changing only `service_name` still shares the default state directory and `127.0.0.1:4018`. Do not stop or replace an agent that already owns unrelated tunnels. - Create or update only the selected agent config, then start it with `portal agent run --config <path>` after the user accepts OS-service installation (`portal agent run` requires the file to exist; it never creates one), or `portal agent run --foreground --config <path>` when the current session should own the process. `--foreground` opens the interactive dashboard when stdin and stdout are TTYs. Run that command in a non-TTY managed session so logs stay capturable and the TUI does not start. - Do not run `portal agent dashboard`. It is an interactive TUI. Give the user that command in the handoff. - Capture bounded output. Redact tokens, identity material, signed payloads, and credentials. - HTTP tunnels are ready on a log event with field `public_url`. The message still starts with `service ready at`. Relay `https://` values in `listener_relays` / `added_relays` are not ready. Raw TCP/UDP tunnels log `raw transport endpoints allocated` with `tcp_addr` and/or `udp_addr` instead. Do not wait for an HTTPS URL on a raw transport. A later line starting `relay no longer active for` retracts a URL; re-verify before handoff if you see one. ### 6. Verify the Public Endpoint - Confirm the lease the way the relay website does: `GET <relay>/api/state` lists the hostname with `ready > 0` once the tunnel can serve; `ready` at zero means registered but not yet connected. - For HTTP, make a bounded HTTPS request to every public URL being handed off. A deliberately authenticated app may return `401` or `403`; explain that as reachable but protected. Treat unexpected `5xx`, TLS errors, or a Portal error page as a failed deployment. - For each paid route, make an unpaid request with a protected method and require `402 Payment Required` plus a payment-requirements header. Compare the returned network, asset, recipient, amount, and resource with the requested policy. Verify the method scope by requesting an intentionally unprotected method when one exists. Never spend funds merely to verify configuration. - For raw TCP or UDP, protocol-probe the allocated `tcp_addr`/`udp_addr` without mutating application data. A successful local port open is not enough. - When a browser-capable tool is available and the app has UI, load the primary page and check for an obvious render or runtime failure. Do not log in or submit data unless the user requested it. - Re-check the local health endpoint if the public request fails so the handoff distinguishes app failure from tunnel or relay failure. ### 7. Hand Off the Result Report: - Deployment mode and exact local target. - Public URL or allocated raw endpoint, and the verified status. - Whether the tunnel is listed on public relays or hidden with `--hide`. - Whether MITM handling is detect-only or `--ban-mitm`. - For x402, the protected paths and methods, human amount, network, public recipient, facilitator mode, and whether the unpaid `402` challenge was verified. State explicitly when settlement was not tested. - The identity path and that it must stay out of version control. - The app and Portal process/session or OS-service ownership. - The exact stop or restart command, and whether that command affects other tunnels on the same agent. - Anything that remains temporary, unavailable, or unverified. Do not call the result permanent when the local machine, app process, or foreground tunnel must remain running. If Portal-specific friction materially affected the task, report one sanitized sentence (command, expected versus actual). Do not initiate GitHub feedback handling, write feedback files, or query extra relays unless the user explicitly requests that follow-up. ## Loopback Relay Variant Use this variant only when the user asks to expose through a relay running on this machine. The relay must already be running; the client never starts one. Do not consult or fall back to the public registry in this mode. - Run the client and the relay from the same Portal checkout or release. This pairing is for local development and test harnesses only; production users expose through their relay's public deployment. A mixed pair (for example an installed release against a worktree relay) can register hostnames the relay's SNI router never matches, and the tunnel can stall while the client retries. - Point the client at the relay's canonical SNI origin: `portal expose <loopback-target> --name <name> --identity-path <absolute-path-outside-repo> --relays https://127.0.0.1:<sni-port> --discovery=false`. When port 443 is unavailable, start the same-tree relay with `relay-server --portal-url https://127.0.0.1:<port>`; `SNI_PORT` follows the `PORTAL_URL` port, so pass `--sni-port` only when the bind port differs from the public one. The SNI router is the relay's single ingress: it serves the root host's Admin/API handler in-process. - Treat the tunnel as ready only when the log prints the line starting `service ready at` carrying `public_url`. Listener or added-relay `https://` URLs in the same output describe relay listeners, not tenant readiness. - Verify the emitted `public_url` itself with one bounded request. `*.localhost` often resolves to `::1` first, so use `curl -sk --ipv4 --connect-timeout 5 --max-time 15 -o /dev/null -w '%{http_code}' <public-url>` and accept the app's real status (401 or 403 means reachable and protected). - Stop and report instead of improvising when a required fact is missing: no relay admin URL or port, no identity path outside the repository, or no `service ready at` line within a bounded wait. Do not substitute registry relays or start extra relays to unblock the run. ## Failure Rules - Local app unhealthy: stop before exposing it and report the failing check. - Portal absent and installation not approved: provide the official command without executing it. - No ready public URL or allocated raw endpoint: keep the bounded diagnostic output and report the relay/tunnel failure. - Paid route returns anything other than the expected `402` challenge: do not describe it as protected or hand it off as ready. Stop only the tunnel created by this workflow, preserve bounded diagnostics, and report the policy mismatch. - MITM self-probe warning without `--ban-mitm`: report the warning and offer `--ban-mitm`; do not claim the relay was blocked. A `self-probe timed out` or `self-probe failed` line is not a detection; report passthrough as unverified. - Requested name unavailable: offer an auto-generated or alternative name; do not silently hijack another identity. - `--name` had no effect: the identity file already existed and supplied its saved name. Point `--identity-path` at a new file for a new name. - Existing agent owns other tunnels: do not stop or replace it to publish this app. - Cancellation: stop only processes started by this workflow, unless the user explicitly asks to stop an existing app or agent.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.