Skip to main content
When an agent runs npm run dev or starts a service inside its container, the operator wants a clickable URL to open it. Port expose creates a capability URL — https://<host>/exposed/<token> — that reverse-proxies to the in-container port. The token is the auth: anyone with the URL gets through, anyone without it gets a 404. Exposes are created by the sidecar on the agent’s behalf, then listed and revoked through the user-facing crew endpoints. This is the right shape for ephemeral dev URLs: short-lived, easy to share with a teammate via Slack, no JWT plumbing needed. For long-lived or sensitive surfaces, do not use this — set up a proper authenticating proxy. Implementation: internal/api/port_expose_handler.go. Capability URLs hit Go 1.22+ ServeMux with /exposed/{token} and /exposed/{token}/ (trailing slash and bare-token forms both routed to the same handler).

Endpoints


Capability URL — reverse proxy

No auth header. The token IS the auth; presenting a valid, non-revoked, non-expired token yields proxied traffic to the registered in-container port. All HTTP verbs are forwarded (GET / POST / PUT / DELETE / PATCH / HEAD / OPTIONS) so static sites and REST APIs work. WebSocket upgrades are not supported. A request carrying Upgrade: websocket is rejected with 426 Upgrade Required (websocket not supported); the exposed-port proxy only handles plain HTTP request/response traffic. Errors: There is no rate limit on the capability URL — it forwards as fast as the underlying TCP connection allows.

Container runtime requirements

The capability URL is proxied by crewshipd itself, which dials the crew container on its Docker bridge IP (http://172.19.0.2:3000). No host port is ever published — that is the design intent — so the proxy works only when the process running crewshipd can route to the container network directly.
On an affected runtime the expose is still created and the capability URL is still handed to the agent — nothing in token handling, policy, or IP resolution is wrong. Every request to it then returns 502, because the packets have nowhere to go. There is no setting that fixes this: either use a runtime from the “Works” rows, or run crewshipd where it shares a network namespace with the crew containers.
services: sidecars are unaffected on every runtime — agent↔sidecar traffic never leaves the container network. Only the host-facing /exposed/{token} proxy needs host→container routability.

Sidecar request

How a new capability URL is minted — the agent never calls this directly.
Internal only — X-Internal-Token required. The agent asks the sidecar (e.g. via a tool call), the sidecar asks the server. Agents cannot reach this endpoint directly.
Request body:
Response: 201 Created
The sidecar surfaces the URL back to the agent, which can include it in its tool result. The operator sees it in the chat and clicks through.

User-facing audit + revocation

List

Lists every active port-expose for a crew. Workspace-scoped. By default only ACTIVE exposes are returned. Filter with ?status=active|revoked|expired|all. Response: 200 OK — a bare JSON array (not wrapped in an exposes object).
The token (and thus the public url) is intentionally not included — only the issuing agent receives the capability URL.

Revoke

Marks the expose as revoked; the next request to its capability URL returns 404. Requires MANAGER+ (the create permission). Not idempotent — a second revoke of the same expose returns 409 Conflict because the conditional update only matches ACTIVE/PENDING rows. Request body (optional):
reason is optional (max 500 chars) and recorded on the audit row. Response: 200 OK

Tenancy and policy

  • crewId is validated via crewBelongsToWorkspace.
  • An AllowAllPolicy runs on every capability-URL request (currently always returns allow). Custom policies can be wired by replacing the policy object — useful for restricting expose to specific IP ranges or to authenticated browser sessions.
  • The capability URL itself has no workspace context (it’s a public URL). The token-to-port lookup happens server-side and is workspace-isolated by construction.

Security notes

  • Tokens are 256 bits of crypto/rand. Brute-forcing a valid token via the 404 oracle is not feasible.
  • Tokens land in URL paths, which means they end up in HTTP server logs and browser history. Do not paste a capability URL into a public chat or screenshot it without thinking.
  • TLS termination is the operator’s responsibility — Crewship does not currently terminate HTTPS for /exposed/. Run behind nginx / Caddy / a cloud load balancer that does.
  • Revocation is fast but not transactional with in-flight requests. A request that is mid-stream when revoke fires completes; subsequent requests fail.