From 666735b0eb5afab599f5dbeed67597a3bd3fcb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Sat, 29 Aug 2026 13:25:35 +0200 Subject: [PATCH] feat: deploy the Approvisionnement facade The adapter runs in the agents namespace with the other agents, not with n8n: what it is, is an agent, and the workflow behind it is an implementation detail its callers never see. Co-Authored-By: Claude Opus 5 --- adapter/.dockerignore | 8 ++ adapter/Dockerfile | 42 ++++++++++ deploy/agent-facades-kustomization.yaml | 17 ++++ deploy/agents/approvisionnement.yaml | 103 ++++++++++++++++++++++++ deploy/agents/kustomization.yaml | 4 + 5 files changed, 174 insertions(+) create mode 100644 adapter/.dockerignore create mode 100644 adapter/Dockerfile create mode 100644 deploy/agent-facades-kustomization.yaml create mode 100644 deploy/agents/approvisionnement.yaml create mode 100644 deploy/agents/kustomization.yaml diff --git a/adapter/.dockerignore b/adapter/.dockerignore new file mode 100644 index 0000000..2f6fbff --- /dev/null +++ b/adapter/.dockerignore @@ -0,0 +1,8 @@ +.venv/ +.git/ +.pytest_cache/ +__pycache__/ +*.pyc +tests/ +k8s/ +dev/ diff --git a/adapter/Dockerfile b/adapter/Dockerfile new file mode 100644 index 0000000..3d6b024 --- /dev/null +++ b/adapter/Dockerfile @@ -0,0 +1,42 @@ +# Two stages so the runtime image carries no build toolchain and no uv. +FROM python:3.12-slim AS build + +COPY --from=ghcr.io/astral-sh/uv:0.9.7 /uv /bin/uv + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=never + +WORKDIR /app + +COPY pyproject.toml README.md ./ +COPY src ./src + +# One step, no lockfile: the version floors in pyproject.toml are what pins this, +# and they are deliberately recent (August 2026) to keep the vulnerability +# surface small. Add a uv.lock when the build needs to be byte-reproducible. +RUN --mount=type=cache,target=/root/.cache/uv \ + uv venv /app/.venv && uv pip install --python /app/.venv/bin/python . + +FROM python:3.12-slim AS runtime + +# Non-root, no shell login, no home to write to. The agent holds a Keycloak +# client secret in its environment; a container that cannot be written to is one +# less way to read it back out. +RUN useradd --system --uid 10001 --no-create-home --shell /usr/sbin/nologin agent + +WORKDIR /app +COPY --from=build /app/.venv /app/.venv +ENV PATH="/app/.venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +USER 10001 +EXPOSE 8080 + +# The Agent Card is the natural health endpoint: it is served by the same app, +# needs no auth, and a 200 means the A2A routes are actually mounted. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD ["python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/.well-known/agent-card.json', timeout=4).status==200 else 1)"] + +ENTRYPOINT ["python", "-m", "a2a_n8n_adapter"] diff --git a/deploy/agent-facades-kustomization.yaml b/deploy/agent-facades-kustomization.yaml new file mode 100644 index 0000000..cf786a4 --- /dev/null +++ b/deploy/agent-facades-kustomization.yaml @@ -0,0 +1,17 @@ +# The agent facades, in the agents namespace. Separate from the n8n +# Kustomization because they are a different thing in a different namespace, +# with a different reason to change. +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: agent-facades + namespace: n8n +spec: + interval: 10m + sourceRef: + kind: GitRepository + name: agent-flows + path: ./deploy/agents + prune: true + wait: true + timeout: 3m diff --git a/deploy/agents/approvisionnement.yaml b/deploy/agents/approvisionnement.yaml new file mode 100644 index 0000000..759e09c --- /dev/null +++ b/deploy/agents/approvisionnement.yaml @@ -0,0 +1,103 @@ +# The A2A facade for Approvisionnement. It lives in the `agents` namespace with +# the other agents, not with n8n: what it is, is an agent — the workflow behind +# it is an implementation detail its callers never see. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: approvisionnement-agent + namespace: agents +spec: + # The adapter is stateless — every task is one webhook call — so this is a + # default rather than a constraint. What is not replicable is n8n behind it. + replicas: 1 + selector: + matchLabels: + app: approvisionnement-agent + template: + metadata: + labels: + app: approvisionnement-agent + spec: + securityContext: + runAsNonRoot: true + runAsUser: 10001 + seccompProfile: + type: RuntimeDefault + containers: + - name: adapter + image: gitea.sttlab.eu/stt/a2a-n8n-adapter:v0.1.0 + ports: + - name: http + containerPort: 8080 + env: + - name: ADAPTER_AGENT_NAME + value: approvisionnement + - name: ADAPTER_AGENT_DESCRIPTION + value: >- + Supply decisions for the Classic Models catalog. Answers whether a + reference can be promised, from stock on hand. Holds no standing + access of its own. + - name: ADAPTER_SKILL_ID + value: check_availability + - name: ADAPTER_SKILL_NAME + value: Check availability + - name: ADAPTER_SKILL_DESCRIPTION + value: >- + Decide whether a product reference can be promised. Name the + reference in the question, as S10_1949. Returns a decision and the + reason for it, never the stock table. + - name: ADAPTER_SKILL_EXAMPLES + value: | + Can we promise S10_1949? + Is S18_3782 close to a reorder? + - name: ADAPTER_WEBHOOK_URL + value: http://n8n.n8n.svc.cluster.local/webhook/check-availability + - name: ADAPTER_PUBLIC_URL + value: http://approvisionnement-agent.agents.svc.cluster.local:8080/ + - name: ADAPTER_CA_BUNDLE + value: /etc/sttlab-ca/ca.crt + volumeMounts: + - name: sttlab-ca + mountPath: /etc/sttlab-ca + readOnly: true + readinessProbe: + httpGet: + path: /.well-known/agent-card.json + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /.well-known/agent-card.json + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: 20m + memory: 96Mi + limits: + cpu: 500m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumes: + - name: sttlab-ca + configMap: + name: sttlab-ca +--- +apiVersion: v1 +kind: Service +metadata: + name: approvisionnement-agent + namespace: agents +spec: + selector: + app: approvisionnement-agent + ports: + - name: http + port: 8080 + targetPort: http diff --git a/deploy/agents/kustomization.yaml b/deploy/agents/kustomization.yaml new file mode 100644 index 0000000..e678c90 --- /dev/null +++ b/deploy/agents/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - approvisionnement.yaml