Skip to content

Campus public endpoints and addresses

Status: backend public routes, opaque per-installation addresses, and exact direct UDP publication are implemented; custom domains remain target design.

This feature lets an account deliberately expose selected app traffic to the Internet. HTTP and WebSocket traffic uses a Campus-assigned installation address and approved method/path pairs. Schema-version-4 native protocols may instead request exact direct UDP publication, for which Runtime assigns a host port. Neither path exposes a private launch capability or Gateway ticket.

See the permission model, security model, and account MCP model.

Invariants

  1. An app and its box are private by default.
  2. Public exposure requires both a package declaration and an installation grant.
  3. HTTP/WebSocket exposure is method-and-path scoped. Direct UDP is protocol/container-port scoped, and Runtime—not the app or user—chooses the host port.
  4. An HTTP public address identifies only one installation and contains no account ID. A direct UDP descriptor exposes the operator's Node host and assigned port, not Campus account identity.
  5. Ingress and the installation Gateway independently enforce HTTP/WebSocket routes; Runtime reconciles direct UDP bindings.
  6. Public traffic carries no CampusOS session or private app capability.
  7. A public request cannot exercise the owner's or ordinary Agent session's authority.
  8. Revocation, stop, uninstall, or stale topology closes reachability fail-closed.

Topology

mermaid
flowchart LR
  Client["Internet client"] -->|"HTTPS / assigned hostname"| Ingress["Campus Ingress"]
  Ingress -->|"approved host over Iroh"| Gateway["Installation Gateway"]
  Gateway -->|"approved method + path / fixed internal port"| Backend["App backend"]
  Gateway -->|"explicit public-agent route only"| PublicAgent["Ephemeral PublicInvocation"]
  PublicAgent -->|"one action + restricted profile"| Bridge["Agent Bridge"]
  Native["Native UDP client"] -->|"operator host + assigned UDP port"| Runtime["Node Runtime"]
  Runtime -->|"declared UDP container port"| NativeProtocol["App native protocol"]

Ingress receives hostname-to-Gateway routing, not backend addresses or app capabilities. Gateway receives the exact effective routes and rejects any other host, method, or path.

Backend route declaration

The existing package shape is:

yaml
workload:
  port: 3000

permissions:
  - id: endpoints.public
    resources:
      - GET:/health
      - POST:/hooks/github
      - WS:/events

publicEndpoints:
  - method: GET
    path: /health
  - method: POST
    path: /hooks/github
  - method: WS
    path: /events

Both conditions must hold:

text
declared(publicEndpoints, METHOD, path)
AND granted(endpoints.public, METHOD:path)

