Skip to content

Campus permissions and shared services

Campus treats every installed application as an independent principal. An installation has three credential uses with deliberately different exposure:

  • The frontend service capability is delivered in the launch descriptor and calls only reviewed Campus services for that exact installation. It remains useful before any workload exists.
  • The workload data-plane capability is delivered with an Iroh endpoint only after lazy connect, can call only that installation's Gateway/backend, and is injected into that target workload as CAMPUS_APP_CAPABILITY so a direct backend protocol can use the same installation identity.
  • The CampusOS service credential is delivered only to the trusted Runtime and the adjacent per-installation Gateway. It is never materialized in the workload or a launch descriptor.
  • The workload Gateway credential is injected as CAMPUS_GATEWAY_TOKEN. It is meaningful only to that adjacent Gateway, which replaces it with the CampusOS credential on the trusted hop.

The runtime also injects CAMPUS_API_URL. Workload backends can construct CampusServicesClient::from_env() from the Rust Campus SDK or CampusServicesClient.fromEnv() from the JavaScript @campus/sdk package. CAMPUS_APP_ORIGIN contains the workload's exact trusted frontend origin. A backend may use these two app values for its own protocol authentication and Origin policy; they do not grant Campus service authority. Server-to-server Campus calls use the distinct workload-local CAMPUS_GATEWAY_TOKEN.

Every Campus App, including system-supplied Launcher, Store, Settings, Wallet, Contacts, and Personal Agent, has the same installation identity. UI and workload are independent declarations; optional workloads receive workload Gateway credentials only when they exist. See the unified application model.

