From 8f1f0245dab9823a285b0b1aead23842a05bf63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Sat, 29 Aug 2026 17:42:33 +0200 Subject: [PATCH] feat: the offline_access mandate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standing authority the autonomous runs act under: an offline refresh token issued by a new classic-models-mandate client for the user who authorised the agent, stored in Vault, redeemed at trigger time. The mandate client holds no domain audience. It cannot reach data; it can only say who authorised whom. Direct access grants on it is a lab shortcut, noted as such — mandating an agent is a decision a person takes, and a password grant records no consent where the authorization code flow would. Verified end to end: refresh gives sub=pm-user azp=classic-models-mandate with the agent in the audience, the agent exchanges it to azp=itself and aud=mcp-catalog, and the MCP call answers 200. sub stays the person who mandated, azp says which agent acted. Co-Authored-By: Claude Opus 5 --- deploy/agent-secrets.yaml | 6 ++++++ deploy/flux-kustomization.yaml | 1 + deploy/mandate-secrets.yaml | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 deploy/mandate-secrets.yaml diff --git a/deploy/agent-secrets.yaml b/deploy/agent-secrets.yaml index 7d1b846..0609104 100644 --- a/deploy/agent-secrets.yaml +++ b/deploy/agent-secrets.yaml @@ -22,3 +22,9 @@ spec: remoteRef: key: keycloak/classic-models-realm property: KC_SECRET_CLASSIC_MODELS_AGENT_APPROVISIONNEMENT + # Redeems a mandate. Held by the workflow rather than by the adapter because + # it is the workflow that acts as the agent. + - secretKey: MANDATE_CLIENT_SECRET + remoteRef: + key: keycloak/classic-models-realm + property: KC_SECRET_MANDATE diff --git a/deploy/flux-kustomization.yaml b/deploy/flux-kustomization.yaml index 38a47d1..ccf05ae 100644 --- a/deploy/flux-kustomization.yaml +++ b/deploy/flux-kustomization.yaml @@ -37,6 +37,7 @@ spec: # The agents' Keycloak client secrets, so a workflow can exchange tokens # as the agent it represents. Created by agent-secrets.yaml beside this. EXTRA_ENV_SECRET: n8n-agent-secrets + EXTRA_ENV_SECRET_2: n8n-mandate-secrets EXTRA_CA_CONFIGMAP: sttlab-ca # "false" so a workflow can read $env._CLIENT_SECRET. It means any # workflow on this instance can read any variable in the pod — acceptable diff --git a/deploy/mandate-secrets.yaml b/deploy/mandate-secrets.yaml new file mode 100644 index 0000000..4179b71 --- /dev/null +++ b/deploy/mandate-secrets.yaml @@ -0,0 +1,25 @@ +# The standing authority the autonomous runs act under. +# +# A mandate is an `offline_access` refresh token, issued by the +# classic-models-mandate client for the user who authorised the agent. It does +# not expire, which is exactly why it is here and not in a workflow: it is +# revoked by that user's session being revoked in Keycloak, and by nothing else. +# +# Two ExternalSecrets rather than one, because they answer to different owners: +# the mandate is the agent estate's, the client secret that redeems it is the +# realm's. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: n8n-mandate-secrets + namespace: n8n +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-n8n + target: + name: n8n-mandate-secrets + dataFrom: + - extract: + key: n8n/mandates