Skip to main content
Version: main 🚧

External Apps

External Apps are independently deployed web applications that appear in the CAIPE Apps hub. The host supplies navigation and authenticated access; the application keeps ownership of its user experience, data, and domain-specific authorization.

This integration is useful when a product should be discoverable from CAIPE without rebuilding it as a CAIPE agent or merging its source into the UI.

What users see​

After an administrator registers and enables an app, users can:

  1. Open Apps in CAIPE.
  2. Select an app they are allowed to use.
  3. Work in the app inside the CAIPE shell while the host supplies the signed-in identity for approved requests.

The app remains responsible for its own screens, data, and business rules. Users do not need a second CAIPE login, but they still need the roles and scopes required by that app.

When to use External Apps​

Choose External Apps for a complete web experience with its own UI and domain model. Choose Agent Builder when the experience is primarily a conversation; choose an MCP server when an agent needs a tool or service operation without a separate web interface.

Request flow​

The browser never receives the runtime's private origin or the signing secret. It also never performs a token exchange. The BFF mints a fresh token for each proxied request after access and route policy checks succeed.

/apps/<id> is the single canonical browser and application base URL. A top-level document reaches the hosted shell. The Apps hub uses a normal document navigation for launches; iframe, asset, and fetch requests then use the same URL prefix and are internally rewritten to /api/agentic-apps/runtime/<id>. This lets a bundled application use one stable base path while the private runtime route remains an implementation detail.

Routes outside /apps remain unframeable. The Apps surface and private runtime route permit only same-origin framing so registered applications can render in the host shell without allowing cross-origin embedding.

Trust boundary​

The app-scoped JWT is the authoritative identity contract:

ClaimMeaning
subStable subject from the authenticated OIDC session. Required.
audagentic-app:<app-id>. Prevents accidental cross-app replay when verifiers enforce it. A holder of the shared signing secret can still forge another app's token.
app_idRegistered destination application.
issHost issuer, caipe-agentic-apps unless configured otherwise.
scp / scopeScopes granted by the matched route policy.
name, emailOptional display metadata. Never authorization keys.
decision_id, correlation_id, jtiPer-request tracing identifiers.
iat, expFive-minute token lifetime by default.

The gateway strips browser cookies, Authorization, conventional proxy identity headers, all X-CAIPE-* headers, and destination-specific identity headers before adding its own values. X-CAIPE-* response/request hints are useful for logs, but an app must authorize only after verifying the Bearer JWT.

The signing key is symmetric in this first contract. Set a dedicated AGENTIC_APP_TOKEN_SECRET of at least 32 random bytes in both the CAIPE UI and each registered app. Do not reuse NEXTAUTH_SECRET. Because this first slice uses one verifier secret, every registered runtime that receives it is inside the same token-signing trust boundary. Per-app keys or host-only asymmetric signing with JWKS are required before treating runtimes as mutually untrusted.

Configure the host​

Enable the feature and mount one YAML file into the UI container:

AGENTIC_APPS_INSTALL_ENABLED=true
AGENTIC_APPS_CONFIG_PATH=/etc/caipe-ui/agentic-apps.yaml
AGENTIC_APP_TOKEN_SECRET=<dedicated-shared-secret>
AGENTIC_APP_TOKEN_ISSUER=caipe-agentic-apps

The catalog is deployment-owned. It is not written to MongoDB and contains no built-in or vendor-specific registrations.

proxied-next-zone is retained as the compatible manifest identifier from the previously accepted contract; it does not by itself promise support for an embedded Next.js App Router transport.

agentic_apps:
packages:
- package_id: example-app
source: helm
manifest:
id: example-app
displayName: Example App
description: Example independently deployed application.
apiVersion: "1.0"
runtime:
kind: proxied-next-zone
origin: http://example-app.example.svc.cluster.local
mountPath: /apps/example-app
preserveMountPath: false
chrome: iframe
# Optional per-app limit. The default is 10 MiB; the maximum is 64 MiB.
maxRequestBodyBytes: 67108864
surfaces:
showInHub: true
navOrder: 50
access:
requiredRoles: [user]
tokenScopes: [example-app:read, example-app:run]
policyActions:
- action: proxy:GET
defaultEffect: allow
requiredScopes: [example-app:read]
- action: create-report
method: POST
path: /api/reports
defaultEffect: allow
requiredScopes: [example-app:run]
health:
endpoint: /health
catalog:
categories: [example]
capabilities: [reports]
installations:
- app_id: example-app
package_id: example-app
installed: true
enabled: true
visible: true
runtime_mount_path: /apps/example-app
runtime_origin_override: http://example-app.example.svc.cluster.local