An exact route matches only that method and path. A declared trailing /* may match descendants according to ingress rules. WebSocket routes use synthetic method WS. Query strings never participate in authorization. Paths are normalized before matching; encoded separators, dot segments, invalid UTF-8, duplicate/conflicting host headers, and ambiguous paths are rejected.

The app's workload.port is node-internal. It is never included in the public URL and cannot be selected through a permission resource.

Direct UDP declaration

Direct UDP is a separate protocol path for traffic such as WireGuard that cannot traverse Campus HTTP Ingress. It uses the same schema-9 package:

yaml
schemaVersion: 9
workload:
  type: container
  port: 3000

permissions:
  - id: endpoints.public.udp
    resources: [UDP:51820]
    reason: Receive authenticated WireGuard packets.

publicPorts:
  - name: wireguard
    protocol: udp
    containerPort: 51820

# Protocols such as TURN that must know their public port inside the workload use:
# permissions resource UDP:auto and containerPort: auto.

Both conditions must hold:

text
declared(publicPorts, name, udp, container-port)
AND granted(endpoints.public.udp, UDP:container-port)

Only UDP is accepted. Names must match [a-z][a-z0-9-]{0,31} and be unique; fixed protocol/port pairs must be unique; container ports are 1–65535 or auto; and one package may declare at most four direct ports. Runtime assigns a collision-free host port rather than accepting a package-selected host port. With auto, Runtime assigns the same collision-free number to the host and workload port and injects that binding into the workload. A running installation receives:

json
{
  "name": "wireguard",
  "protocol": "udp",
  "containerPort": 51820,
  "host": "vpn.example.net",
  "hostPort": 32791
}

host is the operator-configured CAMPUS_RUNTIME_PUBLIC_HOST. It must be a DNS name or IPv4 address without a scheme or port. The operator remains responsible for forwarding the assigned UDP port through upstream NAT and allowing it through host/cloud firewalls.

Direct UDP does not use the Campus-assigned HTTP hostname, TLS, Ingress, Iroh Gateway, HTTP header filtering, or Campus client authentication. The application protocol is responsible for cryptographic peer authentication, confidentiality, replay handling, rate/DoS resilience, and safe parsing. WireGuard supplies the cryptographic channel in the bundled example. Stopping the app, revoking the grant, removing the declaration, or uninstalling reconciles the managed binding closed.

Three authorities remain independent:

  • endpoints.public.udp(UDP:<port>) authorizes inbound direct publication;
  • runtime.network-tunnel(*) authorizes only fixed NET_ADMIN plus /dev/net/tun container setup;
  • network.public(*) authorizes broad app egress.

None implies either of the others. Runtime implements direct ports for Docker/container and Firecracker; the Firecracker adapter uses scoped DNAT/FORWARD policy for each declared port. Tunnel-device policy remains Docker-only.

Address assignment

Campus reserves one stable opaque ID when it creates an installation and returns the base address when that installation has at least one effective public route:

text
https://<app-id>--<opaque-public-id>.<campus-app-domain>

Example:

text
https://campus-notes--pub-7a4f09c18d234ef69a1b43cd.apps.campus.example/hooks/github

Rules:

  • opaque-public-id is a random, non-account identifier generated by CampusOS. It is not an authentication secret.
  • The address is installation-scoped and stable across app restarts and package updates.
  • The app cannot choose or claim its own hostname. Rotation and custom-domain binding are not implemented yet.
  • Operators may map a package ID to an exact platform-owned hostname with CAMPUS_PLATFORM_APP_HOSTS (for example campus-discourse=forum.campus.host). The alias is offered only to that package's installation in the singleton platform account; its generated opaque address remains routed as a fallback.
  • Revoking all routes removes the address from active Ingress topology while reserving it for the installation. Re-enabling a route restores the same address.
  • A future explicit rotation must create a new opaque ID, atomically publish the new address, then retire the old address after a short bounded drain period.
  • Uninstall removes routing immediately. Address reuse by a different installation is forbidden.

An optional custom domain is an account-owned binding, not an app permission. Campus must verify a DNS challenge, issue TLS only after verification, and bind the domain to one PublicAddress.

Public route records

Today installation/public-id is persisted and the address/status/routes below are derived into the catalog response. Rotation and custom domains will require first-class lifecycle records shaped as:

text
PublicAddress {
  id, accountId, appId, instanceId,
  publicId, hostname,
  status: reserved | active | draining | disabled,
  createdAt, rotatedAt?
}

PublicRoute {
  id, publicAddressId,
  method, pathPattern,
  handler: backend | agent,
  agentAction?, agentProfileId?,
  declarationHash, grantHash,
  status, createdAt, updatedAt
}

PublicDomain {
  domain, publicAddressId,
  verificationChallengeHash,
  tlsStatus, status
}

accountId, instanceId, Gateway EndpointId, and backend location are control-plane-only fields. They are never returned to an Internet client or embedded in the assigned hostname.

Request lifecycle

  1. Publisher declares a route in publicEndpoints.
  2. Account user approves the exact endpoints.public(METHOD:path) grant in trusted Settings.
  3. CampusOS allocates or reuses the installation's PublicAddress.
  4. CampusOS reconciles a revisioned host and route snapshot to every configured Ingress.
  5. Ingress terminates TLS, normalizes the request, enforces host/size/rate policy, and forwards over Iroh to the selected Gateway.
  6. Gateway re-checks hostname, method, normalized path, route revision, and running installation.
  7. Gateway strips Campus/private credentials and untrusted forwarding headers, then dispatches only to the route's declared handler.
  8. Response limits, timeout, and safe header policy are enforced on the return path.

Campus public endpoints are unauthenticated at the platform layer today. Apps needing client authentication must use a route-specific application protocol, such as a signed webhook payload. A future client-auth mode must use a credential namespace that cannot be confused with Campus service, launch, or session credentials.

Public capabilities

A backend public route does not automatically expose Agent or any account capability. Apps that need anonymous structured invocation publish an MCP tool with the public audience; CampusOS serves it through the account MCP endpoint with the tool schema, live serve grant, concurrency limit, and audit boundary described in MCP. HTTP routes and public MCP tools remain independent.

Settings surface

Trusted App Settings currently shows the assigned base address, live/stopped state, effective method/path routes, and declared MCP tools. A running direct-port app receives its own authorized port descriptors in the launch data. The remaining target controls are:

  • assigned base address and copy action;
  • each declared route, its grant state, handler type, and effective public URL;
  • explicit enable/disable approval per method/path;
  • a stronger warning for routes that invoke an agent;
  • route-level traffic, error, model-token, and budget summaries;
  • address rotation and verified custom-domain controls;
  • clear status when topology is pending, active, stale, or failed.
  • trusted display of declared versus granted direct ports, the assigned host/port, and the warning that firewall/NAT and application-protocol security are operator responsibilities.

The trusted catalog response includes effective public URLs; an app may read it only with apps.catalog.read. Apps cannot grant a route, rotate an address, bind a domain, or modify topology.

Revocation and failure behavior

  • HTTP permission revocation removes the route from CampusOS state and pushes topology withdrawal. Direct-port or tunnel permission revocation sends a narrower Runtime spec; reconciliation removes the old container binding before the replacement instance becomes authoritative.
  • Gateway rejects a route absent from its latest desired policy even if Ingress is stale.
  • Stopping the app returns unavailable without revealing placement and keeps the route closed.
  • Package declaration changes invalidate grants for changed method/path.
  • Ingress snapshot authentication failure, unknown revision, hostname conflict, or Gateway identity mismatch must fail closed.
  • Address rotation and custom-domain changes are audited account actions.

Implementation status

ComponentStatus
Manifest publicEndpoints and endpoints.public grantsImplemented
HTTP and WebSocket route reconciliationImplemented
Ingress TLS-to-Iroh and Gateway route re-checkImplemented
One fixed internal HTTP app port; no HTTP-selected port publicationImplemented
Schema-version-4 exact direct UDP declaration and grant enforcementImplemented
Runtime-assigned UDP host ports and advertised public hostImplemented for Docker/container and Firecracker drivers
Fixed NET_ADMIN plus /dev/net/tun tunnel policyImplemented for Docker/container driver
Firecracker direct-UDP supportImplemented with scoped TAP DNAT/FORWARD policy
Firecracker tunnel-device supportNot implemented
Opaque random per-installation hostnameImplemented; no account ID in hostname
Stable address in catalog API and App SettingsImplemented
Address rotation lifecycleNot implemented
Custom-domain verification and bindingNot implemented
Public MCP toolsImplemented through the account MCP server, independently of public routes
Route-specific public-agent rate, concurrency, and intelligence budgetsNot implemented

Until the public-agent enforcement points exist, no public route should be described as safely invoking Agent.

Software belongs to people. Campus gives it a durable place to run.