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
- An app and its box are private by default.
- Public exposure requires both a package declaration and an installation grant.
- 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.
- 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.
- Ingress and the installation Gateway independently enforce HTTP/WebSocket routes; Runtime reconciles direct UDP bindings.
- Public traffic carries no CampusOS session or private app capability.
- A public request cannot exercise the owner's or ordinary Agent session's authority.
- Revocation, stop, uninstall, or stale topology closes reachability fail-closed.
Topology
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:
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: /eventsBoth conditions must hold:
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:
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:
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:
{
"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 fixedNET_ADMINplus/dev/net/tuncontainer 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:
https://<app-id>--<opaque-public-id>.<campus-app-domain>Example:
https://campus-notes--pub-7a4f09c18d234ef69a1b43cd.apps.campus.example/hooks/githubRules:
opaque-public-idis 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 examplecampus-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:
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
- Publisher declares a route in
publicEndpoints. - Account user approves the exact
endpoints.public(METHOD:path)grant in trusted Settings. - CampusOS allocates or reuses the installation's
PublicAddress. - CampusOS reconciles a revisioned host and route snapshot to every configured Ingress.
- Ingress terminates TLS, normalizes the request, enforces host/size/rate policy, and forwards over Iroh to the selected Gateway.
- Gateway re-checks hostname, method, normalized path, route revision, and running installation.
- Gateway strips Campus/private credentials and untrusted forwarding headers, then dispatches only to the route's declared handler.
- 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
| Component | Status |
|---|---|
Manifest publicEndpoints and endpoints.public grants | Implemented |
| HTTP and WebSocket route reconciliation | Implemented |
| Ingress TLS-to-Iroh and Gateway route re-check | Implemented |
| One fixed internal HTTP app port; no HTTP-selected port publication | Implemented |
| Schema-version-4 exact direct UDP declaration and grant enforcement | Implemented |
| Runtime-assigned UDP host ports and advertised public host | Implemented for Docker/container and Firecracker drivers |
Fixed NET_ADMIN plus /dev/net/tun tunnel policy | Implemented for Docker/container driver |
| Firecracker direct-UDP support | Implemented with scoped TAP DNAT/FORWARD policy |
| Firecracker tunnel-device support | Not implemented |
| Opaque random per-installation hostname | Implemented; no account ID in hostname |
| Stable address in catalog API and App Settings | Implemented |
| Address rotation lifecycle | Not implemented |
| Custom-domain verification and binding | Not implemented |
| Public MCP tools | Implemented through the account MCP server, independently of public routes |
| Route-specific public-agent rate, concurrency, and intelligence budgets | Not implemented |
Until the public-agent enforcement points exist, no public route should be described as safely invoking Agent.