---
title: Cratis Arc capability hosting
editUrl: https://github.com/Cratis/Arc/edit/main/Documentation/backend/csharp/capability-hosting.md
description: Require verified callers on a dedicated Cratis Arc endpoint-group port, read the admitted caller, and record capability causation with Chronicle.
lastUpdated: 2026-11-06T00:00:00.000Z
---


Cratis Arc capability hosting requires a verified caller for every request on an assigned endpoint-group port. Place the commands and queries you want to expose in that group, then enable the caller gate.

## What it does

Requests without a verified caller receive `401` with `WWW-Authenticate: Bearer`. Malformed authorization, multiple credentials, and credentials in a URI receive `400`. A verified principal kind outside the configured allowlist receives `403`. These refusals use `application/problem+json`, not HTML, and do not run the handler. Requests on other ports do not resolve a capability caller.

The default allowlist admits `Person` and `DelegatedActor`. Configure `AllowedPrincipalKinds` explicitly if the application also admits service principals.

## Turn it on

Configure the server listener, the [endpoint-group assignment](/arc/backend/csharp/endpoint-groups/), and a [verified caller installation](/arc/backend/csharp/asp-net-core/verified-callers/). The following **configuration fragment** belongs in the runnable acceptance sample's `Program.Build`, where `settings` supplies public verification material and the sample's `PersonDirectory` provides persisted account mappings:

```csharp
using Cratis.Arc;
using VerifiedCallerAcceptance;

builder.WebHost.UseUrls($"http://127.0.0.1:{settings.DefaultPort}", $"http://127.0.0.1:{settings.CapabilityPort}");
builder.AddCratisArc(
    options => options.EndpointGroups.Assign("capabilities", settings.CapabilityPort),
    configureBuilder: arc => arc.WithCapabilities().WithVerifiedCallers(callers => callers
        .ForResource("https://api.example.test", "example-api", "sample")
        .UsePersonDirectory<PersonDirectory>()
        .UseSignedForwardedIdentityV2(v2 => v2.WithRealmIssuer("https://identity.example.test")
            .AddPublicKey(settings.KeyId, settings.PublicKeyPem)
            .UseProcessLocalNonceStore())));
```

Tag capability commands, read models, or query methods with `[EndpointGroup("capabilities")]`. Group membership is the explicit exposure choice; commands and queries outside that group retain their existing routing. Enabling capabilities without assigning the group's port fails startup with `CapabilityEndpointGroupHasNoPort`. Startup also fails with `CapabilityHostingHasNoCredentialInstallation` when no credential verifier and receiving installation are registered.

For a different group name, set `EndpointGroup` through the `WithCapabilities` configuration delegate and assign a port to that same group. The [verified caller guide](/arc/backend/csharp/asp-net-core/verified-callers/) explains custom signed tenant headers and fleet-wide nonce stores. Process-local replay protection is only a single-process sample choice.

## Read the caller

Inject `Cratis.Arc.Capabilities.ICapabilityCallers` into capability code. Its `Current` property returns the admitted `CapabilityCaller`, including the endpoint group, protocol `rest`, principal kind, person, current delegated actor, client metadata, credential identifier, checked grant identifier, tenant, and scope ceiling. Outside a capability call it is null.

A declared client is metadata, not an authenticated actor. `Client.IsAuthenticated` distinguishes authenticated client metadata from declared metadata. The scopes are a verified ceiling, not an authorization decision.

## What events record

With Arc's Chronicle integration, a command executed while a capability caller is present adds a `Capability` entry to event causation. It contains these properties:

| Property | Value |
| --- | --- |
| `endpointGroup` | Receiving endpoint group |
| `protocol` | `rest` |
| `principalKind` | `person`, `delegated-actor`, `organization-automation`, `cluster-workload`, or `runner` |
| `person` | Product person reference |
| `actorIssuer` | Current delegated actor's issuer |
| `actorSubject` | Current delegated actor's subject |
| `clientId` | Client identifier |
| `clientAuthentication` | `declared` or `authenticated` |
| `credentialId` | Non-secret credential identifier |
| `grantId` | Checked delegation grant identifier |
| `tenant` | Verified tenant identifier |

Absent values are omitted, including an unset tenant. Tokens, secrets, scopes, and the internal credential kind are not recorded by this entry. Commands outside a capability call add no capability causation.

## Limits today

- The public installer supports signed v2 browser-session callers; PAT, OAuth-grant, service-account, bearer-verifier, and on-behalf installation are separate work.
- Product authorization still applies: a capability is not authority. The verified caller is the principal used by Arc authorization, the current-principal accessor, and Chronicle identity/causation. Roles, policies, and live tenant membership must authorize that subject.
- `[RequiresScope]` narrows access against the verified credential's scope ceiling. Missing scopes mean an empty ceiling. Product cookies, roles, or a same-subject scheme cannot widen it, and scopes cannot replace product authorization.
- A signed tenant is a candidate, not proof of membership. A conflicting authenticated subject is refused, not merged.
- Keep the group port private. Never expose it publicly or forward public traffic to it.
- Default-group endpoints reachable on the capability port also require a caller. There are no anonymous paths on that port, including health probes.
- MCP serving and protected-resource metadata follow separately; this API hosts REST capabilities only.
