10 KiB
@elizaos/plugin-tailscale
Tailscale tunnel backend plugin for elizaOS — exposes a local agent port through either the locally-installed tailscale CLI or an Eliza Cloud auth-key-minting path.
Purpose / role
This plugin adds a Tailscale-backed serviceType="tunnel" implementation to an Eliza agent. It registers no actions of its own; user-facing tunnel operations go through the canonical TUNNEL action from @elizaos/plugin-tunnel. Backend selection (local CLI vs. cloud) happens at init() time via selectTunnelBackend. The plugin is opt-in — add @elizaos/plugin-tailscale to the agent's plugin list to enable it. If another plugin has already claimed the "tunnel" slot (tunnelSlotIsFree returns false), this plugin logs a warning and leaves the existing tunnel service in place.
Plugin surface
Providers
tailscaleStatus— injects current tunnel state (active, url, port, uptime, provider) as compact JSON into the LLM context on every turn. Dynamic, cache-scopeturn, context-gated tosettings/connectors.
Services (registered dynamically in init, not declared statically)
LocalTailscaleService— wraps the localtailscaleCLI (tailscale serve/tailscale funnel). Requirestailscaleto be installed and the user already authenticated to a tailnet.CloudTailscaleService— callsPOST /v1/apis/tunnels/tailscale/auth-keyon Eliza Cloud to mint a scoped ephemeral auth key, then runstailscale up+tailscale serve/funnellocally. RequiresELIZAOS_CLOUD_API_KEY.
Both services implement ITunnelService from @elizaos/plugin-tunnel and register under serviceType="tunnel". Consumers call getTunnelService(runtime) and stay backend-agnostic.
ConnectorAccountProvider — registered into ConnectorAccountManager at init(); surfaces multi-account Tailscale config through the standard connector account CRUD surface. Provider id: "tailscale".
Layout
src/
index.ts Plugin definition, init(), all public re-exports
types.ts Re-exports ITunnelService, TunnelStatus, TunnelProvider from @elizaos/plugin-tunnel; adds TailscaleBackendMode
environment.ts validateTailscaleConfig() — reads + validates all env vars via zod
accounts.ts Multi-account config: readTailscaleAccounts(), resolveTailscaleAccount(), TailscaleAccountConfig
accounts.test.ts
connector-account-provider.ts createTailscaleConnectorAccountProvider() — adapts accounts.ts to ConnectorAccountManager
connector-account-provider.test.ts
providers/
tailscale-status.ts tailscaleStatusProvider — injects tunnel status into LLM context
services/
TunnelBackendSelector.ts readBackendMode(), selectTunnelBackend() — chooses LocalTailscaleService or CloudTailscaleService
LocalTailscaleService.ts Drives local `tailscale` CLI; requires tailscale installed + authed
CloudTailscaleService.ts Mints cloud auth key, joins tailnet, runs serve/funnel
CloudTailscaleService.test.ts
__tests__/
TailscaleTestSuite.ts elizaOS test suite class (used in plugin.tests)
Commands
All scripts defined in package.json:
bun run --cwd plugins/plugin-tailscale build # tsup ESM build + .d.ts
bun run --cwd plugins/plugin-tailscale typecheck # tsgo --noEmit
bun run --cwd plugins/plugin-tailscale test # vitest run
bun run --cwd plugins/plugin-tailscale test:watch # vitest watch
bun run --cwd plugins/plugin-tailscale test:coverage # vitest --coverage
bun run --cwd plugins/plugin-tailscale lint # biome check --write --unsafe
bun run --cwd plugins/plugin-tailscale lint:check # biome check (no write)
bun run --cwd plugins/plugin-tailscale format # biome format --write
bun run --cwd plugins/plugin-tailscale format:check # biome format (no write)
bun run --cwd plugins/plugin-tailscale clean # rm -rf dist coverage
Config / env vars
Config resolution order for each key: account record in character.settings.tailscale.accounts → TAILSCALE_ACCOUNTS JSON setting → flat runtime setting → process.env.
| Var | Default | Notes |
|---|---|---|
TAILSCALE_BACKEND |
auto |
local / cloud / auto. In auto, picks cloud if isCloudConnected(runtime) returns true. |
TAILSCALE_AUTH_KEY |
— | Optional pre-minted auth key (local backend). |
TAILSCALE_TAGS |
tag:eliza-tunnel |
Comma-separated ACL tags for cloud-minted keys. |
TAILSCALE_FUNNEL |
false |
Truthy: use tailscale funnel (public Internet). Falsy: use tailscale serve (tailnet-only HTTPS). |
TAILSCALE_DEFAULT_PORT |
3000 |
Fallback port when none is specified. |
TAILSCALE_AUTH_KEY_EXPIRY_SECONDS |
3600 |
Expiry hint for cloud auth-key minter. |
TAILSCALE_ACCOUNTS |
— | JSON array or object of TailscaleAccountConfig records for multi-account setups. |
TAILSCALE_DEFAULT_ACCOUNT_ID |
default |
Which account entry to use when multiple are configured. |
ELIZAOS_CLOUD_API_KEY |
— | Required for cloud backend. |
ELIZAOS_CLOUD_BASE_URL |
https://api.elizacloud.ai/api/v1 |
Override cloud base URL. |
How to extend
Add a new provider:
- Create
src/providers/<name>.tsexporting aProviderobject. - Import and add it to the
providersarray insrc/index.ts.
Add an action:
- Create
src/actions/<name>.tsexporting anActionobject. - Import and add it to the
actionsarray insrc/index.ts.
Add a new tunnel backend service:
- Implement
ITunnelService(from../types) and extendServicefrom@elizaos/core. Setstatic serviceType = "tunnel". - Add the new class as an option in
src/services/TunnelBackendSelector.ts. - Export the class from
src/index.ts.
Conventions / gotchas
- First-active-wins tunnel slot. Only one service can hold
serviceType="tunnel". If another plugin (e.g.@elizaos/plugin-ngrok) registers first, this plugin'sinit()logs the conflict and leaves the existing tunnel service in place. Enable only one tunnel plugin at a time. tailscalebinary required at runtime.LocalTailscaleService.start()throws ifwhich tailscalefails. The cloud backend also requires the local CLI fortailscale up/serve/funnel/logout.- No actions registered here. All start/stop/status user interactions go through
@elizaos/plugin-tunnel's canonicalTUNNELaction. - Cloud backend billing. Each
startTunnel()call onCloudTailscaleServicemakes a POST to Eliza Cloud that debits org credits.getLastProvisioningBilling()returns the billing payload from the last provision. - Multi-account config.
TailscaleAccountConfigcan be declared incharacter.settings.tailscale.accounts(array or keyed object) or viaTAILSCALE_ACCOUNTSJSON. Per-account fields mirror the flat env vars — seesrc/accounts.tsfor field aliases. - Peer dep on
@elizaos/plugin-tunnel.getTunnelService,ITunnelService,TunnelStatus, andtunnelSlotIsFreeall come from there — do not duplicate them here.
⛔ NON-NEGOTIABLE — evidence, trajectories & real end-to-end tests
The binding, repo-wide standard is AGENTS.md. Read it. Nothing in this package is done until it is proven done — a reviewer must confirm it works without reading the code, from the artifacts you attach. This applies to every feature, fix, refactor, and chore here. "Tests pass" is not proof; "CI is green" is not proof.
- Record AND read model trajectories. Capture the actual inputs and outputs of the model
from a live LLM — not the deterministic proxy, not a mock: the prompt, the
providers/context, the raw model output, every tool/action call, and the result. Then open
the trajectory and review it by hand. A captured-but-unread trajectory is not evidence
(
packages/scenario-runner/bin/eliza-scenarios run <scenario> --report <out>). - Real, full-featured E2E — no larp. Every feature ships detailed end-to-end tests that drive the real path end to end. Not the happy "front door" only: cover error paths, edge/empty/invalid input, concurrency, roles/permissions, and adversarial input. A test that asserts against a mock/stub/fixture standing in for the thing under test does not count. If the real model/device/chain/connector/account is hard to reach, make it reachable — that is the work, not an excuse to mock. If the existing tests here are shallow or mocked, fixing them is part of your change.
- Screenshots + logs at every phase, plus a complete walkthrough video/run-through of
the entire feature or view, start to finish (
bun run test:e2e:record). - Manually review every artifact the change touches — never just the green check: client
logs (console + network), server logs (
[ClassName] …), the model trajectories in and out, before/after full-page screenshots, and the domain artifacts listed below for this package. - No residuals. No shortcuts. The goal is not "done" — it is everything done. Clear every blocker by the hard path: build the real architecture, stand up the real model/device/service, actually test it. Never leave a TODO, a stub, a stepping-stone, or a "follow-up." When unsure, research thoroughly, weigh the options, and ship the best, highest-effort, production-ready version. Keep going until every possibility is exhausted.
Artifacts → attached inline in the PR (MP4 video, JPG screenshots, logs in <details>); attach each evidence type or
explicitly mark it N/A with a reason — never leave it blank. If develop moved and changed
behavior, re-capture evidence; stale proof is worse than none.
Capture & manually review for this package — CLI / tooling:
- The real command/flow invocation transcript (args in, stdout/stderr, exit code) and the artifacts it generated (files, scaffolds, manifests, screenshots/recordings).
- Failure paths: bad args, missing deps, partial state, permission/network errors.
- A recording/log of the actual run end to end — not a unit test of one helper.
- Any model interaction captured as a live trajectory and reviewed.