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 URLhttps://<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.

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.