feat: initial harness spec v1.0.0 + reference template

Adds the normative contract every platform agent must conform to, plus a
working reference template under template/ that ticks every box out of
the box.

Spec is derived from the production agents shipped in
cjot-backstage-az/agents/ on branch sandbox/jonathan:
  - agent-registry, agent-factory, decomposer, discovery-agent,
    golden-path, modernization-factory, modernization-factory-v2,
    policy-transformer, scaffold-agent, support-intake-agent, the-watcher

Contents:
  - SPEC.md          normative contract (13 sections + conformance checklist)
  - CHANGELOG.md     spec versioning (1.0.0)
  - docs/
      registration.md   self-registration with the agent-gateway
      observability.md  OTel logs/metrics/traces wiring
      manifest.md       /.well-known/agent.json schema + AgentSkill
                        serialisation (pydantic vs protobuf)
      kubernetes.md     k8s deployment shape, mandatory cross-refs,
                        per-namespace agents, resource sizing
      deployment.md     .image-version, ACR build, deploy scripts, rollback
      deviations.md     tracked debts against the spec
  - template/
      .env.local.example, .gitignore, .image-version (0.1.0),
      Dockerfile (python:3.12-slim, non-root UID 1001, HEALTHCHECK),
      requirements.txt (harness floor + optional LLM stack),
      app/agent.py (Starlette entry, lifespan + self-registration,
                    defensive _skill_dict for pydantic vs protobuf),
      app/logging_setup.py (canonical OTel log bridge — copy verbatim),
      app/metrics.py (meter + example counter/histogram),
      app/skills.py (AGENT_CONFIG + AgentSkill list),
      k8s/configmap.yaml + deployment.yaml (Deployment + Service,
                                            OTel annotations + 6-var env block,
                                            agents-sa + agents-kv-spc bindings),
      scripts/deploy.sh (auto-bump + az acr build + apply + rollout),
      scripts/full-deploy.sh (preflight + deploy + post-deploy smoke),
      scripts/deploy-local.sh (docker/podman + .env.local)
This commit is contained in:
2026-06-09 15:54:18 +01:00
parent c87dd3fa55
commit e6f10ba8c9
25 changed files with 1933 additions and 1 deletions

133
docs/deployment.md Normal file
View File

@@ -0,0 +1,133 @@
# Build & deployment
> Normative requirements: [SPEC.md §10](../SPEC.md#10-build--deployment-must)
Every agent ships three deploy scripts under `scripts/` that wrap the same build → push → apply → roll-out pipeline at three layers of fidelity.
## `.image-version`
A single-line file at the agent root:
```
1.0.3
```
Conventions:
- Semver only.
- **MUST** match `OTEL_RESOURCE_ATTRIBUTES`' `service.version` value at all times.
- Bumped whenever a code change ships. The patch number is auto-incremented by `scripts/deploy.sh` when no `--tag` is given.
- Pure image-version-only commits are reserved for *redeploy reruns* (e.g. rebuilding without code changes to refresh dependencies). They **SHOULD** be rare.
## ACR
All images are pushed to:
```
bstagecjotdevacr.azurecr.io/<agent-name>:<X.Y.Z>
bstagecjotdevacr.azurecr.io/<agent-name>:latest
```
Both tags are pushed every build so that pinning to `:latest` works for sandbox/dev clusters and pinning to `:<version>` works for prod clusters.
## `scripts/deploy.sh`
The fast path. Used for routine redeploys.
```bash
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACR_NAME="bstagecjotdevacr"
APP_NAME="<agent-name>"
NAMESPACE="agents"
VERSION_FILE="${SCRIPT_DIR}/../.image-version"
TAG=""
while [[ "$#" -gt 0 ]]; do
case $1 in
--tag) TAG="$2"; shift ;;
*) echo "unknown arg: $1"; exit 2 ;;
esac
shift
done
if [ -z "$TAG" ]; then
CURRENT=$(cat "$VERSION_FILE" 2>/dev/null || echo "1.0.0")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
PATCH=$((PATCH + 1))
TAG="${MAJOR}.${MINOR}.${PATCH}"
echo "$TAG" > "$VERSION_FILE"
fi
IMAGE="${ACR_NAME}.azurecr.io/${APP_NAME}:${TAG}"
echo "Building ${IMAGE} ..."
az acr build --registry "$ACR_NAME" \
--image "${APP_NAME}:${TAG}" \
--image "${APP_NAME}:latest" \
"${SCRIPT_DIR}/.."
echo "Applying manifests ..."
kubectl apply -f "${SCRIPT_DIR}/../k8s/configmap.yaml"
kubectl apply -f "${SCRIPT_DIR}/../k8s/deployment.yaml"
kubectl -n "$NAMESPACE" rollout status "deployment/${APP_NAME}" --timeout=180s
```
Behaviour:
- Auto-bumps the patch on `.image-version` if `--tag` is not given.
- Rebuilds and re-pushes both `:<tag>` and `:latest`.
- Applies `configmap.yaml` and `deployment.yaml` in order.
- Blocks until the rollout completes (or fails) within 180 s.
## `scripts/full-deploy.sh`
The "first time" path. Adds preflight + post-deploy smoke.
Preflight (refuse to continue if any check fails):
- `kubectl` available and pointed at a cluster.
- `az` available and logged in.
- Target namespace exists.
- ServiceAccount `agents-sa` exists in the namespace.
- SecretProviderClass `agents-kv-spc` exists in the namespace.
After `rollout status` succeeds:
- `kubectl exec deployment/<agent> -- curl -fs http://localhost:<port>/health`
- `kubectl exec deployment/<agent> -- curl -fs http://localhost:<port>/.well-known/agent.json | jq -e '.name'`
Failures abort with a non-zero exit code so CI can block.
## `scripts/deploy-local.sh`
For dev loop on a workstation without cluster access.
- Builds the image locally with `docker` or `podman`.
- Reads `.env.local` (sibling of `.env.local.example`).
- Runs the container with `-p <port>:<port>` and `--env-file`.
Useful for iterating on prompt changes without touching the cluster.
## CI integration
The platform's GitHub Actions / Gitea Actions runners invoke `scripts/deploy.sh --tag $GITHUB_SHA` (or similar) when an agent's directory changes on the deploy branch. Pull requests trigger a build-only run that pushes a `:<sha>` tag without applying manifests.
## Rolling back
There is no automated rollback. To roll back:
```bash
kubectl -n agents set image deployment/<agent-name> \
<agent-name>=bstagecjotdevacr.azurecr.io/<agent-name>:<previous-version>
kubectl -n agents rollout status deployment/<agent-name> --timeout=180s
```
If the rollback ships a different `service.version`, also revert `OTEL_RESOURCE_ATTRIBUTES` in the ConfigMap or the dashboards will misattribute. Prefer a forward-fix (new patch version) over a rollback whenever possible.
## Re-registration after redeploy
The agent re-registers with the gateway on every pod start. No manual step is required after a rollout. If the gateway pod is also restarting at the same time, expect 12 `WARNING`s in the agent log before registration succeeds — see [registration.md](registration.md#failure-modes--resolution).