Install
$ agentstack add skill-inkbox-ai-inkbox-inkbox-tunnels ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
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 →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-key → INKBOX_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
0700under 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 desiredtunnel.tls_mode. - Identity-delete cascades. Deleting an identity removes its tunnel and revokes its scoped API keys.
forward_tois loopback-only by default. Passallow_remote_forwarding=Trueonly 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 referenceinkbox-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.
- Author: inkbox-ai
- Source: inkbox-ai/inkbox
- License: MIT
- Homepage: https://inkbox.ai/docs
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.