Containers without network.public remain on the no-masquerade campus-restricted-egress bridge. Campus Node Runtime starts one logical Rust Campus Gateway per installation and injects its address into that installation only. In the Docker substitution, the app-facing portion runs as a per-installation Rust gateway sidecar attached to both the restricted app network and a trusted upstream network; the app itself remains restricted. The listener accepts only /v1/services/*, requires that installation's workload Gateway credential, replaces authorization with the host-held CampusOS service credential before forwarding to the fixed CampusOS origin, and is not a general HTTP proxy. This lets restricted apps use granted Campus services without acquiring public internet access. Apps granted network.public additionally join the regular Docker bridge and lose it immediately when the grant is revoked. This Docker path is a development substitution. On the Firecracker production path, network.public permits only publicly routable IPv4 destinations: Runtime rejects loopback, link-local and cloud-metadata addresses, RFC1918/private space, carrier-grade NAT, benchmark and documentation ranges, multicast, and reserved address space before allowing egress. IPv6 is disabled in the guest until the same policy is implemented for it.

Grants

Applications publish requested permissions in their catalog manifest. On first installation, Campus stores a grant snapshot. Changing the publisher manifest does not silently add authority to an existing installation. A grant is an ID plus one or more resources:

json
{
  "id": "notifications.publish",
  "resources": ["research:*"]
}

Resources are exact by default. * grants every resource and a suffix wildcard such as research:* grants resources in that prefix. Campus rejects grants that exceed the app's published request. Users can inspect or replace an installed app's grants through:

  • GET /v1/apps/:app-id/permissions
  • PUT /v1/apps/:app-id/permissions with { "permissions": [...] }

The PUT route requires the CampusOS session cookie and canonical request origin. The trusted Settings → Campus Apps surface edits the same resource list and shows public HTTP routes separately from MCP tool publication. See Campus Settings.

Account-scoped capability discovery and execution use mcp.tools.call, mcp.tools.propose, and mcp.tools.serve. Managed Agent apps have intrinsic authority to create inert proposals only; mcp.tools.propose remains the explicit equivalent for non-Agent backends. Their source/target split, passkey-backed owner approval path, selected-user/Contacts/public audiences, revision invalidation, and fixed Gateway ingress are documented in Campus account MCP.

Apps granted apps.catalog.read can inspect the same account-scoped package view through GET /v1/services/apps using their workload Gateway credential. The native Rust SDK exposes this as CampusServicesClient::list_apps().

Runtime lifecycle services are intrinsically scoped to the authenticated installation and require no manifest permission. Active Gateway requests, MCP calls, and renewable /v1/services/runtime/work-leases keep an automatic workload alive. When the final signal ends, Campus starts the installation's idle delay; there is no separate self-sleep marker. A lease can be created, renewed, or released only by its own installation. This is separate from the critical account-wide runtime.pause shell permission. Browser and Node callers should use the Campus SDK's acquireWorkLease(), renewWorkLease(), and releaseWorkLease() methods rather than construct these routes directly; the returned JavaScript lease handle also supports bounded auto-renewal.

system.inventory.read is a separate critical permission for a platform resource manager. It can be used only by an app owned and installed by the assigned master account, even if another installation somehow holds the same grant. GET /v1/services/system returns secret-free, fleet-wide account, node, capacity, installation, traffic, topology, and aggregate usage data. See Platform master account.

Browser shell and custom launchers

Browser applications use the CampusSDK shell bridge rather than the service credential for interactive parent-owned operations. The shell reports only capabilities present in the active installation's grant snapshot and enforces resources again for every target:

  • apps.catalog.read exposes live account-scoped app summaries.
  • apps.create creates a private account-owned app through the trusted shell.
  • apps.install may request an installation, but the trusted shell always owns approval.
  • apps.uninstall removes an installation while preserving its persistent app volume.
  • apps.lifecycle opens, starts, stops, or returns from allowed applications.
  • apps.launcher.register lets the active package become the account launcher or restore Home.
  • schedules.manage lets a verified Campus package bearing schedules.manager-ui use the narrow CampusSchedules SDK surface to inspect and mutate account schedules. It does not let an arbitrary target app schedule itself or bypass that target's lifecycle choice and MCP grants.
  • runtime.status.read and account.profile.read expose only their respective context fields.
  • runtime.pause lets the active package request the trusted account-wide stop; resume remains on the trusted paused surface.

The protocol is versioned and bound to the active iframe's app ID, source window, and exact origin. It never transfers the CampusOS cookie or a target app's launch descriptor to the caller. See Campus application shell and the browser SDK README for the JavaScript API.

wallet.payment.propose and wallet.policy.propose are prompt capabilities, not financial authority. They let an app open trusted review for an exact payment or VAS policy operation; only the owner's fresh, operation-specific passkey approval can execute it.

Intelligence gateway

An app with intelligence.use for a resource such as openai:* can use provider-compatible routes beneath:

text
/v1/services/intelligence/:provider/*provider-path
Authorization: Bearer $CAMPUS_GATEWAY_TOKEN

Gateway associates the request with the installation and asks CampusOS to check the provider/model resource, request rate, monthly token budget, envelope, and placement. Gateway then decrypts and injects the protected BYOK or Campus-owned provider credential. CampusOS does not receive the provider request body. Campus-owned credentials are authorized only on campus-hosted Nodes; BYOH must use BYOK. The provider credential is never returned to the app. Runtime environment variables point ordinary SDKs at this service automatically:

  • OPENAI_BASE_URL and a scoped OPENAI_API_KEY
  • ANTHROPIC_BASE_URL and a scoped ANTHROPIC_API_KEY

Those API-key values are request placeholders bound to the workload-local Gateway token, not CampusOS or reusable provider secrets. The installation recipient key remains in Gateway. The native SDK exposes the same path through CampusServicesClient::intelligence_request().

Campus-managed Pi workers also receive the campus-web-search extension. It uses the exact pinned pi-web-access OpenAI hosted-search module, with Pi/external provider fallback disabled and its Responses URL forced to the installation-local Gateway. Before the first request it shows the query in trusted Campus UI and offers Allow once, Allow for this Agent session, Always allow for this Agent, or Deny. Persistent approval is scoped to that Agent's private session directory, stores only a versioned consent boolean, and can be revoked from Agent actions → Ask before web searches; session approval resets when Pi starts a new session. This path does not grant network.public, browser cookies, arbitrary URL fetching, command credentials, or direct provider connectivity.

Protected secret slots

General provider credentials use manifest-declared secret slots. A usable request requires both secrets.use for the slot name and network.connect for the slot's exact origin, plus an active account binding whose declaration hash still matches the installed package.

text
METHOD /v1/services/outbound/:slot/*upstream-path
Authorization: Bearer $CAMPUS_GATEWAY_TOKEN

CampusOS resolves the fixed origin and path prefix from the manifest and authorizes only declared methods and a current binding. It returns an installation-sealed envelope to the broker-authenticated Gateway; Gateway rejects redirects and credential/hop-by-hop headers, decrypts transiently, and injects the value on the exact upstream request. The reusable secret is never returned by account or app APIs, launch descriptors, or runtime environment. Removing an app removes its bindings; removing a secret removes every binding that references it. The Rust SDK exposes this route as CampusServicesClient::outbound_request().

Secret slots are valid only for manifests with a workload. Frontend-only apps must use interactive browser authorization and cannot store durable Node credentials. See Installation-sealed secrets.

Logical storage

Hosted workloads may request object-storage.raw for self when a database or another backend component needs the provider's complete S3-compatible API instead of Campus logical Files. CampusOS provides the usual CAMPUS_S3_* shape only for a paid Campus-hosted installation with that grant, but the endpoint and credential values terminate at the adjacent trusted Gateway. The workload sees a local Gateway endpoint, a non-secret compatibility keypair, and its installation-local Gateway token in CAMPUS_S3_SESSION_TOKEN; it never receives the reusable provider access key, secret, or provider session token.

Gateway validates that local token, rejects presigned and streaming-signature inputs, confines every object, listing prefix, copy source, multipart operation, and multi-object delete to the exact CampusOS-derived bucket/prefix, and then signs the upstream request with the host-held provider key. Request bodies are limited to 16 MiB per operation with four concurrent buffered operations per installation, so large objects must use multipart upload. Provider endpoints require HTTPS except for loopback development. Package-authored CAMPUS_S3_* values are removed before launch, permission changes rebuild a Firecracker VM, and revocation removes both the local route and environment contract. Provider-side credentials should still be restricted to a dedicated workload-data bucket as defense in depth. Local and unpaid workloads receive no raw S3 variables. Raw object storage is suitable for ClickHouse parts and database internals; user-selected files and cross-app sharing must continue to use logical Files or Content.

App-owned files are private to their owning app by default and need no broad storage grant. files.user.manage is a trusted permission that allows the Campus-supplied holder of the nondelegable files.user-manager role to create, list, change, and remove the account's user-owned files; it never lists private app-owned files. Drive hides reserved .campus/ resources such as launcher appearance. Per-file ACLs can grant read, write, or manage to explicit app or user principals without exposing the backing blob object key. Logical metadata and transfer authorization remain in CampusOS, while the caller moves bytes directly to local campus-blob-store in development or Spaces in production. Both use the same CampusSDK file methods and transfer-grant shape. Logical user files are available before Node/payment provisioning; they are Campus account storage, not workload storage. Drive adds browser-side encryption and Content collections without changing this primitive. See Campus Drive and logical storage.

files.user.manage requires the verified files.user-manager system role. Campus Photos/Gallery and other encrypted-content clients use content.collections.* and Content-issued transfers; they must not bypass the Drive E2E layer by requesting raw user-file management.

Encrypted Content collections

content.collections.read, content.collections.write, and content.collections.manage cover the platform Content graph. The effective authorization is deliberately conjunctive: the account must have an owner/member role, the application must have an active collection admission (directly or on a cycle-free ancestor), and its installed permission resources must cover the admitted collection. A wildcard package declaration therefore does not expose another app's collections by itself.

An app-created collection atomically admits that app. A shared collection is admitted to an app only by the trusted account surface after the user accepts the invitation through that app. Descendant collections inherit server authorization from an admitted parent; encrypted parent entries carry the child keys. The account recipient private key remains passkey-protected in the shell, which releases only the exact collection key after repeating these checks.

Public endpoints and direct UDP

Public HTTP and WebSocket routes have two independent gates:

  1. The publisher declares an exact method/path (or a trailing /* path) in publicEndpoints.
  2. The installation has endpoints.public for the METHOD:/path resource. WebSockets use the synthetic method WS, for example WS:/native.

Only then does CampusOS place the installation hostname and Gateway EndpointId in its durable Ingress topology. It pushes the revisioned topology to each Ingress connection. Ingress selects the Gateway, which still rejects any hostname or WS:path not present in its desired route policy.

The HTTP public address is controlled by CampusOS and is not exposed in app launch/login data:

text
https://<app-id>--<opaque-public-id>.<configured-public-ingress-domain>/<path>

The hostname selects exactly one installation but does not bypass endpoint authorization. Public endpoints receive neither Campus session cookies nor app service credentials. Public requests also discard caller authorization, cookies, hop-by-hop headers, and caller-supplied forwarding metadata before entering the package Gateway. An internet-facing deployment must put TLS, rate limits, and abuse controls at Ingress. Native RunCampus clients instead receive only their private endpoint ticket and scoped capability.

Native protocols that cannot use HTTP Ingress may declare schema-version-4 publicPorts. A fixed UDP declaration or Runtime-assigned same-port declaration is active only with its matching grant:

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

declared publicPorts(name, udp, auto)
AND granted endpoints.public.udp(UDP:auto)

For a fixed container port, Runtime chooses a collision-free host port. For containerPort: auto, Runtime chooses one collision-free port used on both sides of the binding and injects CAMPUS_PUBLIC_HOST plus CAMPUS_PUBLIC_PORTS_JSON into the workload. Both forms return name, protocol, containerPort, operator-configured host, and hostPort. This is not an Ingress route: the operator must make the assigned UDP port reachable, and the app protocol must authenticate and encrypt clients. A direct-port grant neither supplies egress nor TUN authority. network.public separately controls egress, while runtime.network-tunnel maps only to the fixed NET_ADMIN plus /dev/net/tun policy. Direct UDP works with Docker and Firecracker; tunnel-device authority remains limited to Docker.

Notifications

Apps publish notifications with notifications.publish scoped to a topic:

http
POST /v1/services/notifications
Authorization: Bearer $CAMPUS_GATEWAY_TOKEN
Content-Type: application/json

{
  "topic":"research",
  "title":"Trend changed",
  "body":"Interest is rising.",
  "priority":"high",
  "presentation":{
    "mode":"flash",
    "tone":"accent",
    "actions":[{
      "id":"open",
      "label":"Open",
      "type":"open-app",
      "style":"primary",
      "target":"?alert=42"
    }]
  },
  "delivery":"inbox-and-system",
  "dedupeKey":"trend:42"
}

Every item is stored in CampusOS and presented by the system-supplied Notifications app. delivery is inbox, inbox-and-system (the default), or inbox-and-web-push. The second is mirrored through the active trusted shell; the third enters the durable encrypted Push API outbox and can wake an opted-in browser after all Campus windows close. Account settings and browser permission still govern both device surfaces. priority is low, normal, high, or critical. Producers should supply a stable dedupeKey for retryable events such as a payment proof, message ID, or completed background job. Unread high and critical records appear as trusted-shell Flashes. Their optional presentation is declarative and may use only the bounded layouts, tones, expiry, and actions documented in Campus notifications. open-app actions can open only the source installation with a same-app ?query target; raw HTML, script, external URLs, and cross-app targets are not accepted.

Apps with notifications.read see only authorized topics from GET /v1/services/notifications. They mark one visible record read with POST /v1/services/notifications/:id/read and all visible records read with POST /v1/services/notifications/read-all. CampusOS session routes remain an internal shell projection for the global badge and device delivery; app UI must use service capabilities instead.

Browser frontends use app.notifications() or CampusNotifications.connect() from CampusSDK. Their origin-bound frontend capability reaches the same service routes and is checked against the same installation grant snapshot as a backend Gateway token. CampusSDK also mediates notification activation and device permission through the active, source-bound launcher bridge.

Campus search indexing

Apps granted search.index for resource self can publish selected context to the account's private full-text index:

http
PUT /v1/services/search/index
Authorization: Bearer $CAMPUS_GATEWAY_TOKEN
Content-Type: application/json

{"entries":[{"id":"task-42","title":"Prepare launch notes","kind":"task","url":"/?task=task-42"}]}

The replace route is atomic for one installation. POST /v1/services/search/index upserts a batch, DELETE /v1/services/search/index/:entry-id removes one record, and deleting the collection clears the app's index. The account session queries installed apps and indexed context with GET /v1/search?q=launch. CampusOS uses Datalevin full-text attributes for both app metadata and published context, applies account filtering inside the search engine's document filter, and never returns the optional search-only text field.

Search targets must be app-local path/query URLs. Campus keeps the fragment for the launch descriptor and rejects absolute, protocol-relative, backslash, and fragment targets.

Initial catalog and example packages

Catalog seeding creates default schema-9 installations during account setup, before Node/payment provisioning. Home, Store, Settings, and Contacts have frontend-service credentials and no workload. Wallet and Personal Agent also have installation identities immediately, but workload/backend credentials become useful only when paid compute is available and an admitted action connects them.

Home's manifest requests the launcher grants it uses through the typed shell protocol. A custom launcher additionally needs apps.launcher.register before it can persist itself as the account launcher. The trusted parent retains installation approval and target launch descriptors.

Hybrid package examples exercise catalog reads, shared-file access, notifications, public routes, Iroh application transport, and lifecycle controls. Wallet private state and Bark operations live in its Node workload after the one-time encrypted bootstrap claim. Browser uses a separate Chromium workloads can request network.public; the Runtime still applies the shared egress policy.

frontend.chrome accepts auto, always, or never. auto and always retain the branded parent chrome; always records an explicit continuity requirement. never requests an immersive iframe in every display mode.

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