Tungent — agent onboarding
Hand this file to an agent (Claude Code, Codex, ADK…). No account is needed to start. Every command below uses this deployment's own base URL, so it can be copied as-is.
Tungent gives your agent a stable HTTPS hostname with minted Basic-auth credentials for any service it runs — on a laptop, a VM, a sandbox — plus an audit of what is open. Hostnames never rotate. Your device key is your identity: the first time a key shows up, a free workspace is created for it. Signing in later and claiming attaches that workspace to an account; nothing about it changes.
0. Four words this page uses
| Word | What it means |
|---|---|
| workspace | The thing that owns your hostnames and your monthly allowance. One is created for you the first time your key appears — you do not make one. |
| slug | The workspace's short name, and the second half of every hostname it publishes: in api-witty-island-c54a, the slug is witty-island-c54a. |
| endpoint | One published thing: a hostname pointing at one port of yours. 10 on the free tier. |
| device key | The Ed25519 key the CLI writes to ~/.tungent/id_ed25519. It is your identity — there is no username. |
1. Fastest: the CLI (zero signup)
Install it from npm. Needs Node 18+ and ssh (or WireGuard with --wg); no other dependencies.
npm i -g tungent
Or run it without installing: npx tungent up 3000.
The npm CLI talks to production (https://app.tungent.ai). To point it at another control plane set
TUNGENT_BASE, or use the installer this server hands out, which does that for you:
curl -fsSL https://app.tungent.ai/cli/install.sh | sh
Publish a local port. This prints the URL, the user and the password, and holds the tunnel open until you stop it.
tungent up 3000 --name my-app
Endpoints are open by default: anyone with the link can open them. That suits the common case — a demo, a preview, a page you want someone to just look at — and the hostname is not guessable at a glance, but it is a link, not a secret.
Require a password with --auth. The command then prints a URL with the
credentials already in it, so sharing is still one copy-paste.
tungent up 3000 --name private --auth
--public <path> requires a password except on the paths you name, matched
exactly — for webhook receivers and OAuth callbacks that cannot send one. Naming a
path implies --auth, since an exemption means nothing on an endpoint that is
already open.
tungent up 3000 --name hooks --public /webhook --public /oauth/callback
GET /api/v1/audit lists everything currently reachable without a password —
worth checking before you finish a task.
The same key over WireGuard instead of ssh -R. An ssh -R session forwards one
port; as a WireGuard peer your machine gets an address on our side, so the edge can
reach several ports on it — and anything else that machine can route to — without a
separate session for each.
tungent up 3000 --name my-app --wg
Attach this workspace to an account whenever you like. Prints a signed link. Claiming renames nothing, moves nothing, and doubles the free egress allowance. Merging it into a workspace you already own also keeps every address you have handed out — see §4 for what that looks like.
tungent claim
The rest: list, remove, identity, print the token.
tungent ls
tungent rm <name>
tungent whoami
tungent token
First run generates ~/.tungent/id_ed25519, registers it, stores the workspace token in ~/.tungent/config.json.
The same Ed25519 key is used for ssh -R, derived into the WireGuard key, and signs the claim link.
Second machine? Join, don't start over
On the machine that already works, mint a code. It is signed by that device and valid ten minutes.
tungent invite
On the second machine, join with it rather than starting a new workspace.
tungent up 8080 --name admin --join tgi_…
A CI box with a workspace token joins without a code.
TUNGENT_TOKEN=tg_… tungent up 3000 --name build
Invites never add allowance: every device shares the workspace's endpoints and bandwidth. Agents can mint codes
with POST /api/v1/invites.
2. Nothing to install: plain ssh
First contact prints your workspace, your device port and a claim link.
ssh tunnel@ssh.tungent.ai -p 2222
Then publish, using the port it gave you. As with the CLI, the endpoint is open
unless you ask otherwise: --auth requires a password and --public <path>
requires one except on that path.
ssh -R <device port>:localhost:3000 tunnel@ssh.tungent.ai -p 2222 expose my-app 3000
ssh -R <device port>:localhost:3000 tunnel@ssh.tungent.ai -p 2222 expose my-app 3000 --auth
The second command prints the URL and credentials and keeps the tunnel open. Any OpenSSH key works (ed25519 recommended — it doubles as your WireGuard key).
3. API (agents with an existing token, or after tungent token)
Every call: Authorization: Bearer $TUNGENT_TOKEN, JSON bodies. Sanity: GET /api/v1/whoami.
| Goal | Call |
|---|---|
| Register this device, get a token (no auth) | POST /api/v1/devices/register {public_key, name} → {token (once), workspace{slug, claim_url}, device{port, wg_address}} |
| Join an existing workspace instead | same call with {invite: "tgi_…", join_sig: <invite signed by this key>}; mint codes with POST /api/v1/invites |
| Expose something the edge can reach directly | POST /api/v1/services/expose {name, upstream:"http://10.0.0.5:8080", public_paths:[…], persistent, ttl_seconds} |
| Tunnel a local port over ssh | POST /api/v1/tunnels {name, local_port} → {ssh_command, service{url, auth, …}}, run the command. A raw ssh -N -R does not reconnect: for anything that must stay up use tungent up (reconnect loop) or tungent install-service, or wrap the command in autossh -M 0. Your app sees its public hostname in Host (and X-Forwarded-Host). A name belongs to the device behind the token (device tokens) or to device if you send it; a held name answers 409 NAME_HELD — send takeover: true to move it. Send a per-process instance and initial: true on the first call to be told (409 SUPERSEDED) if another process takes the name later |
| WireGuard config for this device | GET /api/v1/wg/peers (the device: true one) → GET /api/v1/wg/peers/<id>/config, fill PrivateKey (CLI does it) |
| Expose a port through a WireGuard peer | POST /api/v1/wg/expose {peer_id, name, port, public_paths} |
| List (passwords withheld) | GET /api/v1/services |
| Require a password at creation | add auth_disabled: false to any expose call above (new endpoints are open unless you do) |
| Open a path / switch auth on or off / TTL | PATCH /api/v1/services/<name> {public_paths, auth_disabled, persistent, ttl_seconds} — switching auth on (auth_disabled: false) returns user and password |
| Remove | DELETE /api/v1/services/<name> |
| What is open right now | GET /api/v1/audit — check before finishing a task |
| Bandwidth this month | GET /api/v1/usage |
Response of any expose says who can open the endpoint:
auth: "open"(the default) — anyone with the link can open it. There is nouserorpassword, andauth_notesays how to require one. Never tell a user an open endpoint is protected.auth: "password"—userandpasswordprotect it, except on anyopen_paths.share_urlis the link to give a person: it opens the app in any browser (a cookie on that host, then the clean URL) and stops working when the password rotates. Don't hand peoplehttps://user:pass@host: Chromium then refuses the app's own relativefetch()calls. Scripts and curl use-u user:password.
Both carry name, hostname, url, expires_at, audit_url.
A password is returned only by the call that makes it protect something; store it. Never guess it later —
re-expose keeps the same credentials, a new auth_user rotates them.
One laptop, several workspaces (consultants)
List the workspaces this device belongs to; the current one is marked.
tungent workspaces
Switch, or pass --workspace globex on any single command.
tungent use globex
Over plain ssh: expose globex/api 3000. A device joins more workspaces with invites; each has its own port and token.
Teammates
Owner → dashboard → Invite member (or POST /api/workspaces/<id>/invites) → a link valid 7 days. Whoever opens it
signed in becomes a member: full use of the workspace's services, tokens and tunnels; the owner keeps billing and
invites. Members are unlimited on every plan and never add allowance.
4. Claiming (the human, once, any time)
tungent claim prints a link signed by the device key (valid 1 h). Open it while signed in — or sign up and you
come straight back to it.
- If you had never signed up before, signing up hands you an empty account workspace named after your email. Claiming folds your working workspace straight into it, so you end up with one workspace: your existing addresses keep working, and what you publish next is named after the account. No choice to make; keeping the two apart is one click if you want it.
- If you already own a workspace with things in it, the page offers to Merge into … it, or to keep the claimed one on its own.
Merging folds one workspace into another. The clearest way to think about it: the old workspace name becomes an alias living inside the new one. Everything you have already published keeps the address it already has — old slug and all — and anything you publish from now on is named after the workspace you merged into.
Say you had been working without an account, in a workspace called witty-island-c54a, and you own one called
acme:
| Address | |
|---|---|
| Published before the merge | https://api-witty-island-c54a.tungent.ai |
| The same thing, after the merge | https://api-witty-island-c54a.tungent.ai — unchanged |
| The next thing you publish | https://dashboard-acme.tungent.ai |
Nothing you have handed out stops working, which is the point: a link you gave a colleague, a webhook URL you registered with Stripe, an OAuth redirect you configured — all of them keep resolving. Passwords, open paths and idle timers come across untouched, and so do your devices, tunnel ports, WireGuard peers and tokens. The page lists what you are about to fold in before you confirm. The dashboard then shows credentials, tunnels, peers, members, audit, usage and billing.
5. Limits and plans
Only two things are metered: live endpoints and egress. API tokens, devices and members are unlimited on every plan.
New anonymous workspaces are rate-limited per source address — a few dozen a day — to keep the free tier from being farmed. Existing workspaces and claimed accounts are unaffected; if you hit it, claim or reuse a workspace, or try tomorrow. The 429 message says which.
While its connector is connected a tunnel never goes idle, traffic or not. If the
connector goes away, the hostname is kept for 2 hours (idle_ttl_seconds: 7200)
and comes back unchanged when the connector returns; pass --persistent, or
persistent: true, to keep it registered indefinitely without a connector. The
name is never recycled either way — that is the part that does not expire.
- Free — 10 live endpoints, 1 day of logs, hostnames on
tungent.ai. Egress is 1 GB a month before the workspace is claimed and 2 GB after — claiming it is free and doubles the allowance. - Solo, $4 — 10 endpoints, 25 GB, 30 days of logs, 1 custom domain.
- Builder, $12 — 50 endpoints, 200 GB, 90 days of logs, 1 custom domain, 1 delegated zone.
- Team, $39 — 250 endpoints, 1 TB, 365 days of logs, 5 custom domains, 5 delegated zones.
Upgrade from the dashboard. Passing the egress cap returns 402 to browsers and 429
with Retry-After to API clients, rather than silently dropping traffic.
6. Rules for agents
- The URL is printed the instant the tunnel is registered; the edge takes about half a second more to route it, so the first request may 502 — retry for a second or two before treating the endpoint as broken.
- Treat
user/passwordas secrets — tool result or the user's secret store, never logs, commits or chatty output. - Prefer
public_pathsoverauth_disabled; if you open a host, say so and point the user toGET /api/v1/audit. - Never fabricate a credentialed URL or a share link. If the password is gone: re-expose with a new
auth_user, or the dashboard shows it. - Clean up:
DELETEone-off hostnames, or let the TTL retire them. Keeptungent up/ the ssh command running for as long as the URL must work.
Your apps, as cards: the dock
GET /api/v1/dock lists the workspace's apps the way the Apps page shows them to a person: title, the
open link (credentials inside it when the app is locked), whether it is up, how often it was opened, and a
screenshot taken after the app had loaded (shot.url, owner-only). PATCH /api/v1/dock/<name> sets
title, icon (an emoji), hidden and screenshots; POST /api/v1/dock/<name>/capture asks for a fresh
shot. The screenshot worker fetches the public hostname with the app's own credentials and is never metered
against the workspace. A person can add the dock to a phone's home screen; that is where your apps are found
again.
One name, one connector
A name belongs to the tungent up process that holds it. Start a second
tungent up with the same --name on the same machine and the new one takes
the name; the first is disconnected and exits saying so. From another device you
get 409 NAME_HELD and the holder's fingerprint; --takeover moves the name to
that device and the previous connector exits with the reason. A connector whose
network dropped simply reconnects (it is still the holder), and a fresh
tungent up after a crash always wins over the crashed one's leftover session.
When the process that holds a name stops: on the same machine any fresh
tungent up takes it; from another device the name is yours again as soon as
that connector is gone (no --takeover needed), and --takeover moves it while
it is still running.
Restarts and maintenance
Tunnels survive edge restarts without you doing anything, on both transports. What differs is how fast they come back: an ssh connector reconnects in a few seconds (measured 3.6 s median over six restarts); a WireGuard peer re-handshakes on its own timer — a kernel client (wg-quick) in about 15 s, a userspace client (wireguard-go, wireproxy) in one to two minutes. Staging has a nightly maintenance window at 03:10–03:40 UTC.
WireGuard config naming
tungent up <port> --wg writes the readable config as ~/.tungent/tungent-<workspace>.conf
and brings the tunnel up through a short sibling symlink tg-<6 hex>.conf, because
wg-quick names the interface after the file and Linux caps interface names at 15
characters while workspace slugs run to 20. The CLI prints the interface, the config
name and the exact wg-quick down command.
Two workspaces on one machine therefore no longer collide on the interface name. One
case remains manual: two separate device keys on the same host each route the edge
at 10.77.0.1/32, and only one can own that route in the main table, so the second
wg-quick up reports RTNETLINK answers: File exists. Add Table = off to that
config plus a PostUp that puts the route in its own table with an ip rule from <peer address>. A single device in several workspaces is unaffected: it shares one
peer and one address.
Keeping a tunnel up across restarts
tungent up reconnects by itself: it notices a dead server in about 30 seconds
(ServerAliveInterval=15, ServerAliveCountMax=2), then retries with jittered
exponential backoff capped at 30 seconds, re-registering the tunnel each attempt
so an idle TTL or a lost registry row cannot strand it. The hostname and
credentials are stable across a reconnect because the device port is derived
from the device key, not the session. Pass --no-reconnect to opt out.
For something that must also survive a reboot, let a supervisor own it. This writes a systemd user unit on Linux, or a launchd job on macOS:
tungent install-service 3000 --name my-app
It writes the unit with --no-reconnect, so the supervisor does the restarting
rather than both of them at once, and prints the commands to enable it. On Linux
run loginctl enable-linger "$USER" so it keeps running while you are logged out.
WireGuard needs none of this: peers re-handshake on their own.