tk-link — dev.tktk.link ========================= The backbone is a reverse tunnel: backend services open persistent outbound connections to the backbone and have client connections handed to them through one. Clients reach a service at .dev.tktk.link. Everything authenticates with a TLS 1.3 CLIENT CERTIFICATE. You generate a keypair, self-sign it, and register the public half; the private key never leaves your machine and the backbone stores nothing that could be leaked or replayed. There is no bearer string, no header to copy, no signed URL. FOUR HOSTNAMES, because a certificate is demanded during the HANDSHAKE and that decision is made per SNI name, before any request exists. Every path is served on exactly one of them; the wrong host answers 421, never a redirect. api.dev.tktk.link THE API. Everything programmatic except registration: the management calls below (client certificate) and the admin calls (Bearer token). The handshake asks for a certificate and lets you decline — declining is how the admin token works — so a call that needs to know who you are and got no certificate answers 401. provision.dev.tktk.link REGISTRATION, and nothing else. The one place a machine with no registered key can be heard: it accepts a certificate the backbone has never seen, where every other name refuses an unknown key inside the handshake, before HTTP exists. Nothing reachable here grants anything, which is why it is a name of its own. dev.tktk.link The console, for humans in a browser. NO certificate is requested at all — a browser prompted for one is a dead end. Sign-in, `/healthz` and this page live here; `/v1/*` does not. models.dev.tktk.link THE LLM GATEWAY, per user. An OpenRouter-compatible API: point your client's base_url at https://models.dev.tktk.link/api/v1 and connect WITH your certificate — REQUIRED in the handshake here, like any service. Your first request mints you your own upstream key and your own api service behind the scenes; every request after rides them, so usage is YOURS (GET /api/v1/key shows it). No grant to ask for: the certificate is the grant. See FOR LLM CLIENTS below. openai.models.dev.tktk.link THE SAME THING, straight to OpenAI. base_url https://openai.models.dev.tktk.link/v1 and your certificate; your first request mints you your own OpenAI key. Independent of models.dev.tktk.link: a separate key, a separate service, separate usage — and either host may be configured without the other. gemini.models.dev.tktk.link GEMINI, through the Gemini Developer API. Your path travels verbatim, so address it exactly as you would Google's own endpoint: POST https://gemini.models.dev.tktk.link/v1beta/interactions That ONE endpoint is the whole surface today — not :generateContent, not the model list. Google offers no way to create a Gemini key programmatically, so nothing is minted for you here: every caller goes out on one shared key, and reading interactions back is refused precisely because that key makes everyone the same consumer upstream. Model names are Google's and they ROT: an older one answers 404 with "no longer available to new users", which reads like a bad path but is not. gemini-flash-latest and gemini-pro-latest do not rot. Each gateway serves an ALLOWLIST of endpoints: inference and model metadata, nothing that stores or reads back server-side state (no /v1/responses, /v1/files, /v1/vector_stores, /v1/batches, and no reading stored completions). Anything else is 404 endpoint_not_allowed. Your key lives in a shared upstream project, and reads there are not tenant-scoped — so the endpoints that could hand you someone else's content are simply not reachable. ── 0. GET AN IDENTITY (once) ─────────────────────────────────────────────── 1. Generate a keypair and self-sign a certificate. Compute the fingerprint the backbone keys you on: openssl x509 -in cert.pem -pubkey -noout \ | openssl pkey -pubin -outform DER \ | openssl dgst -sha256 2. Ask a human to recognise it — CONNECTING WITH THAT CERTIFICATE: curl --cert cert.pem --key key.pem \ -X POST https://provision.dev.tktk.link/v1/principals/requests \ -d '{"name": "laptop"}' There is no key in the body and none is read from one: the fingerprint registered is the one taken off this TLS connection. Completing the handshake signs it with your private key, so it proves possession — whereas a public key in a request body proves nothing, and could be anyone's. Connect without a certificate and you are told to bring one. Response: an authorize_url, a short user_code, and a poll_secret. 3. Show the user_code, then hold a long poll open: GET https://provision.dev.tktk.link/v1/principals/requests/ x-principal-request-secret: {"status": "pending"} means "reconnect immediately". On approval you get {"status": "approved", "principal": {...}} — your principal UUID. Nothing in that response is a secret. Open the authorize_url while signed in, check the code matches, approve. The principal is owned by whoever approves it: it appears in their console and they can revoke it. On an embedded device whose OWN browser opens that link, append ?embedded=1: the approval page then closes its tab on success instead of linking to a console the device cannot use. Best-effort — a normal desktop tab ignores the scripted close and just shows a "you can close this tab" button. ── 0a. SIDECARS (if your tool is not certificate-aware) ──────────────────── aws-cli, Litestream, rclone, curl and most app HTTP clients do not speak mTLS. Run the sidecar from sdk/go instead: it owns the keypair, exposes a plain unauthenticated localhost endpoint, and attaches the certificate on the way out. Your tool's config becomes http://127.0.0.1:. ── FOR LLM CLIENTS (the models gateways) ────────────────────────────────── Three upstreams, one contract. Nothing to create and no grant to request — your certificate is the key: curl --cert cert.pem --key key.pem https://models.dev.tktk.link/api/v1/key curl --cert cert.pem --key key.pem https://openai.models.dev.tktk.link/v1/models curl --cert cert.pem --key key.pem -H 'content-type: application/json' -d '{"model":"gemini-flash-latest","input":"hi"}' https://gemini.models.dev.tktk.link/v1beta/interactions The first call provisions: your own upstream key (minted for you, sealed, never shown) and a kind=api service you OWN that carries it — it appears in GET /v1/services like any service, and deleting it just means the next call provisions a fresh one. That call and every one after are proxied to the LLM upstream with YOUR key, so usage is attributed to you; GET /api/v1/key reports it. Any OpenAI-compatible client works: base_url https://models.dev.tktk.link/api/v1 and any api_key string at all (it is stripped — the certificate is what authenticates), or through the sidecar (§0a) for tools that cannot present a certificate. 503 models_not_configured means this deployment has no LLM upstream configured. openai.models.dev.tktk.link works exactly the same way against OpenAI itself (base_url https://openai.models.dev.tktk.link/v1). Your key there is a per-user OpenAI service account, so your usage is attributed to you at OpenAI — but note OpenAI returns only token counts, never a cost, so there is no equivalent of /api/v1/key to ask what you have spent. gemini.models.dev.tktk.link is the same contract with two differences you can see. Nothing is minted for you — Google has no create-key API, so every caller shares one key and there is no per-user usage figure to report. And it serves exactly ONE endpoint, POST //interactions; anything else, model list included, is 404 endpoint_not_allowed. Reading interactions back is refused on purpose: with one shared key upstream, a list or a fetch by id would return whatever anyone stored. Your body and path are forwarded byte-for-byte, so point a google-genai client at this host as its base URL, or just POST the JSON yourself. The three hosts are independent: one answering 503 says nothing about the others. ── FOR BACKEND SERVICES ──────────────────────────────────────────────────── 1. Create a service you own POST https://api.dev.tktk.link/v1/services (client certificate) Optional body: {"name": ""} — a non-unique label shown in the console so you can tell services apart. Response: {"service": "", "name": ""} — services are addressed by UUID: clients reach yours at https://.dev.tktk.link/. (The owner is auto-granted access to its own service.) Rename later: PATCH https://api.dev.tktk.link/v1/services/ {"name": "..."} (owner only; blank or null clears it). 2. Park connections for it (the "agent") See AGENT PARK PROTOCOL below. Connecting IS listening: each parked connection is one slot, and keeping the pool full is your job. 3. What your backend receives A claimed connection begins with ONE PROXY protocol v2 frame carrying the real client address and a custom TLV (type 0xE0) holding the caller's principal UUID. Everything after the frame is the client's bytes, verbatim — the backbone does not parse, rewrite or frame anything above TCP, so a tunnel carries HTTP/1, HTTP/2, WebSockets, gRPC, Postgres wire protocol or anything else. You MUST refuse a connection that does not begin with a valid PROXY v2 frame, and you MUST be reachable only from the backbone. The TLV is an unauthenticated assertion; its trust is your network isolation plus the fact that the backbone only ever emits values it verified. 4. List what you can reach GET https://api.dev.tktk.link/v1/services (client certificate) Response: {"owned": [...], "accessible": [...]} — "owned" is every service whose owner is you; "accessible" is every service you hold a grant on but do NOT own. Each entry carries its public "url", its "name", its "kind" and a live "connected" flag (true when the URL answers right now). On "accessible" entries "kind" is "bucket" or "gateway" — the same pair you declare when requesting access; tunnel and api services are both gateways and are not told apart. "owned" entries carry the registry's kind ("tunnel", "api", "bucket"): what is behind your own service is your business. Scoped to your certificate: there is no way to enumerate services you neither own nor were granted. 5. Delete a service you own DELETE https://api.dev.tktk.link/v1/services/ (client certificate) 204 on success. Owner only: 403 for any other principal, 404 for an unknown service. ── AGENT PARK PROTOCOL (v3 — reverse connection pool) ────────────────── There is NO control plane. A backend connects in order to listen: the connection IS the listen slot. Nothing pushes commands at you, because everything a control channel used to say is something you can already see on your own sockets. 1. Dial TLS to dev.tktk.link:443 with SNI agents.dev.tktk.link, presenting your client certificate. SNI routes; the certificate authenticates. (The service cannot ride the SNI — the wildcard certificate is one label deep — which is the only reason a preamble still exists.) 2. Write ONE preamble frame, then stop talking "TKLK" + + + json (≤ 4096 bytes): {"service": "", "conn_id": ""} Routing only. Nothing in it authenticates anything. 3. Read the ack frame + + 0x01 parked — stay SILENT until claimed; body carries {"idle_timeout_ms": N, "slot_capacity": N} 0x00 denied — not authorized to serve this service (do not retry) 0x02 degraded — service registry still cold; retry shortly 0x03 no_slots — the service is at capacity; back off and retry Non-ok statuses carry an empty body (json_len = 0). Advertised limits right now: idle_timeout_ms 55000, slot_capacity 64. Read them from the ack — never hard-code them. 4. Serve a claimed connection A parked connection goes quiet until a client arrives. Then the PROXY v2 frame lands (see FOR BACKEND SERVICES §3) and it is raw bytes from there. Nothing tells you a slot was claimed except the bytes themselves — so when one is claimed or closed, open a replacement. That is the whole pool-management protocol. Multiple backends may park for the same service and claims spread across them, so a deploy is: bring the new one up, let the old one finish its own connections, exit. TLS 1.3 trap: :ssl / crypto/tls returns a connected socket BEFORE the server has checked your certificate. A rejection surfaces on your first READ, so never treat "connected" as "authenticated" — read the ack first. Full spec: docs/PROTOCOL.md in the repo (kept in sync with the code by a doc test). The Go SDK under sdk/go is the reference implementation — and the only one: agent in sdk/go/agent.go, sidecar in sdk/go/sidecar.go, driven by sdk/go/cmd/tk-agent and sdk/go/cmd/tk-sidecar. ── FOR CLIENTS ──────────────────────────────────────────────────────────── 1. Get an identity (section 0 above). 2. Get access to a service The service OWNER grants your principal_id access: POST https://api.dev.tktk.link/v1/services//grants (owner's client certificate) {"principal_id": ""} or you request access yourself and an admin approves the request: POST https://api.dev.tktk.link/v1/access-requests (client certificate) {"service": "", "kind": "gateway"} "kind" is REQUIRED and must match the service: "bucket" for an S3 bucket (its grants carry data capabilities), "gateway" for anything you send requests through. A missing or non-matching kind is a 422 — you must not end up holding bucket access you believed was a gateway grant, or the reverse. There is no third value: every non-bucket service is a gateway to callers, whatever fulfils it. Response: {"request_id": "...", "user_code": "WDJB-MJHT", "kind": "gateway|bucket", "approve_url": "https://dev.tktk.link/access/", ...} Then get it decided, either way round: * Send approve_url to whoever owns the service, however you like. It is a browser page: a signed-in OWNER of the service gets Approve/Deny, and every other viewer — you included — gets the same request details read-only with its current status. Holding the link decides nothing, so it is safe to pass on, and both of you watch the same page. * Or quote the user_code to an admin. They list what is outstanding (GET /v1/admin/access-requests), match the code, and approve or deny with the admin token — so they never have to act on an identifier a stranger sent them. Then long-poll your own request until it is decided: GET https://api.dev.tktk.link/v1/access-requests/?wait=25 (client certificate) {"status": "pending"} means "reconnect immediately". The client certificate authorizes the poll, so nobody else can read the outcome; there is no poll secret and no signed URL. Changed your mind? DELETE https://api.dev.tktk.link/v1/access-requests/ (client certificate) The grant is written for the principal that asked — the certificate the request was made with. Requests are stored, single-use (a second approval is a 409), withdrawable, and expire on their own after 24h. (Admins can also grant directly: POST /v1/admin/grants.) 3. Call a service Open a TLS connection to .dev.tktk.link:443 presenting your certificate. The backbone resolves you from the certificate, checks your grant, and connects you. There is no identity header to send and none is accepted: any x-tk-* or x-backbone-* header you send is stripped before anything upstream sees it. No certificate, or no grant, is a 401. 4. Optional: object storage (S3-compatible) Create a bucket. A bucket IS a service: it gets a UUID that is its .dev.tktk.link subdomain, appears in your service list, and is shared by GRANTING principals access to it — exactly like a tunnel. POST https://api.dev.tktk.link/v1/storage-buckets (client certificate) {"name": "primary db"} Response: {"bucket": "", "url": "https://.dev.tktk.link", "endpoint": "https://storage.dev.tktk.link", "region": "auto"} Reach the data plane two ways: - virtual-host — https://.dev.tktk.link/ - path-style — https://storage.dev.tktk.link// Identity is purely the certificate. An inbound Authorization: AWS4-... is TOLERATED and ignored: S3 tooling that demands a keypair can put any value in it (the bucket UUID is a convenient dummy), and anonymous requests are fine too. Neither grants anything. Point the tool at a sidecar and it needs to know nothing about any of this. Share it by granting another principal, with capabilities: POST https://api.dev.tktk.link/v1/services//grants {"principal_id": "", "access_mode": "ro", "key_prefix": "logs/"} access_mode is "ro" (read-only) or "rw" (default); key_prefix scopes a read-only grant to keys under that prefix. Read-only / prefix grants are enforced at the storage layer itself. The owner is auto-granted full. Real dedicated bucket — copy, multipart, etc. all work; request bodies stream (no gateway object-size limit). SSE-C (customer-provided keys, x-amz-server-side-encryption-customer-*) is supported on every operation; SSE-S3 / SSE-KMS headers are rejected with 400, never silently ignored. GET /v1/storage-buckets lists yours; DELETE /v1/storage-buckets/ revokes it (and revoking your principal disables all of them). 5. Optional: API services (credential-injecting upstreams) Register a service whose upstream is an external API. You supply the upstream base URL, the header(s) the secret goes into, and the secret itself — a static value, or OAuth client creds + a refresh token. The backbone SEALS it per-principal (Cloud KMS) and injects it into every authorized request, so callers never hold the raw secret. POST https://api.dev.tktk.link/v1/services (client certificate) {"kind":"api","name":"stripe", "upstream":{ "base_url":"https://api.stripe.com", "headers":[{"name":"Authorization","value":"Bearer {{token}}"}], "auth":{"kind":"static","secrets":{"token":"sk_live_..."}}}} The header value is a template: {{token}} (or any secret name you define) is replaced with the sealed secret, so "Bearer {{token}}", "Custom {{token}}", or a bare "{{token}}" in any header all work. For OAuth, register client creds + a refresh token and the backbone refreshes on your behalf, injecting the reserved {{access_token}}: "auth":{"kind":"oauth","token_url":"https://.../oauth/token", "client_auth":"basic","client_id":"...","client_secret":"...", "refresh_token":"..."} An API service IS a service: same ownership and grants as a reverse-proxy service. Reach it ONLY at its subdomain — there is no shared-domain path proxy. The secret is injected server-side and the response is relayed verbatim: https://.dev.tktk.link/ (client certificate) GET /v1/services//upstream shows the secret-free config; PATCH the same path rotates it (owner-only). Share access with the usual POST /v1/services//grants. 6. Optional: collect a credential from a human (embedded setup) When a DEVICE is building an api service but does not have the credential yet, it can ask a human for it instead of holding a secret. The device posts a SPEC of what to collect — it does not create the service: POST https://api.dev.tktk.link/v1/services/setup-requests (client certificate) {"name":"Stripe","logo_url":"https://.../logo.svg", "base_url":"https://api.stripe.com", "instructions":"Paste a restricted key.", "fields":[{"name":"token","label":"Secret API Key", "instructions":"Create one in the dashboard.", "help_url":"https://dashboard.stripe.com/apikeys"}]} Response: setup_url + user_code + poll_secret + poll_url. headers default to "Authorization: Bearer {{}}" for a single field; supply your own headers (same template as above) for anything else. The device shows the user_code and opens (or hands off) the setup_url: https://dev.tktk.link/setup/ ← a human fills this in The webview shows the logo, the labelled fields with their instructions and a "how to generate this" link (opens in a new tab), and the user_code to confirm it matches the device. On submit, the backbone mints the api service owned by the REQUESTING device (ownership was fixed by the certificate that opened the request, never by who fills the form) and seals the collected values. Meanwhile the device long-polls: GET https://api.dev.tktk.link/v1/services/setup-requests/ x-setup-request-secret: pending until a human finishes, then {"status":"completed","service":..., "url":...} exactly once. Nothing is created until setup completes. ── QUICK REFERENCE ───────────────────────────────────────────────────────── Grouped by HOST, because the host is half of the address: the same path on the wrong one is a 421, not a redirect. Everything programmatic is under /v1 — that prefix is the compatibility promise, and it is the only place a breaking change is allowed to appear. https://provision.dev.tktk.link POST /v1/principals/requests client cert register THIS connection's key (human approves) GET /v1/principals/requests/:id poll secret long-poll until the key is registered https://api.dev.tktk.link POST /v1/services client cert create a service you own (tunnel, or kind=api) GET /v1/services client cert list services you own + can access POST /v1/services/setup-requests client cert start an embedded credential-collection flow (kind=api) GET /v1/services/setup-requests/:id setup secret long-poll until a human finishes; the service is delivered DELETE /v1/services/setup-requests/:id setup secret abandon a pending setup request PATCH /v1/services/:service client cert owner sets the service's name DELETE /v1/services/:service client cert delete a service you own GET /v1/services/:service/upstream client cert owner views api upstream config (secret-free) PATCH /v1/services/:service/upstream client cert owner replaces api upstream config / rotates creds POST /v1/services/:service/grants client cert owner grants a principal access DELETE /v1/services/:service/grants/:pid client cert owner revokes a grant POST /v1/access-requests client cert request access at your cert's scope; body declares kind "bucket"|"gateway" (returns approve_url) GET /v1/access-requests/:request_id client cert long-poll your own request until it is decided DELETE /v1/access-requests/:request_id client cert withdraw your own pending request POST /v1/storage-buckets client cert mint an S3 bucket GET /v1/storage-buckets client cert list your S3 buckets DELETE /v1/storage-buckets/:bucket client cert revoke an S3 bucket GET /v1/admin/services admin token list services (+ park status) GET /v1/admin/cluster admin token cluster node status POST /v1/admin/principals admin token register a public key directly GET /v1/admin/principals admin token list principals DELETE /v1/admin/principals/:pid admin token revoke a principal POST /v1/admin/grants admin token grant principal → service DELETE /v1/admin/grants/:pid/:service admin token revoke a grant GET /v1/admin/grants/:pid admin token list grants for principal GET /v1/admin/access-requests admin token list pending requests (?service= to filter) GET /v1/admin/access-requests/:id admin token show one pending access request POST /v1/admin/access-requests/:id/approve admin approve it (writes the grant; single-use) POST /v1/admin/access-requests/:id/deny admin deny it (no grant; cannot be approved after) POST /v1/admin/drain admin token begin draining this node GET /v1/admin/drain admin token drain status https://dev.tktk.link GET / none this page GET /healthz none health check GET /console sign-in the browser console (humans) GET /access/:request_id none an access request: status for anyone, decision for the service owner POST /access/:request_id[/deny] sign-in decide it (service owner only) GET /setup/:request_id user_code the embedded webview a human fills in to finish a service ──────────────────────────────────────────────────────────────────────────── JSON? Send Accept: application/json