The UI validates the whole catalog at startup. Invalid IDs, non-HTTP origins, missing packages, duplicate mounts, unsupported runtime kinds, and malformed policy declarations stop startup instead of silently exposing a partial catalog. ConfigMap volume updates are read on subsequent requests.

Try the Weather example​

The repository includes an opt-in Weather reference runtime that exercises this contract end to end:

  • a standalone Node.js application, separate from the CAIPE UI;
  • live forecast and air-quality data from Open-Meteo, with no provider key;
  • browser assets and API calls rooted beneath /apps/weather/;
  • independent verification of the app-scoped JWT; and
  • separate read and write scopes, including an exact mutation route.

It is a hosting example, not the CAIPE Weather agent. Merging or building the repository does not start, register, or expose it.

To try it with a locally running CAIPE UI, generate one secret of at least 32 bytes and provide the same value to both processes. Start the example from the ui directory:

AGENTIC_APP_TOKEN_SECRET='paste-the-same-generated-secret-here' \
node examples/external-apps/weather/server.mjs

Then start the UI with the committed opt-in catalog:

AGENTIC_APPS_INSTALL_ENABLED=true \
AGENTIC_APPS_CONFIG_PATH="$PWD/examples/external-apps/weather/agentic-apps.yaml" \
AGENTIC_APP_TOKEN_SECRET='paste-the-same-generated-secret-here' \
npm run dev

After signing in, open /apps and select Weather. The example README also documents its container build, routes, and trust boundary.

Route policies​

Routes fail closed. A request is proxied only when one of these rules matches:

  • An exact method and path policy. :parameter matches one path segment.
  • A compatibility action named proxy:<METHOD> when no route-specific policy exists for that method.

Use exact route rules for mutations and assign only the scopes needed by that operation. Method-wide rules are convenient during an initial integration but grant every declared app scope when requiredScopes is omitted.

Application requirements​

The external application must:

  1. Be reachable from the CAIPE UI pod at the configured private HTTP(S) origin.
  2. Serve browser traffic beneath the same-origin gateway prefix supplied in X-Forwarded-Prefix. For example, a Vite build for example-app uses base: "/apps/example-app/". Root-relative /assets or /api URLs escape the app's canonical prefix and will not work.
  3. Verify HS256 with the dedicated signing secret.
  4. Verify iss, aud, app_id, exp, and a non-empty sub before trusting name, email, or scopes.
  5. Require the appropriate scp value for every operation.
  6. Avoid session cookies; the gateway deliberately removes them.
  7. Keep its own resource model and fine-grained domain authorization.

This first runtime contract is tested for base-path-capable SPAs and ordinary HTTP assets/fetches. Host launch links use full document navigation. An embedded Next.js App Router application's RSC/client-navigation transport is not part of the supported contract yet and requires a dedicated end-to-end fixture before it can be claimed.

The host treats configured origins as trusted first-party applications. It removes upstream framing restrictions so the response can render inside the same-origin Apps shell. The iframe is intentionally unsandboxed: its JavaScript can access the parent DOM and browser storage and can make same-origin CAIPE requests with the user's session. The gateway also replaces the runtime's framing/CSP response headers with host policy, which is report-only in this initial slice. SAMEORIGIN prevents cross-origin framing; it does not isolate a registered app from CAIPE. Do not register arbitrary or less-trusted websites. Use a separate-origin or sandbox/message-channel design before widening the publisher trust boundary.

Current authorization scope​

This first forward-port provides deployment flags, OIDC identity continuity, role launch gates, per-route policy, app-bound tokens, and the protected runtime proxy. It does not create agentic_app objects in OpenFGA and does not call an external capability authorization service. A later authorization adapter can add that outer launch/discovery decision without changing the external application's token-verification contract.

The parser accepts existing health.blockLaunchWhen and installation health_policy metadata for catalog compatibility, but this first slice does not run active health probes or use health state as an admission decision. Runtime connection failures return 502 upstream_unavailable. Health-based launch admission is a separate follow-up.

The runtime currently buffers request bodies before forwarding them. Requests default to a 10 MiB per-app limit and receive 413 request_body_too_large when they exceed it. Set runtime.maxRequestBodyBytes when an application needs a larger upload/import contract, up to the per-app ceiling of 64 MiB. The host keeps one additional MiB of transport headroom so an oversized request can be rejected cleanly rather than forwarded as a truncated body.

Every reverse proxy or ingress in front of the UI must also admit the configured per-app limit plus that transport headroom. For example, an app configured for 64 MiB requests needs an upstream allowance of at least 65 MiB; otherwise the upstream proxy will reject the request before the app-specific limit is applied.