AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Inkbox Tunnels

skill-inkbox-ai-inkbox-inkbox-tunnels · by inkbox-ai

Use when bringing a local server online behind a public Inkbox URL via `inkbox.tunnels.connect(...)` — covers edge vs passthrough TLS, URL forwarding, and in-process Fetch/ASGI/WebSocket handlers in both Python and TypeScript SDKs.

No reviews yet
0 installs
0 views
view→install

Install

$ agentstack add skill-inkbox-ai-inkbox-inkbox-tunnels

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-inkbox-ai-inkbox-inkbox-tunnels)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Inkbox Tunnels? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Inkbox Tunnels

Inkbox Tunnels expose a process running on the developer's machine (or any POSIX host) at a public https://{handle}.inkboxwire.com URL. The SDK opens an outbound HTTP/2 connection to the data plane; inbound third-party traffic rides back over that same connection. No inbound port to open, no static IP needed.

Tunnels are an identity property: every agent identity owns exactly one tunnel, and tunnel_name == agent_handle. There is no standalone "create tunnel" call — the tunnel is provisioned atomically by inkbox.createIdentity(...). Pre-existing identities created before this rollout already have tunnels (the migration backfilled them).

Two TLS modes:

| Mode | Who terminates TLS | When to pick | |---|---|---| | edge (default) | Inkbox terminates at the edge, forwards plaintext requests as envelopes | Most apps. Simpler. The SDK sees parsed Requests / ASGI scopes. | | passthrough | The SDK terminates TLS in your process | When customers must speak directly to your cert (mTLS, custom SNI), or when end-to-end encryption is a hard requirement. |

Both modes accept either a forward_to URL (proxy to a local HTTP server) or an in-process callable (Fetch handler in TS, ASGI app in Python). WebSocket upgrades work on either.

Platform: tunnels require POSIX. connect() raises on Windows; read-only control-plane calls work everywhere.


Authentication

The data plane authenticates with the same API key the SDK client was constructed with. Two options:

  • Admin-scoped key — can connect any tunnel in the org.
  • Identity-scoped key — only valid for the tunnel attached to its scoped identity.

For a fresh agent bootstrap, mint an identity-scoped key after createIdentity:

identity = inkbox.create_identity("my-app")
scoped_key = inkbox.api_keys.create(label="my-app agent", scoped_identity_id=identity.id)
# Hand `scoped_key.api_key` to the agent process. Its `Inkbox(api_key=...)`
# will use it for both REST and the tunnel data plane.

The legacy per-tunnel connect_secret is gone — there is nothing to print, persist, or rotate per tunnel. To revoke a tunnel's access, revoke the API key.

The key is resolved in order: the api_key= argument → the INKBOX_API_KEY env var → a ~/.inkbox/config file (api_key = ...). The file fallback is handy for background/agent processes that don't inherit the shell's env. The CLI resolves the same way (--api-keyINKBOX_API_KEY~/.inkbox/config).


Python

Install

pip install inkbox

> macOS note: the python.org installer doesn't hook into the system keychain, so TLS to the data plane can fail with SSL: CERTIFICATE_VERIFY_FAILED. The SDK now falls back to certifi's CA bundle automatically; if you still hit it, set export SSL_CERT_FILE=$(python -m certifi).

Forward to a local URL (edge mode)

from inkbox import Inkbox

with Inkbox(api_key="ApiKey_...") as inkbox:
    # Identity (with tunnel) must already exist — create it once:
    inkbox.create_identity("my-app")  # idempotent if you catch HandleUnavailableError

    listener = inkbox.tunnels.connect(
        name="my-app",
        forward_to="http://127.0.0.1:8080",
        # on_status fires "connecting" -> "connected" -> "reconnecting" -> "closed"
        on_status=lambda s: print("tunnel:", s),
    )
    print(listener.public_url)   # https://my-app.inkboxwire.com
    listener.wait()              # blocks; Ctrl-C to stop

Subsequent runs read ~/.inkbox/tunnels/{name}/state.json for cached tunnel-id / zone / public-host. The state file no longer holds a secret.

Forward to an in-process ASGI app (FastAPI, Starlette, …)

from fastapi import FastAPI
from inkbox import Inkbox

app = FastAPI()

@app.get("/hello")
async def hello():
    return {"message": "hi from inkbox"}

