Skip to main content

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.

Authentication modes​

The manifest selects one of two server-side Bearer-token modes:

ModeIntended useUpstream validation
app-scoped-tokenDefault for independently operated or less-trusted appsVerify CAIPE's short-lived app JWT, audience, app ID, and scopes.
forward-user-access-tokenTrusted first-party apps that need the user's existing OIDC/OpenFGA identityVerify the original access token with the OIDC issuer's JWKS, issuer, audience, and expiry checks.

The default is app-scoped-token when auth is omitted. Direct forwarding is an explicit trust decision: the runtime receives the same access token held by CAIPE's server-side session, including all permissions granted to that token. Use it only for a first-party runtime on a private origin. The token is never returned to the extension's browser code.

In both modes the gateway discards any browser-supplied Authorization, cookies, and X-CAIPE-* values. It creates the upstream Authorization header from the authenticated server session and identifies the selected contract in the host-controlled X-CAIPE-Auth-Mode header. Applications must validate the Bearer token; the routing headers are not standalone proof of identity.

App-scoped token 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 app-scoped 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 app using app-scoped-token. 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"
auth:
# Optional; this is the default and preserves least privilege.
mode: app-scoped-token
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​

An application using app-scoped-token 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.

An application using forward-user-access-token replaces steps 3–5 with standard OIDC access-token validation against the configured issuer. It should use the validated sub as its stable OpenFGA subject and keep the access token server-side for any downstream user-delegated authorization call.

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.

External MCP clients​

  • Declare api.mcp to expose an app-owned MCP authentication boundary:

    {
    "path": "/api/example/mcp",
    "metadataPath": "/.well-known/oauth-protected-resource/api/example/mcp",
    "apiKeyHeader": "x-example-token"
    }
  • Declare allowed POST protocol and GET metadata policies. Installed/enabled state, method policy, private origin binding, and body limits still apply.

  • The app validates external bearer/API credentials and enforces resource authorization. External MCP access is independent of the host's browser-session/UI-role policy; enable it only when the app owns that complete authorization boundary.

  • The gateway forwards external credentials only to the exact declared protocol path. It strips host cookies and identity headers, preserves OAuth challenges, and never substitutes a browser identity for an invalid external credential.

  • GET/HEAD requests to metadata and the protocol's openapi.json document bypass host-session authentication only when an applicable allow policy exists. Declare each desired method/path policy or a proxy:GET/proxy:HEAD fallback.

  • Manifest validation requires POST protocol and GET metadata policies; it does not automatically allow openapi.json or HEAD requests.

  • No credentials are forwarded to metadata or schema routes. Other runtime paths retain host-session admission.

  • The iframe resource URL is /apps/<app-id>/api/example/mcp; compiled native API mounts retain their declared public URL. The app's metadata must advertise that exact resource URL and a public authorization server.

  • Path-derived RFC 9728 discovery is available at /.well-known/oauth-protected-resource followed by the resource path. Disabled, unknown, or ambiguous resources fail closed.

Preserve saved external MCP connections​

  • The declared root api.mcp.path and its openapi.json schema remain available when an app moves to an iframe installation. CAIPE proxies these exact routes through the existing app gateway without redirecting clients or rewriting saved connections.
  • Root discovery remains available at /.well-known/oauth-protected-resource followed by api.mcp.path. A root MCP claim cannot overlap another app's MCP path or API mount.
  • For root protocol, metadata, and schema requests, CAIPE supplies x-caipe-mcp-resource-path with the declared root MCP path. Caller-supplied values are stripped. This selects a public resource URL; it does not authenticate a caller or supply host identity.
  • The app must recognize that fixed root selector, advertise the matching root resource and metadata URL, and validate tokens against that resource. Hosted requests continue using the hosted resource. Apps that ignore the selector may accept existing credentials but still fail OAuth rediscovery.
  • Retain the OAuth issuer, client registrations, refresh grants, and authorization-server resource registrations for both URLs. With resource indicators enabled, removing the old permitted resource can cause token issuance or refresh to fail even when routing works. Revoked grants still require sign-in.
  • For secondary OIDC, retain both public URL keys in the host's CAIPE_SECONDARY_AUTH_RESOURCES bindings. Restoring routing does not grant an undeclared resource access to a secondary trust anchor.
  • Deploy matching host and app changes before asking users to reconnect. Check an existing token, a refresh, and a fresh authorization against the saved root URL. No client configuration or CAIPE database migration is required.

Host keyboard shortcuts​

  • A connected iframe can receive hostShortcuts: ["search"] in its bridge capabilities.
  • The SDK forwards Cmd+K/Ctrl+K as a search intent. It does not transmit typed text or arbitrary key events.
  • The host accepts the intent only from the active app frame with the expected origin, app ID, and bridge version. The existing search component owns result authorization.
  • Without the advertised capability, the SDK leaves the key event untouched. Already handled events, repeats, composition, and alternate modifiers are not forwarded.

Current authorization scope​

Hosted presentation context​

The iframe host sends caipe.microfrontend.initialize.v1 messages on document load and live theme changes. The envelope has version: "1.0", appId, and a context containing the host pathname, resolved light/dark theme, locale, timezone, surface: "hosted", and an empty preferences object. This existing presentation-message version is independent of manifest API 2.0.

A child can request initialization after hydration with { type: "caipe.microfrontend.ready.v1", version: "1.0", appId }. Both sides must validate the exact origin, window source, app ID, and message version. The host replies at most once per document load to avoid acknowledgement loops. Children that do not implement this protocol keep their own styling.

This channel carries no JWT, session cookie, credential, user identity, or permission grant. Authentication and authorization stay in the server-side gateway. Theme synchronization does not sandbox a same-origin iframe or make its code less privileged.

New declarations use AgenticAppManifest 2.0, shared with native UI packages. Existing hosted v1.0 manifests are normalized through a compatibility adapter. Installation visibility controls catalog/navigation; authorized hidden apps can now launch through direct lookup. Role and request policy checks remain enforced. Permission requests and capability references are metadata; they do not create CAS grants or provision agents/tools.

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.

Query navigation in embedded apps​

  • An app connects with caipe.agenticApp.bridge.connect.v1 (version: "1.0", its appId). The host advertises queryNavigation: true in caipe.agenticApp.bridge.capabilities.v1.
  • caipe.agenticApp.navigate.v1 accepts a public href and optional history: "push" | "replace". Only query/hash changes on the current app's exact path are allowed. Cross-app, API, and external routes are rejected.
  • The host validates iframe source, same origin, app ID, and version. This capability works independently of assistant availability and grants no API access.
  • The host owns browser history and publishes caipe.agenticApp.navigation.v1 on navigation and Back/Forward. A connected frame keeps its original src; it restores local state and replaces its runtime query without adding a second history entry. Older apps retain their existing iframe behavior.
  • Only display state belongs in query parameters. Credentials, approvals, and operations that change resources must never be restored from a URL.