Stéphane Tailland c1eaead5d2 Declare the Catalogue agent as a Keycloak client
First of the six agents to need an identity. It exchanges the end user's
token for one narrowly-scoped token per MCP server, so `azp` names the
agent while `sub` stays the human — which is what makes the audit trail
able to answer "which agent asked, on whose behalf".

Deliberately narrower than the librechat client it is modelled on:

  * no browser flow at all. standardFlowEnabled, implicitFlow and
    directAccessGrants are off, because the agent never logs a user in —
    it receives an already-issued token over A2A. Any of those three
    would be a way to obtain a token it has no business obtaining.
  * no service account. It has no standing access of its own; everything
    it can reach, it reaches on behalf of a user who asked.
  * two optional scopes, mcp-catalog-audience and mcp-sales-audience, and
    nothing else. librechat carries all of them because it fronts every
    domain; an agent serving the product manager must not be able to mint
    a payments token.

Adds classic-models-agent-catalogue-audience, the scope whoever issues
the user's token requests so the agent appears in that token's audience.
Keycloak's Standard Token Exchange refuses otherwise, with "Client is not
within the token audience" — which reads as a permissions problem on the
target rather than a missing audience on the source.

Verified against the running realm: catalog and sales exchange
successfully and return aud=[mcp-catalog] / aud=[mcp-sales] with
azp=classic-models-agent-catalogue, while payments is refused with
invalid_scope. The confinement is enforced by Keycloak, not by the
agent's own restraint.

Note this file is a bootstrap seed, not the live realm: Keycloak starts
with --import-realm under IGNORE_EXISTING and logs "Realm
'classic-models' already exists. Import skipped", so editing it changes
nothing on a restart. The client and scope were created through the admin
API and recorded here so a fresh install matches. That divergence is
worth closing separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:57:50 +02:00
2026-05-07 21:08:39 +02:00
2026-05-07 21:08:39 +02:00
2026-05-07 21:08:39 +02:00
2026-05-07 21:08:39 +02:00

pm-keycloak

Keycloak deployment with declarative realm configuration via keycloak-config-cli.

Stack

  • Keycloak 26.5.4 — identity and access management
  • PostgreSQL 16 — persistent storage
  • keycloak-config-cli 6.5.0 — declarative realm configuration

Installation

Prerequisites

  • Docker and Docker Compose

Setup

cd compose
cp .env.example .env

Edit .env with your credentials:

COMPOSE_PROJECT_NAME=pm-keycloak
KC_DB_PASSWORD=<your-db-password>
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=<your-admin-password>

Start

docker compose up -d

Keycloak is available at http://localhost:8080.

keycloak-config-cli runs once at startup, applies all realm configuration files, then exits. This is expected behavior.

Configuration

Realm configuration files live in compose/keycloak-config/. Each .yaml file maps to one realm.

Apply configuration changes

After editing a realm file:

docker compose run --rm keycloak-config-cli

File structure

compose/keycloak-config/
├── master-realm.yaml   # minimal patch of the master realm
└── demo-realm.yaml     # example realm with roles and clients

Managed mode

IMPORT_MANAGED_REALM: full is set, meaning keycloak-config-cli is the source of truth for each realm it manages. Anything not declared in a YAML file will be removed from Keycloak on the next apply.

S
Description
No description provided
Readme 405 KiB
Languages
Markdown 100%