with Inkbox(api_key="ApiKey_...") as inkbox:
    listener = inkbox.tunnels.connect(name="my-app", forward_to=app)
    listener.wait()

The runtime drives the ASGI app directly — no socket, no uvicorn needed. WebSocket scopes are supported the same way.

Passthrough TLS

inkbox.create_identity(
    "my-app",
    tunnel={"tls_mode": "passthrough"},
)

listener = inkbox.tunnels.connect(
    name="my-app",
    forward_to="http://127.0.0.1:8080",   # or an ASGI app
)

The tunnel's tls_mode is fixed at create time. In passthrough mode the SDK auto-generates and signs a certificate via the control plane (stored under ~/.inkbox/tunnels/{name}/). The third party connects directly to that cert.

Async usage

import asyncio
from inkbox import Inkbox

async def main():
    with Inkbox(api_key="ApiKey_...") as inkbox:
        listener = inkbox.tunnels.connect(name="my-app", forward_to="http://127.0.0.1:8080")
        try:
            await listener.serve_forever()
        finally:
            await listener.aclose()

asyncio.run(main())

wait()/close() and serve_forever()/aclose() are mutually exclusive — pick one pair.

Control-plane reads + edit

inkbox.tunnels.list()                  # list[Tunnel]
inkbox.tunnels.get("tunnel-uuid")
inkbox.tunnels.update(                 # metadata-only
    "tunnel-uuid",
    metadata={"team": "gtm"},
)
# Passthrough only: sign a CSR
signed = inkbox.tunnels.sign_csr("tunnel-uuid", csr_pem=csr_bytes)

There is no create, delete, restore, force_delete, or rotate_secret here — tunnel lifecycle is owned by create_identity / identity.delete() (which cascades).

Common connect() options

| kwarg | default | notes | |---|---|---| | pool_size | server-decided | parked-intake pool, 1–32 | | state_dir | ~/.inkbox/tunnels/{name} | where state + passthrough cert live | | on_status | None | callback for "connecting" / "connected" / "reconnecting" / "closed" | | allow_remote_forwarding | False | bypass loopback-only allowlist for forward_to (review SSRF first) | | forward_to_verify_tls | True | for https:// upstream forwards |


TypeScript

Install

npm install @inkbox/sdk

Node ≥ 22, POSIX only. The data-plane subpath imports node:http2 / node:tls, so it's loaded from a separate entry — @inkbox/sdk itself stays browser-safe.

Forward to a local URL (edge mode)

import { Inkbox } from "@inkbox/sdk";
import { connect } from "@inkbox/sdk/tunnels/connect";

const inkbox = new Inkbox({ apiKey: process.env.INKBOX_API_KEY! });
await inkbox.createIdentity("my-app");  // once; the tunnel is provisioned atomically

const listener = await connect(inkbox, {
  name: "my-app",
  forwardTo: "http://127.0.0.1:8080",
});
console.log(listener.publicUrl);   // https://my-app.inkboxwire.com
await listener.wait();              // until Ctrl-C / SIGTERM

In-process Fetch-API handler

import { connect, type InkboxHandler } from "@inkbox/sdk/tunnels/connect";

const handler: InkboxHandler = async (req, ctx) => {
  if (new URL(req.url).pathname === "/hello") {
    return new Response(JSON.stringify({ message: "hi" }), {
      headers: { "content-type": "application/json" },
    });
  }
  return new Response("not found", { status: 404 });
};

const listener = await connect(inkbox, {
  name: "my-app",
  handler,
});
await listener.wait();

req is a standard Web Request; ctx exposes signal, forwardedForIp, sniHost, and the read-only envelope.

In-process WebSocket handler

import type { InkboxWsHandler } from "@inkbox/sdk/tunnels/connect";

const wsHandler: InkboxWsHandler = async (ws) => {
  await ws.accept();   // optionally { protocol, headers }
  for await (const msg of ws) {
    await ws.send(typeof msg === "string" ? `echo: ${msg}` : msg);
  }
};

const listener = await connect(inkbox, {
  name: "my-app",
  handler,        // HTTP fallback (any path that isn't a WS upgrade)
  wsHandler,      // every WS upgrade routes here
});
await listener.wait();

wsHandler requires either forwardTo or handler to be set as well — non-WS requests need a destination too.

Passthrough TLS

await inkbox.createIdentity("my-app", {
  tunnel: { tlsMode: "passthrough" },
});

const listener = await connect(inkbox, {
  name: "my-app",
  forwardTo: "http://127.0.0.1:8080",   // or pass `handler` / `wsHandler`
});

Control-plane reads + edit

await inkbox.tunnels.list();
await inkbox.tunnels.get("tunnel-uuid");
await inkbox.tunnels.update("tunnel-uuid", {
  metadata: { team: "gtm" },
});
// Passthrough only:
await inkbox.tunnels.signCsr("tunnel-uuid", { csrPem });

Tunnels are provisioned atomically by inkbox.createIdentity(...); there is no standalone create / delete / restore / forceDelete / rotateSecret surface.

Common connect() options

| option | default | notes | |---|---|---| | poolSize | server-decided | 1–32 | | stateDir | ~/.inkbox/tunnels/{name} | state.json + passthrough cert | | onStatus | — | "connecting" / "connected" / "reconnecting" / "closed" | | allowRemoteForwarding | false | bypass loopback-only allowlist | | forwardToVerifyTls | true | for https:// upstream forwards | | forwardToCaBundle | — | extra CA(s) for upstream TLS verification | | installSignalHandlers | true on main | clean shutdown on SIGINT/SIGTERM |


Server Redeploys

Redeploys of the tunnel service are graceful (SDK ≥ 0.4.8). When the server signals a drain, a long-running listener reconnects make-before-break: it stands up a fresh connection and parks a new intake pool before closing the draining one, so short HTTP requests see no gap and in-flight HTTP replies round-trip across the handoff. The handoff is in-band — on_status does not fire "reconnecting" (that status still means an unplanned drop / cold reconnect).

In-progress WebSocket and passthrough-TCP sessions cannot migrate — the third-party socket lives on the dying task. They end with a typed server_draining close (WS close code 4500) and the peer reconnects onto the new task. Write handlers to reconnect idempotently.

TypeScript — the inbound iterator throws WsServerDraining (a WsClosed subclass with reconnectAdvised = true):

import { WsServerDraining } from "@inkbox/sdk/tunnels/connect";

const wsHandler: InkboxWsHandler = async (ws) => {
  await ws.accept();
  try {
    for await (const msg of ws) {
      await ws.send(msg);
    }
  } catch (err) {
    if (err instanceof WsServerDraining) return;  // peer reconnects onto the new task
    throw err;
  }
};

(SERVER_DRAINING_WS_CLOSE_CODE is also exported.)

Python — the ASGI app receives a normal websocket.disconnect event with code == 4500 instead of a generic reset; clean up and let the peer reconnect.

Operational Notes

  • State dir is sensitive in passthrough mode. It stores the per-tunnel private key. Default is 0700 under the user's home directory; treat it like an SSH key dir. Edge mode keeps only zone/public-host caching there.
  • No secret recovery dance. Data-plane auth is the same API key used for the control plane. Lose the key, mint a new one via inkbox.api_keys.create(...) and revoke the old.
  • TLS mode is fixed at create. Switching between edge and passthrough requires identity.delete() (cascades to the tunnel) + recreating the identity with the desired tunnel.tls_mode.
  • Identity-delete cascades. Deleting an identity removes its tunnel and revokes its scoped API keys.
  • forward_to is loopback-only by default. Pass allow_remote_forwarding=True only after reviewing the SSRF tradeoff.
  • Body caps apply uniformly across URL forward and in-process handlers (defaults: 50 MiB inbound, 50 MiB response). Configurable per connect().

Choosing a Dispatch Path

                       │  edge       │  passthrough
───────────────────────┼─────────────┼─────────────────
  forward_to URL       │     ✅      │       ✅
  in-process callable  │     ✅      │       ✅
  WebSocket upgrades   │     ✅      │       ✅

Pick forward_to URL when you already have a process listening on a port (uvicorn, Express, etc.). Pick the in-process callable when you want to skip the local socket entirely — the runtime drives your handler directly.

See Also

  • inkbox-python — Python SDK reference (mailbox/phone/vault/etc.)
  • inkbox-ts — TypeScript SDK reference
  • inkbox-cli — shell-side workflows

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.