From 69c3390c2e82bd9f60a11cae0960f8bbb2bae638 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:33:59 +0200 Subject: [PATCH 01/25] feat: add Helm chart for Kubernetes deployment --- helm/.helmignore | 9 + helm/ARCHITECTURE.md | 467 ++++++++++++++++++++++ helm/Chart.lock | 6 + helm/Chart.yaml | 29 ++ helm/README.md | 214 ++++++++++ helm/charts/postgresql-16.7.14.tgz | Bin 0 -> 84834 bytes helm/templates/NOTES.txt | 24 ++ helm/templates/_helpers.tpl | 104 +++++ helm/templates/configmap.yaml | 60 +++ helm/templates/deployment.yaml | 128 ++++++ helm/templates/hpa.yaml | 37 ++ helm/templates/pdb.yaml | 18 + helm/templates/pvc.yaml | 45 +++ helm/templates/secret.yaml | 75 ++++ helm/templates/service.yaml | 20 + helm/templates/serviceaccount.yaml | 13 + helm/templates/tests/test-connection.yaml | 21 + helm/values.yaml | 314 +++++++++++++++ 18 files changed, 1584 insertions(+) create mode 100644 helm/.helmignore create mode 100644 helm/ARCHITECTURE.md create mode 100644 helm/Chart.lock create mode 100644 helm/Chart.yaml create mode 100644 helm/README.md create mode 100644 helm/charts/postgresql-16.7.14.tgz create mode 100644 helm/templates/NOTES.txt create mode 100644 helm/templates/_helpers.tpl create mode 100644 helm/templates/configmap.yaml create mode 100644 helm/templates/deployment.yaml create mode 100644 helm/templates/hpa.yaml create mode 100644 helm/templates/pdb.yaml create mode 100644 helm/templates/pvc.yaml create mode 100644 helm/templates/secret.yaml create mode 100644 helm/templates/service.yaml create mode 100644 helm/templates/serviceaccount.yaml create mode 100644 helm/templates/tests/test-connection.yaml create mode 100644 helm/values.yaml diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 000000000..e1eb28a62 --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,9 @@ +# Patterns to ignore when building packages. +.DS_Store +.git/ +.gitignore +.vscode/ +.idea/ +*.swp +*.bak +*.tmp diff --git a/helm/ARCHITECTURE.md b/helm/ARCHITECTURE.md new file mode 100644 index 000000000..88f7b5f65 --- /dev/null +++ b/helm/ARCHITECTURE.md @@ -0,0 +1,467 @@ +# nao Helm Chart — Architecture & Values Reference + +This document explains how the nao Helm chart is structured, how its components interact at runtime, and provides a complete reference for every configurable value. + +--- + +## Table of contents + +- [Application architecture](#application-architecture) +- [Development architecture (docker-compose)](#development-architecture-docker-compose) +- [Docker build (multi-stage)](#docker-build-multi-stage) +- [Kubernetes resources](#kubernetes-resources) +- [Git sync initContainer](#git-sync-initcontainer) +- [Context modes](#context-modes) +- [Secret vs ConfigMap split](#secret-vs-configmap-split) +- [Complete values reference](#complete-values-reference) + - [General](#general) + - [Image](#image) + - [Service account](#service-account) + - [Application config (ConfigMap)](#application-config-configmap) + - [Secrets](#secrets) + - [Service](#service) + - [Resources](#resources) + - [Probes](#probes) + - [Autoscaling — HPA](#autoscaling--hpa) + - [Pod Disruption Budget](#pod-disruption-budget) + - [Node scheduling](#node-scheduling) + - [Persistence — local mode](#persistence--local-mode) + - [Projects volume — api mode](#projects-volume--api-mode) + - [PostgreSQL subchart](#postgresql-subchart) + +--- + +## Runtime architecture + +The nao Docker image bundles **two processes** managed by `supervisord`. Both run inside a single container in the same Pod. + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Kubernetes Pod │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ supervisord │ │ +│ │ │ │ +│ │ ┌──────────────────────────┐ ┌───────────────────────────┐ │ │ +│ │ │ Fastify / Bun │ │ FastAPI (Python) │ │ │ +│ │ │ backend │ │ analytics sidecar │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ • REST API │ │ • LLM orchestration │ │ │ +│ │ │ • tRPC │ │ • SQL generation │ │ │ +│ │ │ • Serves frontend SPA │ │ • Tool execution │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ port 5005 ◄────────────┼──┼── exposed via Service │ │ │ +│ │ │ │ │ port 8005 (internal) │ │ │ +│ │ └──────────────────────────┘ └───────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ +│ Volumes (conditional on context mode) │ +│ ┌─────────────────────────┐ ┌──────────────────────────────┐ │ +│ │ context PVC │ │ projects PVC │ │ +│ │ mountPath: contextPath │ │ mountPath: /app/projects │ │ +│ │ (local mode) │ │ (api mode) │ │ +│ └─────────────────────────┘ └──────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ port 5005 + ▼ + ┌────────────────────┐ + │ Service │ + │ ClusterIP / LB │ + └────────────────────┘ + + ┌────────────────────────────────────────────────────────────┐ + │ PostgreSQL (bitnami subchart, StatefulSet) │ + │ or external DB supplied via secrets.dbUri │ + └────────────────────────────────────────────────────────────┘ +``` + +| Process | Port | Visibility | Role | +|---------|------|-----------|------| +| **Fastify / Bun** | 5005 | External (Kubernetes Service) | REST API, tRPC, serves the React SPA | +| **FastAPI** | 8005 | Internal (pod-local only) | Agent execution, SQL generation, LLM tool calls | + +Key runtime properties: +- `supervisord` starts as **root** (PID 1) and drops privileges to the **`nao`** user (uid/gid 1000) for child processes. The pod security context must allow root startup — see the [security context note](#general) below. +- `supervisord` automatically restarts either process on crash. +- Port **8005** is never exposed outside the pod — all external traffic enters on port **5005**. + +--- + +## Development architecture (docker-compose) + +For local development, `docker-compose.yml` runs two services: + +``` +┌──────────────────┐ ┌──────────────────────────┐ +│ postgres │ │ nao (from Dockerfile) │ +│ :5432 │◄───────┤ :5005 │ +│ │ DB_URI │ Fastify + FastAPI │ +│ PostgreSQL 16 │ │ supervisord │ +└──────────────────┘ └──────────────────────────┘ +``` + +```bash +# Start all services +npm run dev + +# Or individually: +npm run dev:backend # Fastify + Bun (port 5005) +npm run dev:frontend # Vite + React (port 3000) +npm run dev:fastapi # FastAPI sidecar (port 8005) +``` + +The `DB_URI` connects the backend to the `postgres` service by Docker network name. + +--- + +## Docker build (multi-stage) + +The Docker image is built via a **multi-stage Dockerfile** with 5 stages: + +``` +Stage 1: base → Node.js 24-slim + Bun +Stage 2: deps → npm/bun install for all workspaces +Stage 3: frontend → Vite build of React SPA +Stage 4: python → uv install of nao-core Python package +Stage 5: runtime → Final image combining all artifacts +``` + +The runtime image includes: +- Node.js + Bun (copied from base stage) +- Python + nao-core (from python-builder) +- Backend source + node_modules (from deps) +- Frontend build output (from frontend-builder) +- Example project directory (fallback for local mode) +- `supervisord` configuration + +--- + +## Kubernetes resources + +The chart creates the following resources. Resources marked *(conditional)* are only created when the corresponding value is enabled. + +| Resource | Name | Condition | +|----------|------|-----------| +| `Deployment` | `-nao` | Always | +| `Service` | `-nao` | Always | +| `ConfigMap` | `-nao` | Always | +| `Secret` | `-nao` | Always | +| `ServiceAccount` | `-nao` | `serviceAccount.create=true` | +| `PersistentVolumeClaim` (context) | `-nao-context` | `contextSource=local` + `persistence.enabled=true` + no `existingClaim` | +| `PersistentVolumeClaim` (projects) | `-nao-projects` | `contextSource=api` + `projectsPersistence.enabled=true` + no `existingClaim` | +| `HorizontalPodAutoscaler` | `-nao` | `autoscaling.enabled=true` | +| `PodDisruptionBudget` | `-nao` | `podDisruptionBudget.enabled=true` | +| `Pod` (helm test) | `-nao-test-connection` | `helm test` only | + +When `postgresql.enabled=true`, the bitnami subchart additionally creates: a `StatefulSet`, two `Services` (one headless), a `Secret`, a `ServiceAccount`, a `NetworkPolicy`, and a `PodDisruptionBudget` for the database. + +### Git sync initContainer + +When `contextSource=local`, `persistence.enabled=true`, and `gitSync.url` is set, a `git-sync` initContainer runs **before** the main container to automatically clone (or pull) the context repository into the PVC. + +- Uses HTTPS with token authentication (`oauth2:@host`) — **no SSH keys needed** +- On pod restart/crash, the initContainer automatically re-clones/pulls + +--- + +## Context modes + +nao needs a **project directory** containing a `nao_config.yaml` that describes the data sources, schemas, and LLM tools available to the agent. The chart supports three modes, selected via `config.contextSource`. + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ "local" │ +│ ─────────────────────────────────────────────────────────────────────── │ +│ A PVC is mounted at config.contextPath (/app/project by default). │ +│ You are responsible for pre-populating that volume with a valid │ +│ nao project before the pod starts. │ +│ │ +│ Required values: │ +│ persistence.enabled: true │ +│ persistence.size: │ +│ │ +│ Best for: static configurations, on-prem environments. │ +├──────────────────────────────────────────────────────────────────────────┤ +│ "git" │ +│ ─────────────────────────────────────────────────────────────────────── │ +│ The entrypoint clones (or pulls) a git repository into contextPath at │ +│ every pod startup. An optional cron schedule (refreshSchedule) can │ +│ periodically re-pull the repository while the pod is running. │ +│ │ +│ Required values: │ +│ config.contextGitUrl: │ +│ secrets.contextGitToken: ← only for private repos │ +│ │ +│ Best for: version-controlled configurations, GitOps workflows. │ +├──────────────────────────────────────────────────────────────────────────┤ +│ "api" │ +│ ─────────────────────────────────────────────────────────────────────── │ +│ Projects are deployed dynamically after the pod is running, using │ +│ the `nao deploy` CLI command against the running instance. │ +│ Projects are stored in a writable PVC at /app/projects. │ +│ │ +│ Required values: │ +│ projectsPersistence.enabled: true │ +│ │ +│ Best for: multi-project / multi-tenant deployments, SaaS setups. │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Secret vs ConfigMap split + +Environment variables are split between a `ConfigMap` (non-sensitive) and a `Secret` (sensitive). Both are injected into the container via `envFrom`. + +**ConfigMap** — `helm/templates/configmap.yaml`: +- `SERVER_PORT`, `FASTAPI_PORT`, `NODE_ENV`, `MODE`, `DOCKER` +- `BETTER_AUTH_URL` +- `NAO_CONTEXT_SOURCE`, `NAO_DEFAULT_PROJECT_PATH` +- `NAO_CONTEXT_GIT_URL`, `NAO_CONTEXT_GIT_BRANCH`, `NAO_REFRESH_SCHEDULE` +- `POSTHOG_KEY`, `POSTHOG_HOST`, `POSTHOG_DISABLED` + +**Secret** — `helm/templates/secret.yaml`: +- `BETTER_AUTH_SECRET` +- `DB_URI` (auto-built from subchart values when `postgresql.enabled=true`) +- All LLM provider keys: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, Azure, AWS Bedrock +- `NAO_CONTEXT_GIT_TOKEN`, `NAO_LICENSE` +- SMTP credentials, OAuth client secrets (Google, GitHub) +- `NOTION_API_KEY` + +> In production, manage Secret values with an external secret manager (External Secrets Operator, AWS Secrets Manager, Sealed Secrets) rather than hardcoding them in values files. + +--- + +## Complete values reference + +### General + +| Key | Default | Description | +|-----|---------|-------------| +| `nameOverride` | `""` | Override the chart name used in resource names | +| `fullnameOverride` | `""` | Override the fully qualified resource name | +| `replicaCount` | `1` | Number of nao pod replicas. Ignored when `autoscaling.enabled=true`. | +| `imagePullSecrets` | `[]` | List of `{name: secret-name}` entries for pulling from private registries | +| `podAnnotations` | `{}` | Extra annotations added to each Pod spec | +| `podSecurityContext` | `{}` | Pod-level security context. **Do not set `runAsNonRoot: true` or `runAsUser`** — supervisord must start as root (uid 0) to drop to the `nao` user for child processes. You may set `fsGroup: 1000` if PVC write access requires it. | +| `securityContext` | `{}` | Container-level security context. **Do not set `allowPrivilegeEscalation: false` or drop capabilities** — they conflict with supervisord's privilege-dropping mechanism. | + +### Image + +| Key | Default | Description | +|-----|---------|-------------| +| `image.repository` | `ghcr.io/getnao/nao` | Container image repository — the official nao image published to GitHub Container Registry | +| `image.tag` | `""` | Image tag. Empty string defaults to `.Chart.AppVersion`. | +| `image.pullPolicy` | `IfNotPresent` | Pull policy: `Always`, `IfNotPresent`, or `Never` | + +### Service account + +| Key | Default | Description | +|-----|---------|-------------| +| `serviceAccount.create` | `true` | Create a dedicated `ServiceAccount` for the pod | +| `serviceAccount.annotations` | `{}` | Annotations on the `ServiceAccount` (e.g. `eks.amazonaws.com/role-arn` for IRSA) | +| `serviceAccount.name` | `""` | Override the service account name; defaults to the release fullname | + +### Application config (ConfigMap) + +These values are rendered into the `ConfigMap` and injected as environment variables. + +| Key | Default | Env var | Description | +|-----|---------|---------|-------------| +| `config.serverPort` | `"5005"` | `SERVER_PORT` | Fastify backend listening port | +| `config.fastapiPort` | `"8005"` | `FASTAPI_PORT` | Internal FastAPI sidecar port — not exposed outside the pod | +| `config.betterAuthUrl` | `""` | `BETTER_AUTH_URL` | **Public URL** of the app. Must match the URL users access. Used for OAuth callbacks and trusted-origin checks. | +| `config.nodeEnv` | `"production"` | `NODE_ENV` | Node.js environment | +| `config.contextSource` | `"local"` | `NAO_CONTEXT_SOURCE` | Context loading mode: `local` \| `git` \| `api` | +| `config.contextPath` | `"/app/project"` | `NAO_DEFAULT_PROJECT_PATH` | Absolute path inside the container where the nao project is mounted or cloned | +| `config.contextGitUrl` | `""` | `NAO_CONTEXT_GIT_URL` | Git repository URL (`git` mode) | +| `config.contextGitBranch` | `"main"` | `NAO_CONTEXT_GIT_BRANCH` | Branch to clone (`git` mode) | +| `config.refreshSchedule` | `""` | `NAO_REFRESH_SCHEDULE` | Cron expression for periodic git pulls, e.g. `"0 * * * *"`. Empty = disabled. | +| `config.gitSync.url` | `""` | — | HTTPS URL of the context repo (initContainer git-sync, `local` mode only) | +| `config.gitSync.branch` | `"main"` | — | Branch to sync (initContainer git-sync) | +| `config.posthogKey` | `""` | `POSTHOG_KEY` | PostHog project API key (optional usage analytics) | +| `config.posthogHost` | `"https://eu.i.posthog.com"` | `POSTHOG_HOST` | PostHog ingest endpoint | +| `config.posthogDisabled` | `"false"` | `POSTHOG_DISABLED` | Set to `"true"` to opt out of usage analytics entirely (recommended for on-prem deployments to prevent outbound connections to external analytics) | + +### Secrets + +All values under `secrets` are stored in a Kubernetes `Secret` object. In production, inject sensitive values via an external secrets manager rather than committing them in values files. + +#### Authentication + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.betterAuthSecret` | `BETTER_AUTH_SECRET` | **Required.** Signs user sessions. Generate with `openssl rand -base64 32`. Changing this value invalidates all active sessions. | + +#### Database + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.dbUri` | `DB_URI` | Database connection URI. When `postgresql.enabled=true` this is built automatically from the subchart values — leave empty. Accepted formats: `postgres://user:pass@host:5432/db` or `sqlite:./db.sqlite` | + +#### LLM providers — provide at least one + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.openaiApiKey` | `OPENAI_API_KEY` | OpenAI API key (`sk-...`) | +| `secrets.anthropicApiKey` | `ANTHROPIC_API_KEY` | Anthropic Claude API key | +| `secrets.azureApiKey` | `AZURE_API_KEY` | Azure OpenAI service key | +| `secrets.azureResourceName` | `AZURE_RESOURCE_NAME` | Azure OpenAI resource name (mutually exclusive with `azureOpenaiBaseUrl`) | +| `secrets.azureOpenaiBaseUrl` | `AZURE_OPENAI_BASE_URL` | Azure OpenAI base URL (mutually exclusive with `azureResourceName`) | +| `secrets.azureApiVersion` | `AZURE_API_VERSION` | Azure OpenAI API version string | +| `secrets.awsBearerTokenBedrock` | `AWS_BEARER_TOKEN_BEDROCK` | AWS Bedrock bearer token | +| `secrets.awsRegion` | `AWS_REGION` | AWS region for Bedrock (`us-east-1`, etc.) | +| `secrets.awsAccessKeyId` | `AWS_ACCESS_KEY_ID` | AWS IAM access key ID | +| `secrets.awsSecretAccessKey` | `AWS_SECRET_ACCESS_KEY` | AWS IAM secret access key | + +#### Context & enterprise + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.contextGitToken` | `NAO_CONTEXT_GIT_TOKEN` | Personal access token or deploy key for cloning a private git repository (`git` mode) | +| `secrets.naoLicense` | `NAO_LICENSE` | Enterprise license key or path to license file. Unlocks EE-only features (SSO, etc.). Leave empty for OSS mode. | + +#### Email — SMTP (optional) + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.smtpHost` | `SMTP_HOST` | SMTP server hostname | +| `secrets.smtpPort` | `SMTP_PORT` | SMTP server port (typically `587` for STARTTLS, `465` for SSL) | +| `secrets.smtpSsl` | `SMTP_SSL` | `"true"` to enable SSL, `"false"` otherwise | +| `secrets.smtpMailFrom` | `SMTP_MAIL_FROM` | Sender address used for outgoing emails | +| `secrets.smtpPassword` | `SMTP_PASSWORD` | SMTP account password | + +#### OAuth providers (optional) + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.googleClientId` | `GOOGLE_CLIENT_ID` | Google OAuth 2.0 client ID | +| `secrets.googleClientSecret` | `GOOGLE_CLIENT_SECRET` | Google OAuth 2.0 client secret | +| `secrets.googleAuthDomains` | `GOOGLE_AUTH_DOMAINS` | Comma-separated list of allowed Google Workspace domains | +| `secrets.githubClientId` | `GITHUB_CLIENT_ID` | GitHub OAuth app client ID | +| `secrets.githubClientSecret` | `GITHUB_CLIENT_SECRET` | GitHub OAuth app client secret | +| `secrets.githubAllowedUsers` | `GITHUB_ALLOWED_USERS` | Comma-separated list of GitHub usernames allowed to sign in | + +#### Integrations (optional) + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.notionApiKey` | `NOTION_API_KEY` | Notion integration API key | + +### Service + +| Key | Default | Description | +|-----|---------|-------------| +| `service.type` | `ClusterIP` | Kubernetes Service type: `ClusterIP`, `NodePort`, or `LoadBalancer` | +| `service.port` | `80` | Port exposed on the Service | +| `service.targetPort` | `5005` | Container port the Service routes traffic to | +| `service.annotations` | `{}` | Service annotations (e.g. AWS NLB annotations) | + +### Resources + +| Key | Default | Description | +|-----|---------|-------------| +| `resources.requests.cpu` | `500m` | CPU request | +| `resources.requests.memory` | `512Mi` | Memory request | +| `resources.limits.cpu` | `"2"` | CPU limit | +| `resources.limits.memory` | `2Gi` | Memory limit | + +The FastAPI process is the most CPU and memory intensive (LLM calls, SQL execution). Increase limits for large datasets or heavy concurrent usage. + +### Probes + +The nao backend does not expose a `/health` HTTP endpoint. The chart uses **TCP socket probes** on port 5005. + +| Key | Default | Description | +|-----|---------|-------------| +| `livenessProbe.tcpSocket.port` | `5005` | TCP port checked by the liveness probe | +| `livenessProbe.initialDelaySeconds` | `30` | Seconds to wait before the first liveness check (allow supervisord + processes to start) | +| `livenessProbe.periodSeconds` | `15` | Interval between liveness checks | +| `livenessProbe.failureThreshold` | `3` | Consecutive failures before the pod is restarted | +| `readinessProbe.tcpSocket.port` | `5005` | TCP port checked by the readiness probe | +| `readinessProbe.initialDelaySeconds` | `15` | Seconds to wait before the first readiness check | +| `readinessProbe.periodSeconds` | `10` | Interval between readiness checks | +| `readinessProbe.failureThreshold` | `3` | Consecutive failures before the pod is removed from the Service endpoints | + +### Autoscaling — HPA + +| Key | Default | Description | +|-----|---------|-------------| +| `autoscaling.enabled` | `false` | Create a `HorizontalPodAutoscaler` | +| `autoscaling.minReplicas` | `1` | Minimum number of replicas | +| `autoscaling.maxReplicas` | `5` | Maximum number of replicas | +| `autoscaling.targetCPUUtilizationPercentage` | `80` | Target average CPU utilization (%) across all pods | +| `autoscaling.targetMemoryUtilizationPercentage` | `80` | Target average memory utilization (%) across all pods | + +When `autoscaling.enabled=true`, the `replicaCount` value is ignored. + +### Pod Disruption Budget + +| Key | Default | Description | +|-----|---------|-------------| +| `podDisruptionBudget.enabled` | `false` | Create a `PodDisruptionBudget` | +| `podDisruptionBudget.minAvailable` | `1` | Minimum number of pods that must remain available during voluntary disruptions (node drains, rolling updates) | + +### Node scheduling + +| Key | Default | Description | +|-----|---------|-------------| +| `nodeSelector` | `{}` | Key/value node label selector — restricts pod scheduling to matching nodes | +| `tolerations` | `[]` | List of pod tolerations for tainted nodes | +| `affinity` | `{}` | Affinity / anti-affinity rules (pod or node level) | + +### Persistence — local mode + +Only relevant when `config.contextSource=local`. A `PersistentVolumeClaim` is created and mounted at `config.contextPath` inside the container. + +| Key | Default | Description | +|-----|---------|-------------| +| `persistence.enabled` | `false` | Create and mount a PVC for the nao project context | +| `persistence.storageClass` | `""` | StorageClass name. Empty string uses the cluster default. | +| `persistence.accessMode` | `ReadWriteOnce` | PVC access mode | +| `persistence.size` | `1Gi` | PVC storage size | +| `persistence.existingClaim` | `""` | Name of an existing PVC to use instead of creating a new one | +| `persistence.annotations` | `{}` | Extra annotations on the PVC | + +> You must pre-populate the PVC with a valid nao project (a directory containing `nao_config.yaml`) before the pod starts. The entrypoint will fail if the file is missing. + +### Projects volume — api mode + +Only relevant when `config.contextSource=api`. A `PersistentVolumeClaim` is created and mounted at `/app/projects`. + +| Key | Default | Description | +|-----|---------|-------------| +| `projectsPersistence.enabled` | `false` | Create and mount a PVC for dynamically deployed projects | +| `projectsPersistence.storageClass` | `""` | StorageClass name | +| `projectsPersistence.accessMode` | `ReadWriteOnce` | PVC access mode | +| `projectsPersistence.size` | `5Gi` | PVC storage size | +| `projectsPersistence.existingClaim` | `""` | Name of an existing PVC to reuse | +| `projectsPersistence.annotations` | `{}` | Extra annotations on the PVC | + +### PostgreSQL subchart + +nao uses a PostgreSQL database to store chat history and user sessions. The [bitnami/postgresql](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) subchart is bundled and enabled by default. + +Disable it and set `secrets.dbUri` to connect to an external database instead. + +> **SQLite alternative**: For lightweight or test deployments, set `postgresql.enabled=false` and `secrets.dbUri=sqlite:./db.sqlite`. SQLite requires no additional infrastructure — data is stored inside the pod (ephemeral unless a PVC is configured). + +> **Docker Hub note**: Bitnami PostgreSQL images on Docker Hub are only available for older releases. If your cluster has restricted Docker Hub access or the required image tag is not available, either set `postgresql.image.tag` to a locally mirrored version, or disable the subchart and use an external PostgreSQL instance. + +| Key | Default | Description | +|-----|---------|-------------| +| `postgresql.enabled` | `true` | Deploy PostgreSQL as a subchart alongside nao | +| `postgresql.auth.database` | `nao` | Database name created at init time | +| `postgresql.auth.username` | `nao` | Database user | +| `postgresql.auth.password` | `""` | Password for the nao database user — **change in production** | +| `postgresql.auth.postgresPassword` | `""` | Password for the `postgres` superuser — **change in production** | +| `postgresql.primary.persistence.enabled` | `true` | Persist PostgreSQL data to a PVC | +| `postgresql.primary.persistence.size` | `8Gi` | PostgreSQL PVC size | + +When `postgresql.enabled=true`, the chart automatically constructs `DB_URI` as: + +``` +postgres://:@-nao-postgresql:5432/ +``` + +For the full list of PostgreSQL subchart values, see the [bitnami/postgresql documentation](https://github.com/bitnami/charts/tree/main/bitnami/postgresql#parameters). diff --git a/helm/Chart.lock b/helm/Chart.lock new file mode 100644 index 000000000..f85a61fd9 --- /dev/null +++ b/helm/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 16.7.14 +digest: sha256:3d12513cb378249e854df4b9edc49e63c5ab55842efdcc1b3ca6512b371ac811 +generated: "2026-04-28T16:34:47.673475258+02:00" diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 000000000..0857d77c3 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,29 @@ +apiVersion: v2 +name: nao +description: Helm chart for nao — an open-source analytics agent that transforms natural language into SQL queries. +type: application +version: 0.1.0 +appVersion: "latest" + +keywords: + - analytics + - ai + - sql + - text-to-sql + - agent + +home: https://github.com/mfournioux/nao +sources: + - https://github.com/mfournioux/nao + +maintainers: + - name: Maxime FOURNIOUX + url: https://github.com/mfournioux/nao + +kubeVersion: ">=1.24.0" + +dependencies: + - name: postgresql + version: "16.7.14" + repository: "https://charts.bitnami.com/bitnami" + condition: postgresql.enabled diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 000000000..c04997b68 --- /dev/null +++ b/helm/README.md @@ -0,0 +1,214 @@ +# nao Helm Chart + +Helm chart for deploying [nao](https://github.com/mfournioux/nao) — an open-source analytics agent that transforms natural language into SQL queries — on Kubernetes. + +## Prerequisites + +- Kubernetes **1.24+** +- Helm **3.10+** +- (Optional) A running PostgreSQL instance, or enable the bundled bitnami/postgresql subchart + +## Chart structure + +``` +helm/ +├── Chart.yaml # Chart metadata & bitnami/postgresql dependency +├── values.yaml # Default configuration values +├── .helmignore +└── templates/ + ├── NOTES.txt # Post-install instructions + ├── _helpers.tpl # Template helpers + ├── configmap.yaml # Non-sensitive environment variables + ├── secret.yaml # Sensitive values (API keys, auth secret, DB URI) + ├── serviceaccount.yaml + ├── deployment.yaml # Main nao workload + ├── service.yaml + ├── pvc.yaml # Conditional (local / api context modes) + ├── hpa.yaml # Conditional + ├── pdb.yaml # Conditional + └── tests/ + └── test-connection.yaml # helm test pod +``` + +## Installation + +### 1. Add the bitnami repository (for the PostgreSQL dependency) + +```bash +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +``` + +### 2. Install dependencies + +```bash +helm dependency update ./helm +``` + +### 3. Install the chart + +```bash +helm install nao ./helm \ + --namespace nao --create-namespace \ + --set secrets.betterAuthSecret="$(openssl rand -base64 32)" \ + --set secrets.openaiApiKey="sk-..." \ + --set config.betterAuthUrl="https://nao.example.com" +``` + +## Context modes + +nao supports three ways to load its project configuration, controlled by `config.contextSource`. + +### `local` (default) + +The nao project directory is mounted as a Kubernetes PersistentVolume. + +```yaml +# values-local.yaml +config: + contextSource: local + contextPath: /app/project + +persistence: + enabled: true + size: 1Gi + # existingClaim: my-nao-context-pvc # use an existing PVC +``` + +> **Note:** You must pre-populate the PVC with a valid nao project (a directory containing `nao_config.yaml`). + +### `git` + +The project is cloned from a Git repository at pod startup. + +```yaml +# values-git.yaml +config: + contextSource: git + contextPath: /app/project + contextGitUrl: https://github.com/your-org/your-nao-project.git + contextGitBranch: main + refreshSchedule: "0 * * * *" # optional: pull every hour + +secrets: + contextGitToken: ghp_... # required for private repositories +``` + +### `api` + +Projects are deployed dynamically via the `nao deploy` CLI. A writable volume is created at `/app/projects`. + +```yaml +# values-api.yaml +config: + contextSource: api + +projectsPersistence: + enabled: true + size: 5Gi +``` + +## Values reference + +| Key | Default | Description | +|-----|---------|-------------| +| `replicaCount` | `1` | Number of pod replicas | +| `image.repository` | `ghcr.io/getnao/nao` | Container image repository | +| `image.tag` | `""` (→ appVersion) | Image tag | +| `image.pullPolicy` | `IfNotPresent` | Image pull policy | +| `config.serverPort` | `"5005"` | Fastify backend listening port | +| `config.betterAuthUrl` | `"http://localhost:5005"` | Public URL for auth callbacks | +| `config.contextSource` | `"local"` | Context mode: `local` \| `git` \| `api` | +| `config.contextPath` | `"/app/project"` | Mount path for the nao project | +| `config.contextGitUrl` | `""` | Git repo URL (git mode) | +| `config.contextGitBranch` | `"main"` | Git branch to clone (git mode) | +| `config.refreshSchedule` | `""` | Cron for periodic git pull | +| `secrets.betterAuthSecret` | `""` | **Required.** Auth session secret | +| `secrets.openaiApiKey` | `""` | OpenAI API key | +| `secrets.anthropicApiKey` | `""` | Anthropic API key | +| `secrets.dbUri` | `""` | Database URI (overridden when `postgresql.enabled=true`) | +| `secrets.contextGitToken` | `""` | Git token for private repos | +| `secrets.naoLicense` | `""` | Enterprise license key (EE only) | +| `service.type` | `ClusterIP` | Kubernetes service type | +| `service.port` | `80` | Service port | +| `resources.requests.cpu` | `500m` | CPU request | +| `resources.requests.memory` | `512Mi` | Memory request | +| `resources.limits.cpu` | `2` | CPU limit | +| `resources.limits.memory` | `2Gi` | Memory limit | +| `autoscaling.enabled` | `false` | Enable HPA | +| `autoscaling.minReplicas` | `1` | Minimum replicas | +| `autoscaling.maxReplicas` | `5` | Maximum replicas | +| `podDisruptionBudget.enabled` | `false` | Enable PDB | +| `persistence.enabled` | `false` | Enable context PVC (local mode) | +| `persistence.size` | `1Gi` | Context PVC size | +| `persistence.existingClaim` | `""` | Use an existing PVC | +| `projectsPersistence.enabled` | `false` | Enable projects PVC (api mode) | +| `projectsPersistence.size` | `5Gi` | Projects PVC size | +| `postgresql.enabled` | `true` | Deploy bundled PostgreSQL | +| `postgresql.auth.database` | `nao` | PostgreSQL database name | +| `postgresql.auth.username` | `nao` | PostgreSQL username | +| `postgresql.auth.password` | `""` | PostgreSQL password (**change in production**) | + +For the full list of values and their descriptions, see [`values.yaml`](./values.yaml). + +## Using an external database + +Set `postgresql.enabled=false` and provide a connection URI: + +```yaml +postgresql: + enabled: false + +secrets: + dbUri: "postgres://user:password@my-postgres-host:5432/nao" +``` + +SQLite is also supported for single-node / testing deployments: + +```yaml +postgresql: + enabled: false + +secrets: + dbUri: "sqlite:./db.sqlite" +``` + +## Exposing nao with an Ingress + +```yaml +config: + betterAuthUrl: https://nao.example.com +``` + +## Upgrade + +```bash +helm upgrade nao ./helm --namespace nao -f my-values.yaml +``` + +## Rollback + +```bash +# List revision history +helm history nao --namespace nao + +# Roll back to a previous revision +helm rollback nao --namespace nao +``` + +## Running chart tests + +```bash +helm test nao --namespace nao +``` + +## Uninstall + +```bash +helm uninstall nao --namespace nao +``` + +> **Note:** PersistentVolumeClaims are not deleted automatically. Remove them manually if no longer needed: +> ```bash +> kubectl delete pvc -l app.kubernetes.io/instance=nao --namespace nao +> ``` \ No newline at end of file diff --git a/helm/charts/postgresql-16.7.14.tgz b/helm/charts/postgresql-16.7.14.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a0c0ae9f186cc9bd6840db2ebae94c9f85346477 GIT binary patch literal 84834 zcmV)oK%BoHiwFP!00000|LnbSd)qd$H+uf|r@(c0PwlQH$4=Ak=A7*NsQSHoAG~?)Hwc2Tn#QD|DPQmY{vgFlEwJnI)M-RpdW<` z(!%`XfB5|QcboBljAV@e5srU*?2LjXe*>}o?%Qvh#{cl(yYKY)AHFy|MEeh{0RMwM zTXH*U(-%JeP8de1oZK(B7;|C9529rJk{vs#({luehp-qJaPgAe!{3axSm=!SOBRWi z81f*Xhbq%c=0)xePuhO8-}6(*=3Z@(+mrs=T7KgLm#@?=)E z69ryAYae{yJ}|#=a8Ub=-)6SC$oUfLe9?YZs`U_x2foWgfv9w1#~t$4Vfz_8PMz@g z>?j9O@Z=UUZaK04o+koOe#veR;b)xxl=W|UJm#UtL)V8ow{(joFNS&(4o&h?sx)%_ zm;3t(ANV3o#;t?)MEfNtr{)Z3SX)fJY2`KE>INLPKej)@_l2AIaY~QQW%n=t-!rzO z{uaB;7t9G+1np!Z$`Y5esP`*((^kR*q97+=o}wCd6GnIVnSPW&;{)I6`GKE~p$3mK zF+T*55_r^RoukuZY*-B0!gP=MDHB|yJDeh0tP#2*Zx zBUG&bc(}*nByzczL6h5C!w69vrfDnyGKE1KKDFh$*irq)8wcrcqjB(;zdXOYZM#8~ zdHo~`({{+yeMblmauN81dm{|e*!M>R<7bO9v=) zKQ&_Tr+f!P4a7x&!Y?-f1_iTW^h9zP_J<3Vrs*lFkjjzPdK{J{`lyWmKA z+-n0?SrQaI9r)=m>mdX=u`6nsm?LtK>2otn@23gp``E~uIkc72+QD~in4;fqeQvI| zNje$id~wn_etps&dCUIBRQdlM$ol7p75V?|i^K0X^8Yc?fB!$NR*Qi^YRUO;u>Mx- zuUlJRu^;4oW+3KnKwJ_>FLG^dElXHDh^Vj?6k8wO-lDALcP#1~-TT)LvOeH^cXxN~ zVy?8KWU#xnb(J_CA34blr~y|)=(33V>7A2sP!B}vhXaH`;pVa4SYz3S+#LEUVnD>d z<D?mt`5Ojw<>R~3Z9A+AUcEbe&B@eWn zI6ze4fBil{OvAtcbWl8^L~#5vAo0~#?CR{ljxV;hy4@~FNf8Bn3lz8!GzjJj$e7U> zN0H;m+3F&uG=OTiw*Jbl{1~)M-(%w_BMN}#Iw|}gl1>~G1qC8v&aLAIPA}lhfhX*5 zS&t_mmZ4J}$Q4PDI={m>ve$qhPhr{W^8ZuGCkJc@CtQN?7LPc7$~Mr z5DkhB85$9^9`uYj@de-Aqc)+c$P?FxnaVw15)08(4wftHlSFeGrg=kPYTz)^*s~sR zPM{74$AEpP&aEc3BGT&4RMlcvjUUo3f(IWc#=ZE!P5ulhb5|=>fF*$nJ^Ju75O|Rv z$-OOxyE)SvIU)R$Ku>>!3G;JjkHvBV-~fIgnne|TdY5KvO0A)@OJJTSKzP4pKIr_Q zC4vTKQ1e0i@cEzM5jL4UZ~wXd41b-!M?$#uQPF~~DXyrQNCs<*Bp4G*3j2uz;{hWD zgrsPo6lDY@ZC60~7{DD96VZF1Jb1^~_ zzYQ`7mU_g9lPN_>Q^BSdObQ#}d)$~P1z#dKVc3x;8`--06Qav*ogm|E#~pGIIysoI zd8Cz4x00t}0n*i2@vc)NVS`4Es@Nh^R&xun>ibUk7 zLHK>&CBj6?ZPUabIX7I2V$za?kYY30GkU0cFDktO)&(G{K7l@RHw1cRPL@U^7$O&N z8zaiqV%?mq9M^?Kvl4fn-=}$*LPM}#0iu~uk{weIk(@tZeTDcyGly)b4O8v~QLoID zC(Pj2c~jdUm%lnYcmX5xb9BdV5tfo1HpV~^@dP@6tmg|C9+GYdstu^JBTz|DQ|o64 z5)IYlKp`gq6n^w!bVm`<6i~oOX>(yglm!h-=|wV9&HE3aRr(~_4c-9UC&NrsRfF){ zhfKIf0E{xG%>rXK?#u7A^3JvlDZbm5bMicifVhV|Lsb{la#xg;1;sg}%I_eh5`Q*I z^~U)m?W!8uJj9vebrG#Dh&JABONIQ-hiR%*Jcr@jbDhM1h${b}1TTT?+RaGRPE}FaKRnh05 znxS(m8BNG>0y`wVa$_$dh3ud_EnU#kWRcDPxR;dj6TvKzSdnw81brhmz z(^ac=De8QSdP`#rbPqDb1MMu@!^uhzN*PzBWA#BjgI}r1dlHI_)@zO|T=&K95%fM9 zX(4~i`+i8v6qr~_)?$l_m9*AbZUDdRM?71qT(+`k>+!n8%3B)B%#3)7Zb zNbb{{R^1!T-XM$Hg8FG`-2%NIFb3Y*B2EdIhF&X?jcj4N3<=bNG2L(o*L`_xMLp&=r-)Y3cut-&dZ5YTe{`dUvA`SA?tH z^MFp5G>WaQj-3B#6te&{t$-9zoZM(kP?acVr8G?s`2d&dW&RbU{9XibRvUC7zbxcZ z&f^>?p1G_I5blSLXrGCntw&>jU~96X>wH+K^AGdHF#V;8G-94 zS*6wCJ*D=KOSM-fFL4FKmerMP0+>nHSWChP?c=i5sHZSwOqMR*8I`Eso*j42x3=*A zb2F17IVHt&dXe_OvF=6wjR2N5?XGDyfVm5tmK0>Nb!)aJhq@5VMv-DsYiP_wFj0)g zF4Y?PVyUra>)L2s`{o;JkZL_zBTi&dvsjxQOJfO9Ww#Gf2Q-zUbe8yJ$G{@*F3zA` z13wI<`m3~yWF))(LHr9O<$n)>O3-}v*O&i|ANVWiJ#Qjyk`YGLxJi=NO*H0=w?bC&4cr58%N=vau`%@ z0{hjr$a?C50WkE5qb*Wtxo-faJ)n$%PpdalE(^}6a5q>_EPe~RAc=h9_dtq{r&Q9; zYgC(tXz`M20u7@imBKJwSS(@Afj}c85}8K@8-aKu7A0I-<2uAZ?m|5~ogOfIJ;<7^ zlOl#($&@qVjXd4^_Wu;{YJe zGaA*CwGF5RRvpf$$#OKgX{yXeS%Ly+A9p;Q0LBZyqBQf;*Ygj{#uY0y(qX0i-$CWk zab9bv0o7by{_^(Xn7ViV_VVhdi<8Ui{HeKDuuHRA`m^D($xQ=otx=yI za8P)C*Zcx&wfeADYJm<6x^VDHxAs;3iE0MzvsUDsXjQA_CF53-g{&1ZbjD=&Ot%P? zhB0gPSxe3@9NCUsO(5fxxJjCD3hdPLU!$H}{O1;vVk*+kk%x+5T`{R*Ni$Yy?V{+l zGOr7=x`PSJperN=X*HY01KjE+`+y;y@gt}Zc6C`v1OF5Cs)hm5pE0|#K_P*+NJl`D z$Ups3Dp9^DSrdEad*jNf?7NO}!Iiw{;p;_^gr`?2hXhQ(|1VzM~iwc~;BiZfXDCFpUy>GyVT?nq#poRC9od z(3b5>wr%9u-rB;^MK4d}qVN*`*@mB0@ZlBAk@L#agTv>Pxs(akGf5>w4a73<#pDYr zvNZuPwoy%In4d78wJl6eRuf53DAT)$Y5a3pxYLC%=Ipa26e-gj)x$UOnvAM7gCkxV z04og3BmR=re9o$8j7?C}OY%CdexRw<5*ys=MuCrt5GYfLqx+#YeML!qnq;0>|>k-z$|8WX4^N%r~LiPbJmVUv_w8oBosK&;Is6B40h)1kWC z%1U@vgKx+}-b%mi+aZ%$mu=2y_}W+p-R7CZU7=Rl#@< zJ7{VesE#EK8|ZSEU$?FF3VBH0b#iaQ{wwH8Lr~Mpc<^)2X==S>P8v$3Z=)vyIduzZ zg2_LNoE-i}ND{>TNRP9!I%=nniIEdZ2IB@MDxSu6TxCQgoy&?xPHZ-n^mW7Ei*$>U z9SmoGE+SPI6y=_xmEQ@F&KwYkTnr@_4P7o3oDY*4&T@Q>q@|^7z00IYOdAcEbfc&F z;5RoEH?cq((e56yIqqr?7FE6&E^Ps-)L%m$_&u-JRyj(>AA_z-WU?4llD)w#70_gfpi6p$+HN=fSEg8hjBor1 z0HMVWYCkFp5MRW{%?1C)#|>p~__(Qj#q88#Kcd}WPcKXh8$q!yc6r_l$*rLrY8{40 z9{!xZ#5J*KK@XHhqF1w6@2mDc=4!i1?PKgz<*|c#bz9V$4cL#KOLf#$-}c)m$VU8} zCnI0bEGjJ#DT_f%<%g4W@@l)Gx7(#cdhld+>`M8)F}^AbK(JkOmoaIhB8tX2{=V9a zlIy)*+|p1W%ZiJP(qySQm{inua^_X0Ea^{xCme835T;N@?v3b~AGQV&8FHZ7jMvJ? z!tuxalk$uG7vKNo@cAx!Mmo}2tH@Gnt=?5uF6rG|*h;RJjzI#THMN~&VMpM?qboTr z8SE7M=uQkex%$W*SB4%~V-OV32y~VpHI9Eo*LXYtC0VHS{6TM(e&LU4DxStJDQbF| zT#ac{9I}!)BCDZ!-hsD4Pjo`F9_8PFq9cd~1Gz+0u0e$eA>RmE91<&5j!Dof$wp0C zcq#q%<+i3$HFE!@lCNCV)ick9oWEVHb-tp#&Z^Z$%Car>FD_PetKP16X}UoUVhAQ$}|5|?wr`1{gQ2ZUwzBW-7*{;$D?3Z zscSjxO5aa&|2?INYhI$P+tBEnX&`ZzL{M?agGrynudsFDpoPW^)#?`To|e;+0F#iG?m^^Hs+_2e&28Y^ z1HcqLKb*uzo-Ak`LD}g%7XJu?2JAZVBit3?tZZw$xZ%`ctcv z{#2jqzVb8ARVw}`ctB&p4yx~pdP`MR>#hApDN|ly@yQt_raXM=C2s3V)I7Z1`cn-o zf11Q<;?Y*!2vpm2U-JkH8loaEc$xKDuBL(e%A-S7aIr5|Nww6+;^D+2DIbvSq(ht_ z>P$k9r!X-%9g%j@w|r`4sA`w(s&eUuQ<6YtJaOm0aee*8eh!Pf%;#dbwX5W}_au&lb!ZPv5GELtE|%o!r^eyBgZmU>qGYYzNX@0m6gpqCWXzqJxs} zf(*pXapn|OSkUej(HQ;RI*AH_->} zzF!=BRp$xRrhPsim301rVznvQVLzo`RP)RdIRqVPb>L?-uAi6+cGnPAaE%j9fIyT#!oV~lp;mTlGC zS#?{U$3vTwG%l*rWE4VuN)@W$d7k6>UctF@{vOuw8lTJ$1603|V}s|T z;A`A#!S|qIT|rGlv6dsL?M{qQbE$%R(}G#KFd9X1X6==|VNkHqhH`lu1}ljYAJ)$> zrKDlG(hN10vR|r?9(4=r9Ll4dMlCdQKpPi$(3AXb$xRzIT`2cKyOLw~5GAC;5=qb* zjDj8-r^{z|g&+BJa;k;>ZDAfcdrbfLLe)1c;EJKV;VD{Ce;{e{a(G-Sv_Lse5n>6>J(n0ovyb;OO>Czm&zE=fz#-##q)1B6vI zpe397RgYtAh!#xg7I|gv?kSI_lyyV%({%^Qq#4#|?CnAO1^Z9@=R3nvMf)ddme3iz za(^m2AEF2oXEPG4$E2w(@2xwmvuxlFLoxuL0_7I%B4n(Ro$n1K7FmkgVUx zm+xI}L%-TRm#6P!R;t!~w8Vo=Gxo!07pWFZbxAFk|1sOsa z4F&ODzMG*X9v!ZgLgrUQlTlvVY4%2+{=+>iCE2&c#oclTUx?oE@KDNDsoFjyqQ!8D>k$9NZ`- z18;59>A;9SwX4jtO@pev!WsviEOucF-Ek2KHFXtP=i3FuW^r85eoY!$qz-y?McLdg zcN(QLiO|YivBe;DO{vtWz2A^^ai8-@8#iH2b!hjh2h*nSr?QaYaVLqv7sg4INV|{< zrrQI>ajqwho6<=gvPI&tbB+sLPaES)A{J*^n)tm;9%ZHWfv({dl_K{8{^Fyo3H%$5 zC)vWwq2EstY2+sP15&;s_HFy%*&YjcS{zuEy*F^#D}5j(@8*2lf?D1eKcN$K#cK}+ ztoR(MhQ7$RMPt;4(6;a8uEQGt!rA7fah|2TZKC(Nh|ldAJ}33NEamLYduj5$7>=YS zT7EtOA%G{X14Dl>L@R{GvB*L*W;RysJir6bNk*AP^=3w82(*fu_%ald-anlHdU8Cm ziCJ7=tIYde6*Se-S78 zm5_SYdE|X9D9#n0(l13AD7)_?6Aj6+xXPlR4? zOoVL){~2L0pgp}?Mzbi<#s%twi!*^{QxSQ^49}vEST2P&a-G}^HI)t{A$yreEOXJ3 zF3nAFP*mz%BU196KH71*X4tMSL+Tep{hS~#&!DZELRsX&>AtE=`JNE}?K_;Fp!aaZ$IEu^JH$phSN(ozx( z(e`$i{;CcadKF3EL(#RVs61TxKq;S0vgVP0H|TXq;%r86kpx5Z;a^?)%NQ=TlDq^F z4dVv-kC-`P`EY65s##c?%&UqOEF&Jge`}WNXP1~`obrl}PtGq+jyhK-$1h1?Y99Lh zaO5VH>B>gifD~Z<_K-Qq~R$Xl_v%&uXZy)mdSE@DB_!fOk zXybyy!r0Q9Pn*Q6Nda>YS7wBLF-Gg5hpYP8FLSi!AFi5^JTz(#JX{T*{nAEl=5Q%L z-8P*UZJV2})ru-X5q)y?gf+7{3XQdG6b-9XKU{QgTI$Oi=h}E(oggPMx_jfY>N2M) z>d1>ZWcUR$hu)jc9k>j4^)$a!g^!s2Y@nR$-s%tMo=n_p-5#>bqobGTXNaLGbH^a% zbEcf9Oh+obp+-49U_dttIIBG5{Unb+qD|AM$D)m-C#W6rT@0%4yL+5k$D$~3tQ5~;gV6s z<&7?uyUY)~hg@u)=1-<16ei#L=NbT{GM*H(s0EN?z?MsPq!B(G$(YrrTsN zyl0nbq5>Ge;G*~e%FBcONJpPGZFA*@$2?*o+aZSyeL1dj%PlS@+ihx1lL^yojl6}& zwk4m64Y}l3nq%9V!li$%)X*9=yN@G;wQFWK$mK|}0GVsP?>4qZA3h2q*9nyQnL^kd z6CW)%m33&lla_oK7*7XsFYa`lxT|i5BbOr&e~O5u2d_qi7CVAvYzX!fsT}whv>}jL|PP^|NVDUYdXcPK3%glmD_%n*QAElNEOktHAV2E#Sod<8qaqI zA!tdy`#SP?aj@rJ$CfHGU|fD64!X1{iF(EzO1Y@gQEc>Gdpd)fkDxd2=B-hiC(~M^ zs=?N%=!~U{ASUjXQ^y#ARDlhkU!y%XAGSrndAzNNxvn)%GLU11b7~+SrYX8Lekgq@ zsX3-r`-I7DRF*cq*fYo?+YF(4ZCaI7Xlz7Rw7&?b9 zzI*97ZAk1~txjZwQ@S5siMl;pZ-YeLtBZH)3?2)g&zUAyB<0t|*u;5kwx;YgV}phC zA?i(zq%er@nH-pdBfIaz6J zFwhTV*Eg*c!G7n{`@ifoxgspT-n!S@&a^Jo_cKr=)XBO!bl}gtr$sHMYhAV4>@CJt zx$_0zBRew2bD3_YK#w3^XLX8K!`L*b12sIYIzStCHb=6aHxKJ7+P1zmaBtSNfm;36x87p* z*e6Xqi**!Da*V>J$uL=}nInHNRPp9YSD2u)7hNyhDx@yZmb_e7=(uZbb^9`29D5zx zw5Qsb;)-gz8^(HU9>p(o(g|&iT+ZfB0R6A z?PO)MF|JE($|&nHqu?8b*iM(C{y0Gta(Pv*K@c0C+lGW*Kn83RueZt*tBnzshCu02 zCXOhaQrE2O5`zd9IaI_Y^44j~lilAW!yJT^!2+Lnn@O5Vt~YDDyCY|{M6W_=^aM)IoHe)yUTQ5$aTo~c(f zTLOM7uK^K5jDDOSE86U{*(N;4l_XK!z8bwQ$(u9KiO&<>_dnKVzh_ta%w1nzvdjX! z89AmRY)2n&_51Sn5}t0XHPezkPXnm0{1N}X*0!VS(i!r7PaGe-89=1YDBh^k^Cg?H zq20Xvc-yqP)WJ8}5Y(6YY|^61Mo*)$!N?7@E(K;?r{{QuG+wTS#^%uQ?2yAnb*!-k zY`MvL-ryW)&Zp`^qfUPLX;>YU%Kc!QG$YxMO|uSA)MW$@NaQuiYPm_u8O*lxznyYR! zPRj}Iu;$5rY=E+7jtvY8c>_qQ# z*2h|%`<~^mWw5OjSeY|G_sFy&cFOE-lWNnPWAmerx5_o-zgPaNQoK-3?UF_+fEP($ zHGKaknn~zgh2Bz|p#F8qer((!pL#M%``O@q*H}Vy^}rw_yIgLwzldZ#HfbPc*==xa zNNY|q`d8h=esy*Uf4a$d6X#~WWNov_62BpjQ5T8&cNb?;gQm!cO0rNmL#pY6_!nNv z9f|bAOEfn8YhS77bX}i5-m#ZQ=Zf&WuB^F;fmHR*UrRfX&AcfbYmS}NIrf|7P4!+( z&rk5`X>YS*vq-wHy*+dvk?pdHvAM);GVeU$LF9O6elKwnzu060o<{G{V1pT0pmVSt zii_3~c=b0$=?i?Z*}a*O5c&aa&~$5GQ)s^GoyoOXjXeTBiqkR7FMVk?u6Fi4e!CAf zRZkJbk+-?StUlTJsuyD;{NMSRKWZ9Csi)z=t3nfS7#4zgf{xM3HjP7+j^h4ivu$d! zkvA49nyTxh(9F^VucdZ3TWpIb8$Auj217*u#6wQfqA4@eIViR@-q7w+jA6lpehZ_i zfS|!~b9s`=YveX=pHq_U*u-=f_9g9+9r-b+wRL_rzYIBPy!n0ATy3j;E##n3NhT}9 zi@XJcp&aVt0u(KR+*t9-a+2|)vDa&xu>RrLn?j_| z6B~X0@ZM{Yn);~hiIN=KGeY~-kBJ!?OI1_^F5BS){cw+s96_;{1MYbEZ@b-I30qcy zP`CWt#15Eg-ydX2&AHX)dTUq$-SJYYOgauRKm1i*4dn=SMDJfa@$x&nHTmaT$r%uLIHnhuNu28bVPA?ZFd?P2WDKJrQ-Ey;_DLQpi2k#02X^CCRLyvoN zklsBzQH5qBv?F~nxhI`u@E~oHUkO*r`n1^KTy%F*!f-16ElPKKm{x%Rt+2yedAwe6 z8IV=IW{7<%uP)<)S~3h@wECu?d9W+CK!0(9=G|#kuMV@GM^+8=`bPT}8t4M3p>XI3 zHXW&2i8K#(a-CpDda$nW2@0HuFmn~@sVbJ*h0m7mC$2S9&zrH!nO03PquvT{)2K;> zF3CdXq^$2Ju*B}6llTgz1?X-{z)-VZnavKk9n_g|z2O_Ca)GDpQ~wrKXY>+!a*}o) z>>5JlHq@*6U4^sh@a?p#{gJ!mzdPisIH~876|*Cw8}&9|XI=vFC14jP{1m!&Jcqcb z$_l8Wh48EtI5U!!teC)Vwz!y(6%*hUyl++0VstlKT$E+S8Un{Gv7>;Hy|I-aO4Zj-u`e{#xXAJx4J04Sgb)7IV^*R$o<)hpL~HGSlQnQH!1WjSCS3JR*RhyylVm+v&1gHQ+%F!-|9<74GXXeN~*6q-^paqSURXD!;~p|72PQK)IGng z=IlI1&%E9#*kOIE4ji+@F7R)8$b~phqTa0Y1$n*7UAj~j{nzWaFZAv6tCX5tF{faM z3k2T@j(OmWfrmw*S3{Kd>{Mn0vJ3EbVVP0JLMy?!x(N_^UKbC03U)D1{K#uWP1k!) z+13&?l>niGIkCg=Y*Cg@2Tz7}gKTRI9)^?d);({=uJ8Ckmhh`#!o@HO>KWfX`%z|t z#e>SXcIW{@-*xMrH)AI2;JET+9o->PUu<5*$Q?x zg0@M{t{HZ-<*Z1ZB+cSEma~PNHOsD*l(pZlD6q3y&Q6uHB{)|&S*>SF*jX)S>*ega zfTog_oGoEzwVZ8`v+D)k{QS;pIol{_*9kquu1-m5#?ERv+bCz(4ZCjL^U{6=*}~7v zWqe(ka=k~&Q?|7Dr^D!xvc}8P=-MQYHxaB>vhwAT*jId|n*+N` zW4hIpsMf2nD^1dMLe3Jq!0&T+><0YjD7vxr)vobopHPO$D?mnP;gu5=Jq{Icbl`FN z=xw3KV7a~jv}A=HF#9M=fb*ZjUltqNjH!@M1xR@n%yHc)@laW#ZO6;#YaOY9Pslf@ZItOTuFCyxBFP6e8dKrow<~s z#Z=(2`uI=@5b6YX&bT@qXJH43xM?tLJ&V}g@Fe6xaTjrMS)nY?e5ftyJMPNKSWH$v z6?NEUaU5{;rE>zJzzU2LbSHhDi27E{)YO<3#ZJxHnv%L+de2Ueb7ci;dANRjjzo5p zXOqjaYPANM>L% z)Pho5RzBIS$(pcBvaloG)g3dd=y!EAmluBcUOr6vv^h)NuVs9;*h#LqnrR8xy@^7+ zCaGJgw}jo-q5(Tq_>p?M-1KSu4l>+=8Zwv^7Z%tRg&(D-%THGOWX*KP9q(-zjFEJH z%yqdgv!lD(!jBKZ!s*lKj%*(1nto1%>jWxxiUoE}g+Budi=|JaJJ*Swo*($B&qdoy zqC#Zc%V>L1pP6~o`9;hbuk2LK6Rs<^J7;IxmBodve&@Q;2#vzH`f8`+SPy~)-KoN8 zP{PuW-t|vTfY3!2;-2fVG=6v0^V0bWH0?%^4Z}hEF7H66;n!7k!<#}I-Lu!2g{cxu z6#>G7GN>c3!3xc4&f?uMoxpd5z4fycmq#C61iS>p%7E8VZOiS;V<#(H0Cx8cPtinM zleM8<9c#hwOSnQ=0TaV03p`|dYBgMQ5eC~SVgPC6pWZniYq~oZEv>Om4EQ2vifv$`;^+VU#)(wvy@fc8Wq1 z)MfI&)lIg0J!f;0-gLSn&{mh=RI_~XfWf%F?%XYv$ zxwI&Da^!0Y=Jig;PL|K%vn9MImF!|IXOUhmdBEK? zvJJd=&)(!jHeEMfUHGi9OQV1%^R6qdibCs)-RwD=MsXBGgYjjIdkjZ_Mw&Q&I4%An zVkZkRS&A9Oqz@e`ECp^t=5pL55`vARtO&5>jG}PB)Y*iV7h%|P(Pk&eGaYsNfWuNg ziUaf?eY9lfkCONinTrGH>>I1gEbh@kHj;K?ETwP9L)5qfvUQ~_CiTfKhMPO&UItUp z4mVlvJ3z__nG>WKSQh9ZuVs(%_MpwS1+dSX?I$KY=SWuALGMR?NWAN(#Burg>?36q zvy#q2HLNc}&OM?=T{&k1th=A#g_s5BheQ;;o^B@ys=athdI$-89akICToHeksk6AfCc2i+{f>)ti zVJCc#yY{g@WxXBGGvQHnmJ;BFX)Vl12B2)*cVAA?-~3d^9Ja4f%UGE2Tzgg&ZYo%po4X8VOfMg zOysVs_cc)3C>yab8})dyAM?b;69oebph_!QI@PGQY!glwL}~qbCHyVyZ0?Ai^YLBi zSfht~Js)kb8#!a#>mE5CXIb%qHdFpFG`sP9wBKjFC>>54Q9P?Qwb4$(87zC#@lqBu zR@jkzgeiwx{WDS1^)|{`RndwG)tY2yzmu*c^4OSdtW@b!flhaKDk{Fi-k22`9u|_B zuEaz&k`=!zV@Lj)c+DFu@q>widA-L`_;pHWKmCO3>j9*}9Vre(Zpd9}-doZ1so0&M z38^;DEbVFKhoNv=TgI($!ZAzeG$Zl7BqcTll`@ZG0B&h6T?OmP0@xAPIt?2@%j-q` zPBJ)O=4DByg5M*c<>|YU$x6Sg!LEtkih4KL)nKQfcysw0cLy7seD2vV zX`JA`8_3xt8ma3m@X{?2JMLFbXoT;cmJz{P<7b7PkPg>YM$f$7zhyl< zwViU2J8ZA6nX@(lLPv5t>h!#8KCiRgXvYV1Aez&z_B^8g}yGsJ&0u3Vd@r_|Ui^~tL?89iX)#2Ib zYA8*EKz=6mJxZ^alkD~w3%W!5abmH#4Rl9#08*#r9mk*(a(xTqS_qE0umd6zFw;5~ zF0|;&Lz&CIv&KSARl3Jq2cs;qZB+iYQ48EYqIrILzWqR*UQz;tu+80x=N8z>3l=Ae zs(L%dzP64-rne|66+LhtA1ckIqv+Lug`!u&PThCsR5ZSPaj~K^*zQaePlONhBy?@B z5h&zr%`TYdq`(M^I#pP_^Y=#&(7^{xc6-|fcGB%;rUR(D2NaiLk5A~QOu}U-=PYUAiqWxNlQ5%uR*f&3ox*wcopYPI;<&Fsu%B! zjv=2(ad&XljvAQRya3ya)&WeF%w0up6~U?`ifM#51pJctDSr#-$mmARbm#wW=R;=F-Gz**y=M3ULVy0r?@m5% zq&qwN=cMhEma(g*!c9$c>&BLL_k3@AJz=9XH+I7AEGXInWX57=yQJyqDpMvM=-Ez+ zQUB>oGTNnh{Yks0#MK6GLSRB`on$ayOyU_(Vtwm_Bbx`x(gnn}2RW57qY(xWC#WZ!Be+rc7 zQsX-TtmU0?W2~Zkb}F;sLfsjcZg4N7yABBed;Y9j^Lz%B@$7(4w4FoQ)*M5XP(@z^ zO1$18%F^jr-m@#2?Tf$?&-8Tbp3i_%Me(~DCR_}opq^>ovma$PSc9v4YYZj;_FcE` z`3xvUMmyNEpuCjXz7!}e*WmcWpHGD661-e?O+~o91g}78Bf_Ui@Cv$XknDM71xgzc zUMs=Z4?~sgC3po&8xdYF!Pgbbg{jg;gf~g>^#s#if>)rl5#db|d~Kn$l;9$Dk~E9w zSb`T4+$_7M@_UABLNkOdZ2}-Lac)bK)KMYl}m*6ERt(M>o5`0~; zT##2J+|c_K7|~=^(^SXI8YnMHDq&5*a^(ujiXyq5Sk4ROr7<^aid8FDP?l!&dIC8Q zl!4#p?${0Z&rx(^8(XI)6l{%7PTm=Kt@PxD06h*>aP)L>`gnbe8kFUNu$*KMCCCl( zI~TSWEK0Y3bH6sP0wZ}wiHCc&fFjZ&?uw%?w!Han$EjRr7ebyr7CK?H$40)Jz&~OH zncx@q{A{$x0w)X6ew*#Y=V-`JqZeZ8u%k$U8msr{XS37ji_jN;NNg|bp8$ra>Pe?%5ewd zfrPYrD))n7*g}jh@>%!I?Wr=R)eT)~uWq!Txe_nd;Hn_O3MC(|Kc6w#ThVKv?2Dh0 zD2tDV7+j_%K#!!#z8`Qg7AYUG0cK||<<|!*)+Pa9o#4(HSEsENlmL*MPB~^Kp}fJ^ zC`QN+>CZwzKd3zOp|+&&tordEdqYIQmLD0nz6~B-IL4O0PAd%@wtzO-Y{w zrJCS1#dYQMo}C`&S`o(LUeiRDp=Xn8$2zA#C5Y{Xj}|+gVb=Yc>GJGoy=0KR*lC1P z@H0QmKFXULN>f};@XHf=&mx%tgK`!6(Hf;W7LH9&CRx}K??g>R!HQm4N0E8qhwth` zrzK2qLZg;x+CnKg>Z+$@LHQ;MFQTZjoUB0kwP=7+6@H{HKU7)*uZ$D_7WAFLq~x)H zvMBs0-F?VpzvI|Um5}Rg7>tpye#~{qF0-S`+QN^I!xGXGsFJKW=bE}sgzE(Aq?rYj zO@%)L6rY-wK$Wf&J3T+}Q=f~rmqdm1xo2nUsz@^r+SPiTVa|9B=5?MZs!-?bY`e1L znM<#9-Drd+a$F*s*GyPYr7DaTGc5h+{a<3j1N>Qt+uG`4xUPCWd2R>I+<5F(U9Hh^ zDc-|Xbim0zmM|NOqhPF>Vv++e3x@Dn!!;Enfn1Mzgvm(53&r{_o2 zjMFQxLJ1XE!dQfG)KPbzE{HPOLpjGS9!2HTZu2m&Azv{9tWEajj5#j#C@PI`+y6I=Nv9zbk8Ypl0)LTQQLMiV}h8l_}qINhw z+4Mjvr3D z@C%`og_tbGjAGJ967`}2L?LrIZW0N>#!;5&i%gj_ioyX?hg@1-gkkSQo1N6huzoPS zrldK1h7vy_<2l~%F!K%^Rr$Fpq;7>!ceo&k+yR-rf~K_QWG}&mJLFyl^VJSUS@}Bv z&Iy?l&|Qf@EqN__jJF4EwvE?y+-yHlp*&ZzhZ4XT`62PWpAyIA=d+JF;@*xtkk`2C z2|4$OT6OiD4Y2lp76dEi#%d2`7JJaqOW<@VAB?RoU2o7UO+%$n^{v4r?6^%91%V$9 z-Vsz?{d^XbNnNRl%4b5Us$sJB%1IL=koF2Bdu50`c4Z88+DEidiSC*76m)R13bi*O zJ%0nWLGBb61Kt{STORn?c}k%K?IpMYrG0?f`JtSFBJkHIg`bh^p%lKyUHdq{vht4S znP{mx%-9af1iWgv)g*f;aV>q6Lr_zsuJQ_$SjT$e$mUYfyh zjzT$rn#YcPk!12RgdZ|*fW9zT?`k?-6={wmPyi)pew|xbyCE(UMd~Vl4RkolMl7UI z^ZPMRTs)dGpjgVZ_NH^VYHK{<6a`(Y@hj#8GslJmLg{>b7dqBp;x^ew8z@K4nC`Z8 zJkGM>18t@pnTQo&3;@5+dQm!@Im&p5a(46a;&fQFr{kq8w(Ox)7nXbIa4{1^U3sGf zR~4%YMS?s%=11?nNLyf-n!cE6tfJpOymU3HnLZMxds>!TdxXdK1V9 zFHSgS37x+so|&Y?-JnwDaSZD(t;MTlFIp5z;&`WF1BQ9!s8>qn=*zq;$<+NDh;MnW zuXwW8D{G)^qQ;`~4V1H?RLH!!e2tr~4Sreqb))%KH%@Um=M&t)1PPu*BNmZB!fU2P z?6|Ay1-z&P&!GgZrV-71THFL*2%Pp%3hA3~W%JD||6A6>7zHU8xo`jK+jtui0Cptz z^iI#a=JQ4+S@`QXYYD(vopFw8UQ@LTDql<>$&%q;U?hDpE zGb(Ma?`fw6Cb-e-84R-uCGg61#gQ!nC090#E2&`D#1AtnS25Y#z$fo2dBr^-N?y~+ z?dtDB7^%=JyXEIrRC!6*T%d`kJ}x?}!EsT?>@=LZ2f5MU+fkH021HvnwE=cl1F^7l=n*>osH)|(Bah7vM+ zenRJc#z>(Cs%TkPF@k`>T{4L-l<6zC8O8<8|Pd1Y(FJyrE zwS^M^`}0qJck=mksbL24uktt^&nnHGRD zl@vrPoePc+fECudT^0++g8Y<}NpwMdm4v=^RBA?-#wn>)N%n!nte`}D?xpJ}H1GAi zEr9Fsp>yj4Y-IVB8;|q>CgU+G6<$M$@h|C~8!vO8x)&=R%;R|Wqg#2tIF!f0A5+3| z8kEwi9z*5QB{-~u(34et4q)a_%}{QA_0?DG2gkk1V&kyk)|RqSeClV9xo{IdUZ>5S zsR_u6gy|QRa6+~|Ik9C_vQp{G>v0P^jqJNe9LLu7TuXFXtw7!!L<0~60rRp^tVJGb zZ9PYDww>@n7C1>TUYY%++SH_4taUP}#Z98{*Ql2V+UU+v4MqM&vQU*2!Y)6m_h5ba zED2!yznZ^OK9zIao=q*~yO#?Ne9lVnMM?dt%aQe!6s+L<{-DCCefzpOr z6tyIr*nzEvOVv!FmQr;O+nYyB0NdN}M;Ya{`?(wQSgDR$YVMVuWwtsnS>K5{&A)aZeVLB5Ry3t6}o?_S56sA5p0)kJy(-P4^A=ysvd?u>9BGC)}*- zYAx3@qSSspd8hCirHz(5i&_Lf^TX_;49{7TMN$%7p3r+1$qabbIIrt-b?~85lV9mt zr@5#l$-<6!Cu#ymRPHLT<12aL&(O;UNE=CHMJ=@oj4icD?PeX)hFYws}^ESNUDWfrx#PVDsj zz)yWH+FlYB%I-ZoQ`b|OdGN4nk2A~}uZaN469ok5oSkidNe10H?JcewjpFkpLL0vs z>)L|1sKTJXQ0dRw>m?*S=9h)&p{_2<>3;4OJwmFdmeI%w=Q>v;sYTXPA{tc}av4|@ zJyb6~Ln7X?-}A%$o+F0ad)qA+{_p&!^UmevFK;i7U;T6Y5C2>IeA)WL&VMs_Ry?}? z@$~HEpWD0t{^vG}2iJJ|3hyClwcLO^A^hw2TNHgjz~30+vUTe_tr#5a~fN3B14tv~+U`s3@bciZs*^ZaugnP?rg%n|dsz^>{IrGh}OZQ9s!6}8L});h0d zf?CWVgzK0#)S{>bZ#lTM3*VAgBX2thim*6Fd?qEJ^rm#>()txm8*0&B+)==jd3noK zQD`=`h}9>Tjekqw#Z5tp#5u-Lw^H^iYa*6cfw#+no<%-%5ySJXsP|%I9;%z|=m_H! zEK0m&A>`R(p%X@XY~(|e_Lvw!CiulYKO5}<14{;w<)ixdQ2w#oW>*+=3jZ5~Q36%s zEuIZM!))~VX&DlZZRMc{b>Ren)tr~nErakefDIdI1J3%SsnpW7uV#W;C}yCb8wUeC zX(l<2=lA$A_WzlC!R}w zLPR=lJFI(N7sacKp$L^4v*M9*H`G$47Q1liR1G^%qFW!9MP@+DCX(5q@PQP$t?MN2 z&`)9gl_mU@NI`Nr0Y(FPnn5j#v^lF-p%$%LSR7g;y@e>mr)cHdH=M`YbV1u~&K#4t*P4Vj61igF1j6FhL>0Qs0OD`XJ*m9Gjx*lGsFhm zP)mhcEF-m3H4V|)aXi;U@>J7?S}N3H8H}E)0ct75qhI^f6qJ6g(^TGK7jK`cX^gqQ zuH2<5?*3Y*sk~*LYfzC|)_rftOdESkg<34{J)&yZIo*4dU&~Wm)w3=a9#s=Wfv>q^Xj~?rJBhhFw}*?X*sWv(a)Z)N)(cNUqe?9-`rHk$cvW8_*0i79^Kj zSm4U?o z7P?#HTzhb4G5Kc#(Jbk95&Xs%cfp8EHP?(R=Ms+A7kVp2tG3^g<7pq{cSOMdgG_J! zYb)~Y&UkV@ESEN(R;oGf)Bq|n_Nd>V9YNcyTqvJ?C@39L^ulJXVWqS0-SVS)P!~D~ z@fMW*1ov7!d5Xjkw)!vxaNE&X{1&kD#2-1ZK*+P~fTF*X3A`!GTnebm*CF%Zi7o|d zgmeR_PBNG`Zr@Z;vA%Ugl+6PwDS$_kye!cLo=RGyB1&4AV#W9`@_+#k)h6zGv4a}J zMgypE!f*X36IuxqZGCOHe$t(+{9F}cI&UGfc97rtlEdZ?1$Ps8e1~_226(o6tRi=M zPn9a`(kFr11ZGir0qPfkWu86-RCSLf-QZY@uK}u&cl`nW6i~J6)Fr{tt*e|?X1E5| zSSdlI!>h(sN*3#Yv^}VsIDyuw#^Z5bP&eW9s;S0MJuj%6sB_g+6HRO(P&dKZjI=CJ zH!;kNv;>~I3H4P?_ExG*L@evH1WMfmj;f|5Q0gYWQZ>~?S(;x@-Gm`3rqr55m{vb=ZiV-zA z0xOs1-1t+V9=-0?);3Z35h3>4iN7dd?TyqXsExM%YUP&`_Wnf>-9ZzqBWMpn@KC@S z;+E4Hr>AK5pBvQ2^mSZaf}L~v&+Be1c`AS>p;V_BM$)BNE>{6o?rBeeU++?qT5?-p zhhq>Uiaz2%cI67c+h%W~WJDvC33zwsr$g2)4SM@w(6UVSpdz*8{k5Z20M+wMVkC+y zjMh0(mc3L!HQ0Hw-X}K69@I4ow>716dBm+5d1eLOqDC4R%>va1&L&VlDJsP?J)rwMTVcP%rWJ_$;;})B;ptlv;a8=Kk-fq`b%FxhXX3?q?t0v{GObA)C^mCECB5REC` zD%QaqS;+%*=BcDPJgFCnrA>OO5hFqC_$#wGS}561f0(Lgb4Ii$P_^QZQ+?f|*{d`c zsGT{9bpq5cB(O&IP7A>*S8mLzqi%5?1aq~5NKL&7tpQm&?bJ^A z06LsZ4+2SFI4wgeD{RFE1ELz4u5sy|{45DwFHU|sy}Y{k$Mu`e>yvKj<#c4@Ncb2b zrdwZi{sntvb&U_$;YUk^?D^Zv)2p`%+x=Yg`|H7l7`x|0YVwY#mnJsAnN7HwC9_ymKNePX?`l4m>rkM+36+?IJwY z4TLHE$~O>Z%M#p>x)5mk=5;drwi^`&Eqcs;F^O4 z#!G2wd`q_voqvLH@o8BS3;W*O#=NIxxhq;DCxMU~jz7o{qBBfq_w0ynjnc6Nkk(ZN zMG(@HkFf0UrSJHIs$9hkH+!OXPTTT+tNMNzHb2US6c+VadAqiHm*WHOz!s-U>Ut8w zylLtOmc0UQ6RMyQ$ayN=faOUh_#=7F6;_10o=o2uzh@m>Gr0G=yee=`H$#=N^_jsIemnICX4gZ$)@Q73jsN5d3 z4-ar@jb6FQ9?L@K*6{!6>a+-sQ7bnpOr(-4<{}@L}x;EPW)Fo7r%S3X%{Xz8=L&-WX zz&z1luE7rB)D6KRR~?de`+@2!mPoGfj$q9OpbnGR($lY7hotTPpZbb{XEeB-&0??ROi>$aVt-zsT+DlE)z*N3giVU_U@K+ zd^Js?fbl#mpswUoTK|;N)Xk70R|!d4A1U>HW>NK(W8U{eZk}6H>s?jrCwAirqy>;% z=$!hAKI`izQAqCUw@%_CebPb|5XOJ%x#?;so(s+HO|Z|JR1=1#E|(ROUv`AgMfl_< ztY=-S3FA?h%O1%uHkRkok^EuU7kn1tjUpc32YYCxB_mdaD z)#YkN5*b63rPHCjXIC=Y8bkTXG}(9jAWQhwFyUet1@#Q`p8Y7Zu~g?L?sdX)0oh$a(nfeU$?i2na$ebO z#;>z<0nBB0DfZnuN$!QLY9%DCmfcfjcM04TP76q!5|UQS?t0n1E)c5psYqHayBlQp zdO`WAe9~&!-6*@)2}?wBwi?uoq}8&!QFgBzlHJ+4CVka55tb`yVXwbUSmjDu7|Bg6 zUBeg4>g1y&K^n6BP z`pPAqLU&nS1}W?f&SoU4AxQK9wM|v^3fLnBKKVKd@vx@l-l7WEa9JwLE|gr6OGh_x zVwB<~ zDxOB@!D?EZPiiFT)?85crd5KLUYU!*vgU5+yThFF#4ySN4;?9qD7p7}FL+xxeL9k| z|H}T@P*Y)_bo!|DNGwUfP-#?qrNXC=%6EbiVY1?W+CZOCx%PKeY<_YaDwI^*g5 zHhTRkSp=DSW0P?{rS$0%M{s#4c3LI(X7~M2q)zB!U?y2|J?=62bR-i_KwyNm#(M5i zY+x>Vx31KD;y4&nm?CEs<3Z3K2Vuku5x!z+ffJw=B<16asd(P27)17X`gq5~s-1oK zL?nk%7$q|yy7mJwj$$4U=K|h&FX3yAP`l zG5hFtTy?V+mBf4Ca&G(dRHX%EchQ}TEU@*TJj!F9l}cAvj!$|OGd(RplNBTvUE@?B z{SwzW6(pCbPgc-fcYU&gq)n*C`zl5rCsW)yUD{?VySKFP#yuh zl=7GCA5q3aF7J>RSr62+G)raFtT+IEgRT(hP*;tthMOzZ7-3y`nY_aiPCG49#EyZr zv7fQO?hs@7gwTz-#?ItSxUZ3h#2eUg^qP2W>BMZ+O!7 zqx#0a>d7jpQKH-4+CtUA=>dnJ9XBi^Fz6HU>tbJ|tC8%|P8~*+)q_D4P#zzw7odK7 zb@GzEB|vb?O#&gKg9g!}pi=F-WWD{GHVJY606yi( z%_^}6!w2;1prMf`B$&_;t{VdHB|+mzwAqzewEeOYxSF#DV zbN*iJN=6JkmPU~JD#yAdD}v7MVcT&0CcDv5QVIBAWN>9Zu+8$nW&bll{d|D@bN{Lr%M_w|)dd!5DHq zIqw{yx{u0u5@iEv7oy3mXr|3hLoJj9&@p-voxeYl4H-JOh#WoCP26LO(x~Y`2D)ad z^$s9`N6roC{QW+x_S@_jTYz~R;iqo_o4J+n`a)#94XQ+6t|dOu19tfAyMur24AV3g zz&!>qbF*F>da^LPrQAhxzTL5k#tLllgmGwdIo*Jq55 z6%!1CxT~)`Eaawjt_KzOVrm%0eLu3Zd*%&uUfVBoG{6yb!} z+WOIlc8{c~0Z42l=Qr^53~!~rLkK($x+9^k+$wmIU_d&w9i33r&u+JJCts8-mJ}D8!Q0#8$SHxr$1*s$!g|cD$k$&`NcEY zXW7da?Pqx}N6xR%gI+d@OZS8JBWxTZpLb5LnqF>gwOH2-Yb1qjh!J7>pz+c@FxU@@ z{6m==qNFIVm%Pt1Z^g}LqV|xsan;%lrPnAMG2#Et$$5krJaB)Q-YG^o6`7Bbz;~p2 zP+L7J1S9UKdlC$1luS72%hLnO?ix-rj_1URjERllghZAA7`O6a#@G))A{1m}+>T-% zilN_6+f=v`XriW*ih4YWByJl^?Flo|MpGQ6Qhx}>tOo$!khUNl_x#`0MG%R+yG2GE z7gA*aaH!RVz5(L2FLfqgV)QdE$QIbVrpNnxA5Mj{Qw-fZG=Mr^Ze&Y>Kakx|v z_o(+P$lbi=5#DZ}a2Lg{;uPpt7*R=9tMT@A9kikWT>Z6(T%OGqak;1$}(DiF_+$`NfGQ&G6A{-USUA}yb)@1HtSYS zL;@yS1Ag!k+YQ|BBg#9R`Bdyf)CWF?PFg7K6FDhF!FlfP?%IXuXh+F_81w$Ycl(*< zt?gk7l7!?UJ>vP7icTCZqZ8k%PP|xFCtl3z#EViV4rC`tF*T$|MfJ{~qne9cSF(|7 zZ2mRcOfhB;>j0JQ$$BInTEznOh1MK`-=etF^U)8@B@Yt3<9SdPFM|Ri?sEspMv(pj z+OQ;3RM^7+BcA4zns6B%2()IN3a#zv38-YWp&X$kxK4Q9J8ks5Ht2ccdWODp3q69b zTsw(71m;XMZ5APw78hT{EAFD)3-ek|o&fmGHFeYc_;tJ58ti<))yC&!fp!-9zh(Np zxHc7KheWS#b{ns$2-Vuos9{f932@+DMn)F+8$%CHMDy(DXBu5(G~{>E{)j$Mv51_MKYZ zmY>$=J$6#32h@pAOYlM+)n{>00}TiN2#PWESB8!*4FRp=dK_>E^%)fnARp+Ra+|tF z!_75JkGM_S5yfwmawrv$apLMNMuvLJ6{3BAfM5U@h{4q`@HOlfqK96P*x$c@Q zCds|rm)Fk6BgrRFtW~C~`m4xrhMwA$*-Oovn&rxk|HeVb!qY}Vdq7Rg0^DuN>m#)1 zmEWTyFGp2Wc?Bu7jIIoZJ?DatqFW9N2;>h_*)?>DUH|>>f3Hk5)Bs?bNeCsh9dU^= z<%jVcvzMe$Z!F15lvR1v@}`h-^Gu|;xavrk-YZSB`Y-HNcu8$FlPssPKI&!Zpqat3 zeB&VyrQAw$uSydRwx@*cY47f|5{E|d*Y#BNR>O~ay^imQ*(9RYvKVG z)KYsDVspJsSMps;P5mqOv$Rkddc4-iwFZAxdV4J&U&~d-^}#dH(`h52@b~oa#6iM~ zt*rC+M?2a;4^_}X*UAQm2Rakl$!R5?ucMWkFU?00!$m;9Sjm{{7x*qd-YMH^l${dw zcd}HIA}tkrb)^frUI>%3Uhlpvc2DHZFMGvRY(2-l$zmJ{X){o&o_01$vM{90fN-pQ zL2#QxEt$8r4%+N(54no8Dwtayp@c+jq4YIw!X7o?QtF@)HTq@AMlH|EvGcc=S3g~x zK-uf_&gJDVZ!eBt{bA=O>w&Bbm~>O2>pxqq+{++eXcic;R>Xc4Q5ZYv@YVMJwy_w52sn7^UR816gFKe0JSx0WWc3 zQoJH>j_X~uqk`y_q1W9~dss%rq7D5M%mmJWxMl<@ADyULj@|T1@_xTllbVP}Pl#gD z(!mx35XBbCeLH%0adGnI>iYf3#pUVSH^w0HkKC>AS?d`z3#nqrS}yyVyTge6;fK?! zH=Wm~*Qc*LKb`36zTTC4E1LZn9has}^?jI#2| zpVS$=`a=bg9BtgJD7NEhPar2d{h8{Ov^i6Nm77VAKAu3*i(<{0FcUD|X{3jBHVjGL z&L8~{7sql7NAYyQWjoP0qB*T!3XD@6gbbpAkTinI($*iCZUeqxl}R3X!hV)d=0e(@ zo&(hi03}T4XAkXmd&{7%XMcYA?8VE2!}g26u+uj`zGdvytG@#8K$~%8=*WDe$;cxn zjg{g5^rjvOmkAjr{QyvM@@Z3$hUN~fy>hl{1Ge6{Q!Av$(e2q2n^>MGtQTtE% z-aYZPkgJiU9qHWE$KVr+^&y4HZ&ZxfXah#uooKS|kpM^C&mn`1wyooaaA~Jd&2ueP z_X-5*a$yPt zrS^gXWot#nl_xXnJU+SVoMNSD;3D0}^AkFZFedx43Qy(#uAJ>AXU@UFo;q5iwz;Y; zDcP#w^9O3qYOD4$sS|&A8?rYrSw}D+?BMwxgQh>*lUuj9hcK~XQgN_RL`P^jLti&B z)K+Dc67NSkzIW*Wfawd(?WTMS6Z`L#PQq>vx@<@L*I;j8_AM-nPm zooe-c9(cRN{Lq7;KgETBd;yDgbs3e#MdoXi7O+l4<6ejtKO1Ffbw&$SMGeRtr5nSw zrh{w3Kt6-&I^=bwS1{M4c^S1!n9x+B3kH#Ub1j{2P$MCagS5DttE5Qgdv*DqYiYx@ z+)An#T69&WLF~7X5h=p_wobDXvMu?#opyHMV@?ncogPQyHqJwI@`BaWD8d%w$MM2X zWc=v9WA5)1bkbIh0f1o5J96-JeaS#TPB@;Vdu@Zzi}C;zzH9Z$G(=qMEek!$%Fl?d zW0&hd(iKbl;Q7bRx^Y=v{wRJYU*aF(#PoQg4q;d1I}t;{^l%QI_NB()NdHn}^n{p|2GrUdM(WS!yR)rd&+4sT&rIvrUpB_C zX~wT;WBmF;j9-st@mi+wY9ZUw7M ztt9`_>>lOjOB2Jf(qR2k_-Vg6btbJrgi>AC=y3-cBJyICX5!&h!)Mz#M~r)SEq~ zta$9O{Y*-WZS`t<_rJDq%ty|ctRiv`2xJ4IYrRaLaN{-G*} zB$sy>c2=`(t>9Gb!+QgNZz@*UAZ^b zn>=(+rE`a_wGIr66czC0yTr%2gnp#`6b%ns9v=Aeds$k7N4E)`qKyn_{0!N4=aQXX zZnGacm#3F|TfdxM{rvXb75k-ganX5mb$W8i-d_Cw?7jV8<2aHqzCZI<=+eDU!yX9< zZ#~Q1-1E@D^l-~dj?go=w>KZ!BnG^l#92F`o1K~O-~LsVUM$IyEhi=oGh}C*#FnZ` zQb{V6>V+JfpB)}w9-p7lU*D1av%iwR9G@MolQ2T}1G?BJKsyZB2({p?l^&l@0=e)k z;7)^m+M@C83@qty!E=Ai-5$|;9IxrdgB1+%$`xw4=CoG+u#}J8O|4!W?M}PByS+`? z?RMwwP6z(NhyC5$-Q3x2lg>_i^X=y5=Fa9OX?J$Ew>p0y?Rv?ue`XL{`wwlPu~Sqo ziQ{R}d#zFQZ>>Z=-Un2>VqFt!)H++F_UYzy0WVUB^8wdul<{AYUia|7o19CV$0+_$ zc|O@A``!J6?{U84!ukJ%*Ufu$-cn3U;jIu$={gk(p_a~I+z~}=bKQ#vT`l^<8ETy6 zr9uQQ7}5q^I*US0<}}6}9Vp5b_IrP^=EXaRS)mTuhDcW*|D^DIZUOt64&Tk7nVIYo+k5=1xR@ z5!+is6lt&^2N<$J!)*oROMnOffl-6leH!<^WQa0br5s98)w`|EHhLnQRBW3(XXo1{ zUlpJeWqi@;PCyJP72@Oxg)cdLxx{I#oCTC)eU1_x^2fE7A3+QuU7YBt5?Dv{JAERl z`xN5UY7ImdV51ZhaGYjp3B1Yt=+%kBx4L?RftQxnUIW=zXs+U%wzO8;an{y&{9~@G ze3v1Ft@RWNTgbDeU63M65*g%1c{ItF64PCIq!XyT$a+DbC&XlWr8pzO_0HapvPcmd z=!qMY1G%cJQWG(#m&@Px$ zJU&cYz!@xLl<-Cde-pCG;;=@d72wWHYYNRYPfYL6xet9XU1@R}T_6)~Igq!Tou`aB7lTp!J5tpndnxX+QkZ`U#jG;%=Q90o3#@+xZt>~Vp z2_|R3P=Sbkl1%ML_Tf0k1Tgwg4b8zLiz)E}*chyaFed_T_7uqe!f{bR<4I0w26h0C z#JfL`_i1q3#%>DmGPo$$bO!G{D_V5oW>rfTR;R!3X` zhzNb60OwpP@hhaD=LK-Bu zbaWCHF}pfCs4j3tW0prpTSc`y3CDse?ALN}OR^;tbnkmaUTDv*MVK(KXG?5hx~9Qx zx}oqS^T?4(n!oX#YmaDw$bQNbA$3K0SR%}a2F(?}%?esTV14!}nq;0s`Qq?!H5!SF zcxWM-&nwx_)^b!1B8YaL2gaZCp#_WA^e))k+1(4^(xk;zdP1WYDS^P(VK1jVux`=s zQJQiTOz5J{gBazgfOk~VgO}ENLe(aMqbjwswZmv|cs;w#D%=qEfCqw6l8~?!11kXw zCsO?*ws?GDSCb@cLbp&^k3AAIH0%96a(r_ZPrJ-qnxQ!2l{;-yyBJb{^?4*1s_5liYUyHC}XwqubO>ypBDlRw=n+R9LaXwotn3@1LXAuPi4n$<1o?&jsYVcMrt5~(Ano}Jd zuHf#U1;rII!8JH9ghC@3vB0>xbf6NjhisN7bxw6|iyUWJHLWT?IC+V1r*an9H68{@ z-U?m;*3g!iEv%-*owH?yEvlr}mGfHH?ie5y70d^%zoc(g<{YVVhNa#fKD?5pTzK3p zVj`Qb4=t$dZLdph-E!STtefh47PsZ2t4BWOA~8epNrV%0HW5xzs4WE2m>mcwbA;Eb z=-bNIC+2Gh8qkl!K`@bP>qqF$lf(TkOVWi0fEFv$x{k(!tBDUiCNWtMJ1{|%D~L^z zXpF;SYU}eV*%Ae;o3^?Y>ot=0NJ{WIC|t>_i1X@UF;OH(u2?n2+FO^dSb463%kKrE z7RIk77{5k0DFwv`bNc1iz|eKfa1cDu$M;bZk5R#SA0!dX8fD|qpz}>qp+O0hsP?t? zeDz}%9q}V!0Z(PyKB6i{Ex$*y;(+QxSv<86ZtzlDM-?DAcq(4NqM+4<5-m34!?=G( zJ0zZ^pb?0sD26Ie@jy)7jHgQ*hPLZw&*$OD-lpjrr)z+2o^F}USek3}n{AI3%d zXgf@lx%E>qa2k!mCL6cr=U|9VXRqFWIB53%dUo*r#rfI!hhFpG{PgtrQcg5;2wLRV1f$VH=ZL;&s0L<1Be<)UWdMcL=W*qdc<;(pT58a%LeNZ()c*R#WRRx>JM;o zi@gU80Z4jHsFm4Q^P&VJKBoHkn!pa2!zSw>{+&VMWpZ^{8&0F~DLuoWaSZ>0R1CxW zaEJ%qJ5W4HH8+Z+mQFgMBiyAEHTps>#Z3jUHNt|xx)b(tRYr-vVIIKYbj=12f*<}9 zygB3-A=v^%nBjD#9ZgbpP?9th*jRzPPMbwpI1O^#g>VhR{8i0Q$0}o~mRXrON5DU0 zzSxh=I(jhP8~`@R%Jo))27h8!;JFT|K^X`XD1zA!&oGt*m6>x*n%(F?>54ZG%*=R= z!G)6?Epg1ZFm_=&U@-!2X0WA8|8S7)-pERW@f&I0W`j%dRfkI}dBBzDi8;VeK5O9B zE@rJFkdr`kT6in&0z!~-G81I5gR7pA6tdV=lsFdRoCndM%Gm@ zohB9gg4Nuga+CAIKId3&FwW{+cvy&5XU`kNkn9^?{ZAoE(=8l8rv|fYqhp84Q1gQ= z8$|YmpH^_b3}SYSf%pH4ovz1&&VlaVg`-d?O7W!2Qgr}C7_6SHAE)e8kg_?f#X-09 z94#%@JZHx#WhgOHX*(4u3CQJ1kACeZ4-?*u6{xpoyQpJJn^^1V`X|1sD>qw)_zyN= z#1G_s<;Uc+sH2ar_LPl|c!a&mf6%6}<6gmyBKE;7iP+lw;o^kN*f9?Z#+(t85gVYt zr9Z)48r>TIwg=7oX@k-K5wl$grO&#&3ufsX5V8_j*ic|_;rybCa1N~1MAAVfmuJ2t zGA{Kq7!XkXFiLW+hXeZT1~VVQ$mYuL5!+26)U;yhfsiwx8}%>_1}D*Vf^I#8Jq9*_ z=TwdnFIMsLwJDNqz^fGHn3Xb^w4sw*6j-pm7-7qBJ6pw!T2qV-qxzb_dEjnDMs)Fk zd;@TunkC&}H(7YPlLGgEtfG>M8S6aoVanO%*`a1n}DU=VLi#^Ccru!O>WrvjUR*Y1r+5185STI zo;Q_iz3h@VDIahKc|~{@@D7nA?UBy=NS9}HbIZQ90P$lvE331&^|?ESdL6&F0_6UHz&>glkP-{n1Xz|(d(V?0ZYM65nL8S*2`!;8L0hNL9z7b)-Kz(Abo8-NVTm? zZ;idd$xyZyzNj(M0xB9Dt`jP*4d8F9)spKcqmCD~BGc9%8{ufcrp=SI_=yu;QUt|QW=X=4?hyA8;YliTLqb%5>2T7jFxD{dE&BXp1a z(i9dFs1Qb(59PjO##xTO0#Q-E!E*Q{UuN_mS!MK);_0OG=gn4U_Zx`Rv9Y;LMuQ!> zdRoYFba5^Nigopr%)so^vEB|>s%vOB9H ze-pUP0i0LtO)I^l%XRh^{{9Oc=rKLbXiqVWe&Z0L6oHHaA*~x~e+WciX6@4r*~BrX zkY^*=)Va+2g@3f;`)DCnV)h<9E@ksSKJ-F}UzWc(Ql#$`?piQQnquL%W@#Hl?}fnT z1g?8;osA<(22NK;+j_w87jrfrH(kM$#w3w;TX0VO681&nXe(#02+IMFsVlU$4!o*G zogd!3wN^I?aou{vC352xggO;vn-EcA%1(K(Iv+Y0Mp?kIa7+a0eoMz)2u991Ugd$k zXn$2&K`8QiSCR7NBiA3!6~yeBthxS+M-JP^stZRBf@STfM5${j$djtA2JU|Q=v)b2 zy&^&e31#3l?#lEgl_$9Dw(dFCg&nimt z?vVN}MOnmAx%(C`GPK@A$y4i3ympPJg}C`|xU$Ae8vK&k*Dzagj+5*3H5@*`@izAo zSLdWUaL^?(c#aDRLX<|x8K>YbItIyXgxt*z$CHVbHWM-Ct~Nl{u?X|yc)(Ri!j zDN!~#x+X)n@k#-r5MXqvmTl6!DbQXLhe}L#ga>?{(d9542H?FDj|V9pnj?&9Gz>8O z57#EJ?rp|9Z_x3RKUL9PQV!QNBn zA;+!F%_^8xU{1Wl=qBtx^oQa1ar`N@sl{^(03Mo{(M4$7Byj~3I3HmCFHU>5*Q>}f znk?8v%n)(u&SYA&Sn-r)Rgd?K#0@IqgBLhNKANEyIFC)D$hnSvgBL^}cxJGp7(R)& z;K3UwA{Q1|nTyaBtlDZ|_}YwVnRR-69LMWq6!jDOCxsM$^jA2DW}|g7q(=o>16ooq$96%jee#lK>eL!!kxzNRsyfbsBH|c6qA=}~W`Y}s8Z0X(c?jD2DsAOpc&xDlOlQXJC0tzaDSO^2HoF(3CB9a(yAE;Yl=}eL3gKoxic*U+d!;%L)1uvH(axx4%K} z2n$IS5Hn$70IQ*sk^1!I(b9}8sNWnI7{mFYz_N4TIE%;VN}^$TlRB{Q($(-lTo^h7 zZ_)a{1WQ)7Vq2&%2wvMj-FMk^FK?B#UNzh(cytBfM`=G8GNG*$yQ^k3{QY1O$ZSo6 zB%Yv2B50A-H~fAH-cPFbE}i!v)=_~MmeSVrVG@=%j#q+R9=$82GT4s9wO;B|Ti$HS zc_mjrqD%zyA?I5KY{qEnam~&SN;TJrsX)|T7x74;+v&R9*grXG@O545Q&A4OmP%$9 z%PPKmiru=aDSo28=ErN?gl<^x@P}VHIE-W??R^+^fc+RKA;SQrJmytPzgVA;z7Q zw*{HBs1XuO5h9g*j?E0*Sq-HbZ_|bMhJ|+I=Vp7ak${)t=VU#&kQ@zONJcfuN3MVr zLChqAo<#>AE2;{>X>KqZf`Mar&rE7I0u1Vfa~W%C;8a zwp-&cbNc0W?lNw&kPW^%E~;@2LvJ4GaO_30z<$KC+~D_57;#=7w{Vk=!9OwSpk|c% z$r_!$pb*l9bUC?r8c*V3eEZOw!0Cpb zw$f<=Frg*v4Pmc{`VRH8up!D;kd&1QmU|YBHKYA0;T?c$I(Fq^)VHBBZlVoV`f@#&}wV+*fF&)#XnJOQNNO zl?wy9GM7o?Ie<<9hI4b6AleU*8xoQ&Ou=3pvdN0q>zKxjG$2dYr$(jdz8I_y5{xBH zKBS5ca72%w(PS7lC8q3vrP1bW0p#qA#>l7dlb|1VwX0@%aHU zX$dM$tvoAjyi$m3#y2{f6w&Q$?QAljME0iO1%3OF6GVh)r5>bGUKj-iAD~G*gyro6 zJ}raJ3lj#zxLH(uQ0MJSQ0V{;UN$h zne+cC^Ut?L(PrBJA=i%4Q(44{@E8QVqR&;W8jirkTx@>IO3p)GA!SC01O=0moHQDQ zeKhHXJP;W+{#?y9zz?s*c*%FL_GuS)0!7ZA7j_RGHaznoM}rBB{I0w=UUiko>GIZu z7m&G%`0orIywUNXqE3yVw?{ggTibG76NI}u55%2=tb>d$6wWZ!`j20F3L5AW9ZRkk z2XRw+faJRHdX_$1$6vDNnHlbzLHuT&ypeU^{3LK12xaTxw(~GblNn2R{%$t74a?nq zocm2O8#k3;i0XyY;M%_b4z61~aUZ)r+TeLVs9fP74#SW;l<5GDF75lw^?^b;r7a)L zMm(C@#w1MoaOHN3*4QY|p@j%n!X+Vk^NQWiu1-fB!Iux?Aj1-T@xcPgp)rC#m6K;# zbx4Gc3SnfZj~$F?d>v2k6gL&4U8a|Gj--#6tOA3v$WXQu;b+P^Tv30kpyiQJ2Uu+F zPKOF~o9@+56sWe;@mksLAQZZ|REUkoU*xo@p!wJ4goDgZ5bOnCm0YJ}wNMrfi{-M0 zY&P7fB@m?ZtM^Rn%hNch=hysgNf~0izW`U~WQ<92$8V-0VYqORVW)HG|9%#5Q2=M| zDM%SaxE=Bsf5V3sxaD9JR7@EK8s>EpLmXvfYZ;WXA14WgDd1^1v+%S+@6Mz4OWkP# z+1$BTREj%gIA&`fLW2OdNCx!Bx+uwfr23eeU1<^vDibufz;z;-;n9u zzhUZ;pKy~C2{qW8WX64cfMktv1s2r@QU$7MYm_8|%GNzmcV+noM$J-mAkXmnJN6zm zOk9(nf6vNLITJ#WCU6}&9?XT|h^4W|$;}H z9MVnt-?!TI)7;v8yZiP(N=OGSZ7w{zL~H0_1uTDku|?eD&o*8`AEV0H>>5dvt$Uo; zC89-rF=!B25)vF{4^TzU!)V`y!C(mMtf0}hs>-T+SWzWXc~wf1$L;?_XL1$OP@g(( zG!?J@EvWJq0ENwpgTcFC2qF6urmH=H5TV!n5$z&)hUxRNS%w)RPL!G->*XpSswqfl zA_-b&nl$?ipHhc%k={u9z`KP#o?=*isaK`kfH%)@hyWqK-o6o_n_4Hiy2p#PLoNtk zvTcDMVYsO@5C6vfB4|(Ht#EQLR&LQW4?Q}H>#Axa)2HL{ttkl6+lHwEkyf4+j*--4 zBsHUnv5V*1B6oobOwvQ>7jUjX4Hj&5Ay+|wRzP@Ed}b#*HvUnn@r6LQP}fN9Pk$KC z22CETB7tmi;b%(4#w(FEyUD@8g$^`D&LGPwL8)@JR<4{)(rLHYe-HxC#ba7sx8$U7 z8|WlpKRiNV+1MLJ6bHZ6E`#QQI%V>wys_@F-})7{B3bZy>lhd1IwTF)KpLVNU9w58IgpnF^;ld!aRz{6X+hGBuNb!B%xtd^aIs{Uwsh~j|rm`!3 zZ%7bekUN1;rh3N+K|=^L?(PsJ^^H914IY*uFt&Cn$&_%tq{w0EXVVBmN@CCjxx{Xz zfHP23R>X9rv0%i&<|*f?sFr%aBs9C}NX>3k{3A!SoIlhm(LS)_BNou}EMkFcQeZ(xFX@ zGvbGtu~=hdiPIWb-h=3Lb}dP!-`NrtvjAWU^>F6oR@DM1&k;pe_d$5C2F76yb0E`q zf56Eg?$k}Oo=s^+fM*}WbwBL`CfUN!_1FV$m*5C=f()#26fEz9kgg&S!C3{$`0L-* zNB^PJ;VR!ovtg^&pie*FMf7OS?`u*rcz`kK6Wr!u?NT(j6y9visUWM07w#HtNgNfp z^{mt5R}LNCB~S?^TRzdk2@jvbw@i5TO6-tuldRrOHo-rC|Hs}s&oGz3fMt;0|K(^M zUX%Yr&L`oR9@_e!*0fPmi18dcCd@#}cQ$hSOxp_&%yG+4exf+O%_|jM%1SCaLl!v@p>+X3GhQoe7)*R7 z&I&(x7=Bz1w0jWzlNB!9fAhV-HkXSKp0S|Yb7WNaurQG=n=%gV1KwsL@aFdaDWea` zlhhU8-1*eaNeo$s@ZCa^#2seY1mpeJ%bH{rA2Q#fImv@9wAT-S!XHd}uEV?FKBCm% zKC2`oW{EDut|w>&j}-6_4KlX zGdkN-Q-nra%T`KbG&-rOD^Q~cS&mSR8fnXn*Qg!*FpJUiYt!L+s-Dv9% zUo>@va4bNvKpe*cs0#&iEP%UkM8^Wyzxl9^hD2N~9qil!c14tQU_OEK)spKap^e#-{TeVn*6YmVqPPub*l3NJoQSLP$QT z5R%#`Ef`0#3|0-nB+F3L5K*!e#r$CcgI=|{jL$Ds5hV&OoPzSHm}8s(FTMycNke4UV#8V!!P zl-Y^q=|`r_wWpr&l(ydR4N#dQ>1#x()DO)SLsb^G-Vv-)XKp{Y6`j!}OBaN6G-X8Z zz?Fqfa)+)o@y!vxvJ7)k1j`(HXJAIHD}TR%Ec4@46wT7;w3m36mfQ9x9MMv@{V5M? zS=_K?2evGu-iU5#5dXB|Tb8vYALP>7oTnS+Qs3RmVqF>n)JwEWOSH=z%lL%6tgB;d z3wo)w!ZY&a<8a5^u`i7#yad3UKLDmKeasaFvshXzjf1IQ(>~i^m^x2Zjfkm>b*i5Yt7mb~1@5Yztnfd6M`sQ6v2+bVK$BLtwN3^71nuRUo5j9JQd;GAP`L%M9 z*qVh4j{MAw9@5a z?tuJXB`~KIQDrlS%JDhv?fZ=e=`3yR?>$ndwS#`Q;W|s%Fv z7`oF+!Atzk#p8GSbnL#YW^495!8)r``7|PS&PVsx3)}gQZU+$JEwk8q z^@>Gi5GA4!_jtzX4z6=l&K<_Lw1q=5n2jc+PlblR$Je44MS#tg1jP-Uz{E(o!W{G2 z`#^4I6g?HI=z9TU#yk&iDOnjD`X60VDH-WFn51{~E8rQRIT!HUXS(@qT+fPRb7l1Y>&gif6~$XJ^~@=;RNDT6d<=(KqU58BIuBN&{)tR zI)f{a;jYAmao1}WbEAkH2zx`NfI~R0Qc*Uuu65F4ufYXN>$`G>m`;bh>^?-}`3`K0 z@3=?0gY)b)7$e%>m6HM5INE|zp47@ANbSiUIUn;dTgCXwBa3u%C2*%bpaAGtZ?Ltz zGLpGNj#NW4ci54NXsU)DnX9w%;Yau&AV63A5t}NcJO)WV3Q1ucl6)i*Y$0n}q07Io zin3x@l2>$?QRi>ILH%}+RW02nYKdVo4~jbCn9PHB&R8aMqnan4$=t~1jcGCu!nxy` z%+2pBQWoUIzKlBpo!CR2ID(xx!=1PSo_K^j@e6ul z4|`$@eBugyQV{&4a4cD
eg7A0O&?w~!x$A-BTNVVa{H3aAj(mVO2*_U6t@4NfG z-jC-Ohu=0{|D65WYrbCnchY#BJ-Pbs_~fXu_G^PoZm-~+2KKAXW`7t4WBNC`X`+%W zrQh*to1TN46OeH6mN$Wwtl9j>ES`ohGR zaN4~(JihpKBST6lL!YWQ|N6(jHvc$k{&CR!Ut~Ntbd3qA9Sl$Hm%1 z1CpxnG{GtFJhYt#U|&yP&JkR;GQ3Oc5mz#Y-xMvR(2icSkLtPgk>hgWtGDT!yg2(5k*O^c6c&mgL)i)b0ww5UKyWK-;%WyUs@eB_RA z>N8-q#yAx?l>nys@S&Vhm7u2lB0>#oD(@yRL}^ETVum*5FS6jKdfiQ1T^Zoi>ZMb( zVAkrXqZk=j0=BVxv{I6IIa5c$j`9Kd*tIFYKf$-G##uzdYbp{Si1UorJ+XhlPH5 ztOdLrLSWIdQ7nK}MZ17d8mH4(rF9-=0)V}MqpF(+)NdeAw}}A1X!S7>Fo(i=+wU7U7TROKv69^RN(7(3ctl#T2XMy}VGw9H;O*o9?tExbdcH*2aTNokC?;ORxSvH&SW_u` zT&G4j)k)&tO6v_b}Plb{N883y-fCVb~ zPj(5MwIlc16vieNu$-dYHFFVIXjpu6!)_*1uBxFAnDi@F5e2$hfoEhXf?pvgAMLEs z1A2e)RIqB`l*KdlUw zh^66H($wbrLV3AFlEh)c$h;W{B%C>g+&D2iEuDwN92sn@Hu*$~1rzgur6c7#y+s+r z9>~0xGK$P{axGN3)GS{4>dwqi0Y|;*87d%NI7dTOT#F@YsETdzYz-AK){wHHO8e@* z+nkGvM^4^Qfs%zYI8>UhFKHaUej0~t%jw1gs}arecN>ukqCiL}GcFr|`f|V%g<(Xw zzw^7P%KOdZq_y(GQ$ubmKRoj%xT?}$YEyvGKFILg?PMP!_Y|aq zXNnU(_t;f*u45R!e0)e1kqB!jq8h&c?&|R9yZsL*msiJUz03WxgQKgn{nMiraeafg zg6OFdjp0V1FIQ0wtmnCn7e6@VD+vUssG-pYH3z32r#iI)KP&WdCxkf&BC8-If@ymU zYV>g$q02OAY1eGTno@l+!F6bA%6ici*|d-(uBBppOz|^nxNddCUNS*|QkHp$|NZk+ zIlZiE4p!2=szuJ=b{85u1D6N%qxh)O`mtqcfg@~ez8TdbjEZj5bsl;kL|16ftf@~X zQ)<3Do`+#U0o~HJprp*RNSum2Rkk*~q)#nPpXxqxEI*&MiwL+UTxL>h7ahyXY;93F zf1Rvwc|z3GaIhr|!pSgxz|;;;Iv<>+OO;z)8IBKkIX!WgagR}yh0H3*bGL0JB*;X@ z{A0DfgZ=_<-ACc)4c<|#FBt9V1_LR3!=1d$O6I;jd&gcLkoRSo+uyPO=hv@S4eA3Wz86`n47u~mm1lG92Jkiwz+ z-Y-uJUQf}T^_GI7d*R>s0=Gvlj@}>lE-(Iy2c@5kA>e?T6J^*C5)qI*(N1D|a)=GY z-Sgh@+BaD$-BU#7V?7xb=n%WmluoXf|bjzrk&a5yli5Yt5_W zgXk8FTzv8J;ylt8J+qVaF4)}J-3x*i{gCc3gtHi(XTbB3GDFr`;JoQNGwl+)8N#9D zdl5U4i4wdChAK7{PZ0`sMog>WU^iAWMF`7Oz*UTl6CtTV|5KRI)l6JMaqie7LUt8A zh6QN1K&0R1AiR!(ag%;aws-Lj8;0zDOGzMcd~+60QJl@}yDi68NX#P+k1`1bZ@4t} z6&)e04yCoBFhAscAY;eMnu{qVp*=j4254;qJXo<#lvdiB0WSTm44f84dGv6>tNtZ? zUq^SQ5BP_R

IZ=V|hVdN7+;$72J?U6P2eGC323JSev0+}rMR7WuIpIAc+A7eYVFNDnBW$H z1@Bd}`k+P}uH;3-qSOR2h_RO-8Kg+O`Im1}vfn+PS7&CnzfxG({Ght@PS9d(eu^nM zu(nY%qba!#`hr>cjK6?Nz!>o5otW!9zY-JNy1g#+2}mU{?^giIp2D*Ewj&;vR)s4_ zFW&7RWQkT}o^T%GA7(PVQ}W6MlZam*XW|Gv7hJ}UWTqs<%E%m>_?sSGugwy-f&lRV zZU&`%gQURG;uXs`C#4x4$*0QkfgNTs(X2Gl;LntSnWPgjEN^6yx@H+oFyi$k9h;~LESJ0OI@{H3RSC5s!Bgx5RrH5!_yll)k#8_sD6 zk;zkI%Nen!`l`tvW?qV*{HD#LI|Sta3uh_D22ZBhu3Mm#63~2AQOdg8%@7`OghhHN z6O)JbNf@^_MT5NY+jLMm0iaFt0#;e=q~qb*BQs9%lC(-XEs8G5tLZL9QXZ<%n)JA; zrFJDzjukR(QM=W7+v=zqqfX~VW3(KNk--ps2;>NUODUdN>0T<<9+JJXsE!m(MYfEA zerPm<@bhx?uf*uTx*5`WXgk5F3Meh(RQogD1wPSADsaNiX|Avx2?M%mC&xFHR-CXzE0j8oS&UhkZ;z=rBPX- z-Li=g3rMMs2!=)7e!hS6t)9b~XTN39e_BdZ=#V7IQFO*P-W~ ztgkj#;A~2iO^&X~&~3a@fG7kQ9I9D3n&vmU`6Fh0@az;1TyO;u1;fK|7{HkZV{IR^ zXAnvUj@;%!1oDOj#=g9xvejKY91tEUn+lHN z$vj|r4D<|aZ^rYrmw;U%`a{1UzH@~zg* zVolyPjAj>RUISY-?$s;%z%S+B>L5KhxH?cz9isFenX);G`8%rM`U7vn;QaiK5=>e~|7ax)7v2bSD~czWK6$KoKb5M^Ru zwj^ZtNZD$TUk{3~^Gx5zUChfC^-1;s!<>&pJsGf(O0}lc&>|#g^MUqoGyXhoMw4d1 z@}8^Bl*CywA(<^Rb8L&dGj`iyroz6ZD}$Q-8oY`?PKCI)O}ffwN%MZ-ON+>{E&8d} z9#I?ZlMLHzyxn!oFy&Dg46Y$&)3`Z^QcOU{Jf%5L(iIFnh*Y9fV;|0T*(C(p*!7cO zqN@G#MSy8(fuDaSE!L^6;<@qbuY93W2tUWrznNvxX*`BY?!p<#%alw^UdpsXH1J%A zofKt6Xfte1f?}RpI!~_Jm4!Ja9Yo1%Zw!>iz8Th8ISN;~G$VZmFG6MxAwg(4b|s4P zxhAN>;3RbGr3VuJg-TQ`;IQbCEVwsgkb(W)UfKTWh9AbMdc_s4J?J}W?{!)cC$1h{o6Lo`F`>z%xRBV zEMoMAG&VU)1P2$31}${`pm4iNo-=)itItKuTV?WyNG39ntC}CApsNi5YMO+hEu6RIBFPxr=YoA_+HAH2PbR|$v_eF|Cl$fi#k!*6Rv=lT#X8Wl z#}^6_r|J_AQX+8TH2Wb3)}Nxgm`c~&0a@5Nsn4W=mb@v?rkZUUD0M3>sDF>Iu~yE2 znZ-or*1c0W8%(7X#?5hqRJdSsi{-Jp(W%r4EJ`p)p7~jpzkqzG{ECVNUS|b?uCj#I z^l(GPn49b>f1O3j#%eH^2jL`S@n~CO@u!7b2QV=#=RU9H>J>W?3+Wh%jxty56?NB# z#<{j#y_C7_24)u-0?gbStAVy?V>Kvv%#GEcs?sWLkXh?0ZP3NcrVWHT2e5N)K#zeb zYU)@d@o54ne^^#)VS0^L{g@JWDkHG|V<;wDCMO7UY<^s8l3*BuAo>05HoVB3_{Y#h-%$;TDY4? z&w~bOt$~30E7>4hyX`jl4?Pph4eSz~k@{$u&n^#J3LPSp^854;>&;w$Z`z*MrT z17^FPG=TrZ6Z>TSy1|rBnB965wEmvP(gun1J17+$TfQ)vhs2pUSD|)Xts!eJo^6@@DQ~qqc zW@FxGP#laLpe#F}vuAXLrCS-8Owcz!Bhxq3{66xj zQlBoTbRuDW_B@&n@r5$^G#IiZXdWBvZcRQ@mJ)!mZ>Us1DhY-_dibbJ>yKjP_n3>N<`gZK6b@Q^qg${#UM_ z|J$JA=qQ5N_!OpA$?QN(lLyWuA}BfE>>R*a%v17nl!oh={b(BU1T|SYrXkq&54dC! zr4}NTPLhv*%}PkrB5b*a#m~^ie~o@FSY<5S7l4TIYq)1+4u~ zXjUj|_=lP}MV0ov*#$cY4>I6yiB1!%fZND?qm;!dO5&h++t#2lQI(*=Yp*+OfR}zBJIJ*YfTIT|5Eq`>0XUfAS);l5BP%9Ir2Ab(hGP z8R+H=4d#>LWNpWA8AKPs!guU~H}|}l0gA@8d~Eg3JY{#`JT+jxpvDMo*K6d4&Gz;; zjsk)uPNRla;8$Zp_9wHi*cV_PP)|p+T?Ir(jEsJ$u;?gDu+Oxyakny+vgQ1c<*O@S zuDCXHq2r5!xT<^*40vVdB!r8#TXRK|6aWMwK|J*>CKtnJk&R}ak#qHlIieWe>Ok6$ z777Nr5TbhAO)cs>UaWuwabboFE!$m$;&jO!mMtf8^O5!o+)~9-|TFD^Y-Q&TAcpwugJ(sbmDBbH+P!t z-R9=*W&6*2TkXBA-B#z%yPNGl|7&}5uib7uIi34c`Y7|i1-Z$Y*=W5u%I5#h+qOCX zw{~CV|584$ATc)GWLQXcHu@}i>h&xdqRH|k=zoILsjZb)Oqm54iNL~*V5nYlJB+W% zD40@_WqiAi79N;`280>qIhA=RfN^-s1jyA%65d2#M1GZjS!)sS+(hf>2_p%1crv5{ zLTjaU*t>$n~=rbsJ4L@Lxv$O6&Tc$p-!_9^Bn-z(3+=dOzOC z09~_G!kAEM34Mv-d|Q=kHsi!IN2&l=+_-VuR4Tk>2-P55Z`-IF6F} zzx#Iktug=K?rwKp=KnH2KmXh$8-H3kh$jzpw%kq0yCe<12 zdymxjfrZ38@zx>HV^ms(A|sG%uyhI=dJC12undGU{t){NW*ZA@pjf=4>8ZS*+6ScKT2?3MRwtu5Abkl?+fYfg7U-!tt`nwk8>wTaBn$xp;mJ)fkQ1s`U5-bGMWEb1Ke z-!GJvf3BHZVd{=Ks1325DL8HRt)yef}4* zM;F%vl$`%J-|iUtzs=6(+pU-L|1v(yIsdByiHtSMJ!MwClmlz?X$Pu!MmSUkYjx-h zYB)w)SEI#N2{FazI9nHaZa>=S3IzC)N@a2+cn^q) zW=fI1&hi$)(E$4*0MsV{6zKoi_>8!x=>j4^$^O5+^LEqN|2I3G7yqB7e7^qv?^qYi zI2Ln?37mZ)gBJ4zMz>&7c;Lt$>xQ8p2g5M!hh=Teym(j31&p=ATZx*tS|jd)|HAlW z?hov;j$3M|NlI`dC~1yGW+>j^b0Gl`Oy;`p-h%BlwY^^MZ}K?)nw|^m+knf$Y4|f7 zA<8zp@OGRv(luPWnaEOw|3nZ7DL_qZl8!}NQx?NUQj-OYDIfWKZ9`?`ONJhEMpibW zyp$8Np?lF5I+yXcP&+65ix7nwh8I(Hg5pSdmq=&?)7d zWrd;9BRAQew&>w|;0 z8Hk8Ew0}7`2#3dMJd3B00pnp`5HG!1pN?hPQx-R#+ZE%>=Lvt@@*h~mLqu>MM|u=Y z=9L0VHkgtyxn<`|CaGtj{N6ga+PxJ&Jkz31HpB|1j{NKvQnqPq}2S&U}q7X z@x>bKIV)DgfQ$53kh|(;kNnb{U(~bi*P+IgoOg=pd?p*Cd;RC>UAXpi5d}b*|L^u& zbN#2qwqMr&Wqh94`M*d*${CWm=SjODsmMQ5Ust*y&;jS}37*Jpz{{rb2-^53cE+1P zR?TOkRR(Hd!dyN~LV0LE9^cC3XH9Y>e)vyo%!GKv9dcQH?%Rvs1)iVfn1aVtuD~mT z;lIgXoMJ4;;Y{X!Y3fPmGSi!#E%^v@e@x?kJlrE64!gcZFAusJ_INhEhD2--5PTf= z8J|vLkV+xRKruPW)rpai)8;%jjdchH8;MVi>{Mwj>1=1DiccYjsa;fIPXLcHm1*jf z0o|xdSJ0gMn*0%sHMXvW7o65K%9dGxp(G8gfyQRVByT<@sH4Ym%Kd*AR{xgjf4AE^rv7((>*fA`DW9k3|HF3XQ!Z{S>K*K_ApSl2 z2S2A{yBIsxYB5~6C}EdRKnd-pZ{%?s3x^3;8fwJUa$Po)Gn#!yuhD z*`kAu9><@-2L@Hcn^az=0_i3dinrT44V${IoIqyE$mMv zYFAD4OC+Ae%ZqT09y!xsb`}!Jt1yq*K`Lp)^i}Q|&tY~c4~A0&#`e7i%bnKn+gi1o zwzj%L;Hol>9))%V?xWEU0a?9?hr(Xet56x~JSOa?@WHAWY+7n3#L3^03BcsHkdh#Uviaw-3DuJf85Wj*N*byPA2`z* zkR=aOfbx+iRr7E#$T8fu)!nH|A-(2t_n^2IEs*PDh{LX4*-eK^xL(z|Czo$j6oUZk zDgGy)0dJ?yfMs8?hAD3O*|k*toJZqgg}s=eZN_f{o7`*FZ|e$wRXrviykR zgYtUOQ6P^4t7zu|2?zv#OU@GqjMhXJx7&jhO*om0TdyJ?m+RXy2#O%FX)!$EA@Rpy z2^=B8jUlgYAz9|6m#^ts4lsNKFazycSu0GN8ua!Z#N#(p%rPD%AqimDxsQh7ZHOkL zj5(PTaFh~?HS`=ES-Esk{5lJxi$14B?bD+`D4n;uxmA;FTu0-L^v-zN4Ex5DUkppA z{)B8yN0SYlvKxOQ8}Vc+@|B>0T$RCEIub75MHVZ&U~GC)~# zec=^#v92=Wi^5X37~852`$!WX|H=BDwKOI3~aUOWOwkZbl7@R2!>3* zx_QXCeIz2OI-&Ab1GIsSJi7i}{CP}1Mg!8EecZ#p?`aqIXH(J~kOuu<^Jep-`#7aE zNv;hJLsd=XrQ>&j_h{R%zwSAS%bzoNdU;x_mL{3Z1fS6-x>l3sq%wjHSd+?baj=5s z*m`ivZ*8h0gzFb~L)fe1%DefySd48nW56^-UZr=E!Oh!&BXzsgqS!C=)}QM5OG z9j2S34em93?xT$p_7aEQdf<&mQITrLePosBG_!(Ixwo96If(Hk!0i05df+q;P6pH( zgeo8DnNykvAe-Prh!cuqI#pfAWtZg_Z*?GS%ex;11xx#L(GNHpThU;~r8utBV8*~v zi@^-ZG>4iavDxKSG!@nL5{qjgewQjtqeBK!a66_W5%o{wfn!Y&%7%rmUkyYYW~HWZ zRgR;t7`*xkx1w3{2*81&*6zm>N?m=Az-p#9k6@8556Aa*k>lO*<=Ot}@zvqcyASV; zT>&X#ZeQRAl00zlc+$8DXm>O;oiAIB2I2MWwq#`2>NLDb0G~kV=7v zQBp*hMSOWh_Gi;Oa6n>CM7GB8tm3L$k=)=ge>mm-~6sad`FJ@yU_1JB1soOF~MSIq1e83*1|S!6+IF;@Hv6 z+7Y&st&?mU#ouWGKE#KSNbz2TH~F{aZUL-*&RACcd@6gA*F*Ft|4s%R*3vE1|~0id*sr?q~d{%cDx= z^5%fj%DQHPts&28GB~@$lgV=(a&YpYcX@PiwcqVt(bJ53Kr@kBQqT$}^1E&n>_B$9 zkGQ_4znyp-4d;$@c8|(pbUp{6VoxWToWU{2UXja_UfKER=xqPp$?nbNt`_G%8rCDPmk? zLkj1A#$(^Uy*xfW`k(W&3Opkb)Ijg#7Mc4bzV*@nz2EXPyu*jyT(fBk>)8IC& zG822>?_V4p0)wBN?;lcbe|NEeaqNLQTNZnFK@tuqyAR`Fa1vc7K@z!XC{m5GXu#w9 zAA%$&?ANAg*N}K>+>5U{Tz1hari0V+GR)gNr`FF4`M!Nfk>)s{FCzI_4+rvSFb8bE zC8AR6fnatTS&&*gaOa@r9vTs)8D#~j)x<};v8Wz_TK1mmFy(WbR(L0jy2{=(NT#z% zH;J!JD;niZo%f1)v~}IisT4bIwij;Gs%F{bL{AzVe`C#maS6X<;W;wSv6#OzzO$LY zvp3Ct?qP|sFIWv-2WevU^NtSZ+g6+YuVa{&drokJ z#cLY^W&d5QKVbkLy-ypesdb%w>b1I;ua+m#eK@Alm1Vgcs;_EaFLlIqEtne3_7$^+ zW)&=%L%mrqYoWU#Zr0JVf`v0N zTK2?NPVmIEFQ@LoX2RgNqSvW@n zzJj~Zi_ccMTKETLC4St?01eY3Hv&eR3KIr02H-^$z@=_P0%~=4BCTdsx1wA=!>8j> zR7TqG(3$ATc+)6bxIL9neh`N&xTwwFrf3DRgU>CzO}>-1{xJ)a2PzOu%~&#gL(Jz# z^4(i_T0)rN>*Tdy5hXKWSz8&MumrH1pVt|ijbnJu5X|aGBM)#j9p*4#QHj&9>$~8C%lF?!AiEBH9EfGJW>eE&8ltk7WvB zg};~;M8Vzo-E=x>=4BlCxNpY&Gbh(|J+5yZ%eWaYz_@_2Qb1k)eYg$tT*}F5f6|Nl zpTeoR*suiK4uFqZEv1Irv5f6)qBD zoQ&gf74t|-w}zqm@@-*p@8IhY4nRbS$E1Bg7tI`>cbPea(fh2PNWRKNjV4hOuHSd~V6f!rT^r&Ru^#E!eu-^F zeJ_A>$-CKSo;2AW=}jGoy|MsSlsQ#gJWaY%vnavw!cEI(Wv|dGHC-zmhdve2Q&46a z0xG*_D(~g2zP_O}k>~43i>qJ*4(#EvLam)WeWd-OgQ|>}8*kOYNwnDSw>aOiL0x+WWFuA|LQxYxN<;pQK5_TTU5i+z#XGV3?I; zzb&$YGQ!H&KRnJywTKf-{Kj?A|1_IyOz!)5eY_}+cBkFm-QFhccDwU-rvrcCBmQn} zZoMU)o%ZJ2&CSi7%}vtYYQMn|Jr$z6LMv+>He7p~E z*v6k$4&uo}65ZZS$-5*D29)pD$?*W3Kx4nS-y-|NA-RATDY*#YdOIAnR(jpT|8AZ{ z{cxOy&12wQdejT)oZj#5AAEn*+-$X1{X&^O@l6$Rh6WSLy-8|b&!d@J7j zwqTtcg$Wo_Y3CJKqR5c8(A8laaOP+MKzUMQRR!K6{M=e3aj5bjDym4@q*j>5F z8B(cAm@CSbxF{*S3vl$+8uhb)A3auPnDY$%+CymGRxv12jv;YW7%4*T!r`cu-ff7t z%}L(C&<~&?3gu4o{D%;N4e>WG_&Ts!#9o`gtD!eUKvVd$iG2?2d7;m;d{q#L>}cHL z&{CwHmyh@q$$w(Z7L@^u<-gAE*7mL;|8?41?HBoP8K33Ie-7D?&vn(@o`1(bndzKR2(-nL;Gr!KM9p_Ov&Mh2SB#pFx7Y=63Gm-P0 zYy0ITt*UXO>{JWg2EYD?{wk6orq)<&nxI0P)}JM`7y1ttM-KyDt}ugf6GU|Npff!E zJ_32-A%7=j0J7I$(9#*oTXf=Af#|N~)3s=ANKdMVVK5j*pdDxV35s>-&6AErYNcmHM@Cf`NlD7{nfXB-6zIi?I|PTe`xsE1GfN?|F>Tizth zf1NWlsreN}f?UeV;$bwSYxWCW0@O>1NEq_8>llxI$W z4~|%7H)Db83KPR?`lax2T0YL_kd$=??T?~x^jO)e_<91xYaM-lF$WUQxbRxKsSl@JOL8n*1XzdW%ikltFafeMH+sF3i}UkK z2(+Ff%K{Qo)xHnMuu9ti%cdy#mHPp0#?%VDeC3c+GJ^-RPbH~1qPj#?A$fHzKE}Co zLa~qWwn>EBkbb_gF3E}NlNHr}jV`$q!rQkSPlG3q(Go7MvzOS#TUD_G?~IEe>yMJCZ`ze(RG@oZ8=pal=F z<#G7S`I}hUwRjG3o0mfNd=XCtXTuiUWj!mqth3d=v2W8JhT8DajkPn!1v*Vs%WW_E zsz+<6k@Py02n7RO5H`UjUsyd51iX1;dX!5Ii_L|?^X6O=!R1#WYtHLiQG~}|2fI|` z$=|@I=>C5kPANlvV)il%SOJvSe{OEQ-8Sz3H@DiIm;3)^e4eWPC+EGq3l6*DKT}_8 zwg8r^`F=17P@0e6hF$}l)#U5rld>78;TFqk5m3JkW{FAT;@4Pi1SO5JT$iJj0TqK! z9?KddgY9pSfnqto&cZPmflB&pFg&o240C_&d zFXFKgYN^R1V(e^hZ7SRMB#EbSKOXLp%Y$yd@YbS5KOA;nqyqh8-T%+dFOPby>6gL{ z&;L`r|8KV6ZX5f5XLtAQ%l^NV53Yp?U3h{tJQEv{ZArB3Rdfm+e7}EjNg&H7Q#l;m z1-E_%ng5gdqv&|LKwx zCUhI7N78#%IwkQ96d>=IZk6;|lEeQ?zKauD{w^5zsUU$$l#rzH&Ga(>N@tVF@PYL2 zf@n-`!eC0@rt4%j9)d{^tpK;iDY;4F5jlh~U&%T-x$I#*y5g*Z<-d}xna3K4{)xZV zDY}fHZW4~7*$8S}2IGIu$i7mKr(|rA9<2fKg3`T>jnAJyw}K>zKf^Y%!3?Fte!4LZ zHty+C7AL9DB$FEobrjPEMPvGP#L`Hwto-Ruq>DpLu0se>1Zfw^=R0~b#lGr8my;-^ zZ|Uiu48qAUexQy1)1Pt{obDiXmlDuj0Fc6Q28ecQR*^tJO(t1RApBBFE8K^?1`D$} zK#37a<5xQpW=seAa}rIb^!w*%dbhW-@>K%Ya`9rR@oEw0%KLDNU4s$`<#xKc)4qAY z^cXWx-(Z)(4&I**$t^97C7Mk+dC_bjH|Zf8%h0m>M`I8&`k}c-l(uSUQmFAiY~zIi_rs&>;aLMZR@D6NIu}1R5k9E^h;rqVq&%9YHap zA{`~Ali9chM5@ZR`OT!ctjkIWm6UCL=NC_{VH{IYR=wazP|NN>hcjM}bkg{QHVLXa zOd3w4IqL<|;NeGqVADbdQPlwCTA;rbPswWd53QfFkfv)!Y`3Q99Ds0?g*t{QkIsf_ z>>#%0Dk+G#kq_6G@+6(Q0g(2NVh4Enktj17TFvOO#e`iQhtBp&Um6jNS#2pW)sOoZ zXUAvn_Xx+>`>&I1o$d1=QM4>9v+-<7pgx@S`+Qf!sX+e&;7JI4gu$F}6QV2oV1}8l z!JI0h2P68DvX?43zS&LDRX^4~Fzm0^t)kex#cgE-*X;xZGMP z%StXt`ggQcAH)yR5K9wkC!oKI33Y@Xnul20ksu8jw2EO9roxvVHghYNF>YeG z!-#1EET)7jrfVi2$M6esjYt%IwSNeCsfD@Mx35>#eUU9oUYn4=ik&oL^7k|zLnO#= z8$V-!2RZ=$#$3Nbs<~eXWXRjyCe6W`*7?rfFiZgfh`U>!ZinPzVY~W+Z;f>@Qh5yQ zC*PYSi#^VpR?&=dk)4JVvaG?qd{qJ23)f+h4HEj-SiQC6ZqioX*u~(T=yd?$?#i63 z0@_-ZgPsmjGNS)XX}g;d{9@Pm-@Z8u>4D>opP<@A3u8a4Wyt3+zW=t7*`j{?I>%5A z^1ob>)ohCM2o-!MkOZ;WyrZxyZ(M#UJy|M9-sQe&JpBqPlUH7UW-P*UKzeWak~O5L z{9O6r5k~r%jo>(&(EmX|2f0U%Z}3DJWQWY`gf>fIjerx~IvE9@K#7yU`7D@X9dTf5 z%y~>~fYw4D6NCFWVj77sNl^HpRrd%JnqbdD*^(R|s+KomT%2ZVlt1^y{`4yu#32)= z@$AqjUM*3crBXQrkYydi#kapDVNX8*MG<9bNozd|+7)d?~2Dw>8Ce z6^ifhe->P26?COvh*(C3N`JQKLI}$UX|Ew0aBh(4y?+(!OI@=z>lVS@m9!bpeQGb- z{3ksB57=gV-1Xcu#hifVHR)Xc93%s;i9sjOM&-#-rJMIEjmiJ1bABj^-=8$6FMK93 zXNJ=7XV#$HiCz`lr9SzR$>~CWER`*vqu~&?v*>ml!;5%K#&i$F3#Q?a-GEY|yASd$ z1STevB)pGey16NuVm?dA=OCpV!=fTj?~-_S%eF&^_a(&L7C9cX69K?nC-HDVx<4E+ zLh#lx7~?}h;yP+I({Ku^dlF40WQ6)T9>1Bg-8;Owp%c1AFfKEc&8(b-kvv)L{*bOQ zTxmSKXCir&rZc*YuM{}Y-v`5p4rllSH#Ta?%L?<-hAtyxiac#4RM4kiE`!@_L1Kr` zxvH`jbi-ss$%R{W4y+WG@Uu|wkI+nuKEV6RhK!)!;EPuS6x~_SRQfZA-OS5VM#d-} zfI;Hm7>LejiNsIMc8j&FtmS650etF579JRBjWIJiSUyoj_8(UJp9wu%hLl-nX)>cS z=i|ozYHxNrTW0((TJFXEXBnU6+y9p3@yM9(0+>&+Fj38X>BNeraYCnit55fm7)>m* z5MW&mNF}RYn^rJq$=i>EVVL&wQ_!G36Yy5eWg%}+wf3kH!yFVo-^gKEBA4fAgnSDg zchWv~PNswpEYXWL$iW*3x;VATi#hG{{#f^aDiVg*bd+Irtsww>$^PHj-8A?A_I79M zW&dBsXF2=7%#Ls!^jm!@?z26P?bR2Q+ukv5tK`Xpz!q}g@!IO2iPO$=+5=&I;USbV z7MOp$C=YP2cg7O4=^d2}neBD!(>EYDtfaH+ztf{AZn#a3wZ{}4`~5ydtHABFJbsf} z#qg_Lgm1pqK5AY6XA@9ANRQ-=1%p5rum4+}x4RwF|C1g8U)KL+e3rZZ3y%7L4ECSk z=50UT84;bG(VUgki~@F(ac?D)EPxA1E;>dkI$qA#S;Y&_#F!FGO{(6%?@ z8#VGk`ALhf2Xq+_$5O7U-`7JmGr~o}g}XTZglfK!4#{m23~0w_FPlt#%M&udOL&Np zb;Ch8Xeh_vnqxxe`Oq&cw5{FNL-OOrZOsp~I8lNVYJ78(4_%yZty+{3tcTS#L+aM= zF`K7p9;aFO)S-_k_m9=`c=}JPYEw(OTPy4*OUjrUvmTbBba*EzU1VIR=3-3G6F6A$ zP&R@wosYi@qTwtFD|BILsHLTZ3iBbYdUq4>)QB)bRmcYiP?bIu(i$uNcGw_r*(b7m;7L7SGy739eTqzi{R;a86#`ilG* zl5q%z3^4(VH4@Nc7uxnQ7m{#%PwoRc@9BGd{E*U_Xg~oI!W(euj>hEm>J1$NQV?O3 zG|@omX6@g|AT}VuhoZcqD6gO^ziqr;g?_*oz^?(vTEkYQe;1R+5t;;oNyKXvhP48- z9(--$`gaGC0Tr4o9j+RVXP;J?r!Dwjw2X=8tTv3U72c>&3%vQkomVf$&7T^P5_BQ%kiY$nG;atflA6pg zQ;tJ+Zl~w+@hzPLKmGJ?z~o~K!!Vi!wv6Ur2qq*Coc2MB^gczC32m0y1Xz5WaB)q; zzAzEExqbiS^5}v4M>XiSGHhi&MMRIqqcat_7k}m)L`xa>Z#nCEfTpV>z zjt}-PkI&B-+mtZ3TK?-ZdX9QK*V~BpngK;jP;KlVWl|K}4?q~$1Ma-$5@If9V=(+A zSQt-QSllYV)OPOCDO)_!z1%MIf*9>ko)=B+eYJU3n7KSg;;{ej`0{N3^!V!V=-r3+ z1`*h9e%9w6<91P0iMP9apY%Ff{)A<9eEz91?jgZ@F`2*BX5;lT8>Ov z+Y*-Pg^|YDXYq7Cmj18`l3%AQ#^L9s^nzpFC``sBKa67b5>fj`k+nyT@s;fqw}t?y(@RmiDH~o5yLbhI^5= zx|3O!r8H9W5oXFFqmay+2qjDy^W1mZavyQQ$zgwB(pr!=Q9vrgNX3%Hkx?Mt+i}U7 zUzDWr5hM+-ERD5Hw8Wl=Fe%JJSOmtyxP-Gl{!3^eXnSOTl1Aw zov+pBsx4ock*ln(lN5#w%V%n@BYif3DEW8uX&_4}C|3fM5Mal?EsLNT6LT(gM_Fo#7)#ev+ zy}KRKxsZ$dBZZH8j2nH(*9;1Cq*o`oZbLVl2~$yfY*uAvvusOaX$yZ-A#c3oKR9a5 zC;H*1ME={}Y@7c7TU#&k-*P_BNB&dfKDH7%qlDPSMY6^#R*{{IEMAWzamtiutGTdp zj>KbfJ+r4lE30kNP?&0MtpZt90plK#+t3QLxUE@b?3y~xQ|dOYUg{F7vvu}GqJEL6 zf434f#DspH!Zc9+8Fa_4XzH?@~U?S^tYQJC{RMOCt~AnaKvMP|7j6pHw>^ zOQlT!fNsO_!M<9n*$*0IYj;PM6O{{8JJv|FTy( z6%K*P2(xt_b(*?tJbQWXSw!<@Iw4(AM3zmPJ9ar|jEQC&lDJ5UF+ z@d+%y8XOw^#`R-AaH$Vitc7f4=09R27PP>0~hKc)Gfb~bhSuk*IOv-=|dE#tG?^S?9y!%O0a-^Et(iomKeq~ZpJoq|#t z(--Zr3J7k6;$(&I!{k~mI7>qv3W_sXfyxYNyAROj63ft(y`Fba6=1H>+$c=_1#KhkMa-}Ya=|8E)jpE|p5J1_hHQa;PG|EXsUSLFn@Zg1RouIbhI zEBqp};zR3Z0*oT zHz~`GRTDT+3!RUL4;OSnM1N6?MJ>AGWy617A4mRYR8fN87vvp?f+5eQ)?{;U6jYKA z%I^QSw@v@Aovrrv%l^NN&sVVj<+l%%xsEAw9bUJ1v~}xNw!BS)c9KXd}X`L*m7$D@%y1$F9tZ@B@tDwif|KHv2RU2|nQ*K%JgxmJx5pArIyV!QDm_&9 zU*Iu%jknPCzOa$5O5oWO|7RMHzivAZhCym=AinT!8J7OaLw4M?wd1(!-H79EaSL29 zxUmeaUf#w7QC3VFS-FtR3`OZRl36tkQ`oNeZy@Kul&9r09v0%Z0$6|oc9^r$!CuvK zHYmpKz3hUAQ!1gN#>Yr{OT{PvfLeA{{~gcOibxowT`{T>7<-VqfJ`c>bpcv?S`M%?0%U zW%s|kyJq}{&Gz<-{&y*#<=TI7hEg3;t7`}p^(}|@yHVSgtYo}9DxR>#pe*%-U4-;m zp0K4vQ6esz4R?ymmTBV<@9k%Tl1gPV>)muZX=V(iNvtju2u3`$%c}<17tzE^G&N@CZEiWLQdX!7WdggZYbX5 z#pwJ!zQ#^tV8pMYZP;E@+wbK@CzS=S=^3xSP1r$^KwE*=!1zq=w+rvfStDJ;wVR18 zo~%bZbrMJ8On#c!1Raa=$k9m@6nzjVnl16fXVCMt4Goqr8EHQWCgEi?3gelQgi3qt zLy!Z`oKz(tjy7HP75P$mxZt$*!0I?2>@M6i_V~iqz}&0HX=a{+M{Dd|JdP9fejPr_ zLm$qv!qDiETWe2S?{K9kaLw`30sKa+gNp4x2iHrm|JmF%{XaI_TRWW>`F|Oo_mla2Ot4uwL z#{2g{G(>Mmtgk+?-fr5f3ACnVjP{`*osU(f#om}^kt{Gper z)$`-U?c<5>w)MGv7@YRI_xVsiW&R&c5z1|WgJ9YD@9kSN{^Mq6_r?EXDWB)&|6z$P z_)OPg?<11B9IHl%meYW^h?kSLLRjGCcv4iP%yB31*j65%b{>l1#Tgwo`s5Y)F(jV@ z3Mrry2LcCCI?41dBt42bq+&!boRVow?t=Reo<|7@1_R3GDdaksUDM9H3kT#TP7oT@ zOoLA$iEr?GO#ewpH#2`l6ybe1Cf6aYgfJi8(7)iO|FBL{+D+^s!eeM-TA1KDj((j6 z!%y%x4ElE*J|#I$Xcw}Qw0}9c7MO}K8qebC1w7gp#P2~8lJM)>KR?l<=ToyN5$k{9 z{tq5E6!>87lOOw(#{b{S=l|M%(f=;xvz+r^+3||$f{!@69SCj{r>zB#dIlTM5}uFx zcXH_aP3mIBc*pl)l0<{B1!KEMere7xADHWXsIepGnqm^4-|?|<{U5VHNc_%a?gL<% z|NmCojQ_H^^Ai7UIiIg{|Hm1Nx$vqcer#z!Q(srQ9@GIZ6eISL?gC#nfycR!dpa`z z1R@YVpBpE_<0xLzB%?#e8ipgSye_-h~EW@pXFeJ(`K%~ zD_DBs&t&lbxA(3KZ6ry;==|m>^2qKr>P965#_nO)JBxwtW(OMvu=|@G&zh1{pc!J%7_fWIj7FBtPjEszojEIbgQVOR#oKis9%*9SQ z?_@rATbt??h7yb?aW5Wjvv+&Pb&HHNJ&0RB&nIWN#}V6s(ebR3=yR(ttxf`yy2>o%#uX8;$AA* zX6`1f`Xh`CdS1%(7y8c`DI5!Xkfp7jayN2{{q|f|xRT|a)xSvM(S4I{4Q^->Y)!B% z0x!`~xrKyQZx|BwH&7?mD4JV0eg#>*J$;iwWR9W0t+CcfT0GV#dwLbzuCb@IUi8RE z^TDL>gw1O54$sKKJ29jn8z_SmXGhg>+%JS|z8PN-Ph|kz1*)u<&edHV#=yZQg&=U( z+*o!q3p@V$4%-j#sdfK@lngEA}+&CgYjSCX9bD;{w`(Q<5SB27v%f$sq+6>e`)J~J6l_i{-5{qJS6+y zvh9A4=Dy#JrdLjL!NJ9tX{m)(b1Y1Q%R9jI>RSZsg+3f#{?zq%9fP?1IOzJzAAqC3 z`lEQ5SxbY-oyC9abe?a$*vk3;Zaw<{-pg|j`~OqEr(t%ak`&_86cx(K z%sPwFS0~egi=mqLjS2r1N2-$sOP<>+&2qZW?Rj4y&rHX#z=G2W z$QHq+h-qiOmm-X4l;CH)^OYl(FQi>fm^oH8w938{K}p_gKMjuk$$&Kq)JTmc?S^PC#>Ee-2x+cC2@Z%Gd}isu-`IKq z{g>LbW*lsxcX4jeZhzrJ|~YU9tBHW<7yAhRYwdl?+Vu5e0;SyL|my0)n)uzPI%)adIh zz6v5|D2lQQIbRmLGG=Yv8HRou7(u7h%jMiQ)_W1O*)y(!+ghd3!$k?%HOlj%MI)@cRIp*`@skzS z-!5m#82v17rBC8vkk~t-_HsGE>%A-!_HjInFKUh~Qcz@B<0^un zAnd~&qlABB%r8$_tZ-kL)oO^C>`0V+~D-U?#0 zT2{0P4XRhw0L`=hj5PHQENb7)N|eUFpaOi$;SE1h&VdlAGgzUMFhtmTFiwINJgX2T>OsIO5EbiXfToGu6qHG;JB8;#yO>gZM zYj?w_oer$Gt)OSU`D}Sp_r|O}8I9YtDBIt%c08W6g+`>U8AkbcyYgTZ_t}f}^^(O4 zoMRX2{+YyOb0~aGzQMz5YLAEW3reWh>W7KdF&%$V$Fxn*TVK;@mw$s#xyrKX#J?d< z6kdcqH29YH(6*UY)=)Owy6u&r*i^y@25?_nhl^!Xj5wO;7AohyQofyUW-bn1*38Vj zyri)^=jxSxe`&r{B+5GI*?B{RC%{~V2|S%(h7|Z&JodspB$-A#DH-iStZJa*tp zH;8W{b{_UwYkIy-|NaQr>rE%D)n^U(U+ZGy+<6AlMxz)+!HpL~*<^O@Xt~CSe*10P z1ycU3aFPC)#M3cUSab|F&EPY-0uIeu<5}8nBPXh|d{70$ziao6Fn7$<7n&@J!RK4x zOTGnuh45d%8lR;()*2yK7{ebO`0am^zqPxQ^Ngs46&_FgmdTHd3(?JZJ zO@BRvp1!-lYszSk`S4{q0%M>2BFIl@ET*G!UnzK9o7@O~C5mESy>^U{aYW7i-?KJmEYI8td%%JB!qZGD%j}z`Z zw8o>bObb(d_Eg;&cJ@lywHl&1fT;_Bjr=cqa;(@bIc%m%+R^)BqEv-wEXI+qs|Hxn zePIIy>ugrGE^*n<^KyNLT-?4M1SOpQYtaA{47R>&Ns-=nNv`I4)RKx#j3P}qM{m{RR0RLg{P24Y7|58`5khRn5NVGZAP=rn1D-yHr=c!gX#F_^z6rz{nNi3emp)pIs5qb-J93@CxyUe zPTfUend)2WdwhQ7PjNo1J;UGf4C&5MjYxVsAUO(qJsy|o3ZCMKr1k|L$Dzxkz9v=PtasSQn z*#SwR5|#nP3)Vdo#T* zvBIFGrt7DHYa((8N7>@_&S^1;)aqq6QNxrb6u~FGK{yF|lW78-i93%J0wnJqMpw|f zw?*obC+sAvUOxVl{o})f-JP?8qqiU59PJh3LCM7F(7#5Zc;sUenY{3B!&XxoH6SV7 zX$%gb;pv^YihZZsj}GPi@fdBC%i=VyfqYHOrVu5wEkez?Lu=7XZ=>EIiKBR$?!p;1 zgtKaVagnp?5O9)Pm1_WY`PiN{rnvPC0 z2m~WZXJm0X&~^{sou2KVeB3!c{s<@V*)Zad)o+)QzMdGO>p=TmhyDWj<^g>yAIibo zgR{NYA9qfEtVXP>)44W_f(ElS@v}d~LvPhQ2;hI-Ih^ItU)0+&hcb1@gii2JcTWzE z&-ihq7Ae)}Xg=f%zB3cl<)^k1+{@0Eg7ZoN;*=LCAJ>~5TGKzl-?fJ$dlrBH1SD07 zKhe)D+OdM}P**myV(4&h=eQKWzZ`ztf4lSgaQ|bjs8fWx#fxQ)D9(hZVc#EDNleyM z8dVA%xhlK;{#XiVRyWV883Rv>*Ys@}%M`3>|S&D2K ziK||_;}RMwgnRd={TiH#!`&MM?oF=b&_gvf3u>Lij|A@QuwWx=bZRM(3Q>Bp|HHxm zsfpAixClSZjMjHQ)b1gjUd+)0o((_ly`7Qt@nE?3Hvg_4)$aG;?cN;yhEYG)FTCqV zg$#n<4HByBT*Z%a+rjHlWd+AK&D!)vV|%z~4jfZUj(ZXMKi%Is+5PF`&e>Tp$8yhO z8lX1dqd%D>t`9r#KI~3sSSK73IH#VKaL-a$F2W(~U#?F-@GlFgbe=Gq2^H_Lh3u+) zi`Csr>g}*K98xss04(BoAgJ(N>f`Rg@lX3Fa|4Sk5$*oCi|vNv0eWoI=UxcT?mh@i zrOJtN?>fgiDd_3X`+r@eqvDghH67?~v%7<;n#gewEJDN1+_3EwprO7;Q!g3L3hv3_ z-0+?pF06D#LufyoRPvRhA4%}9bprFTMQ<2iqPSnCqbulW_vr20{aw^mRUf%kU%OiF ztJ%8SJ2;)I$GtG^Rd<@xLyFpmVH6y5F{mDxhX-%>>k3(}UKTn@ss_)(QSfUV)y3`E z!JGa6IeI$-&8QBR0Ppyci2E?UtmBRGO>?INjxvU_;2|Mu+Tn}fF>-|U~B?)@cccbfHCMer#L{+CgS2RXC*A+Mz(Z#8L7uUtBV$oOw zgV9~g(r78A2M(5|Lws4%n{j%=@>juab#ZkD%D6#;wFWSsFTHj6U&p#}TXQvAlzk_2 zun<0R0`DBB4gG5BGI3gqm6x){!|*zYKss{p#-aXcnat7ba|zthfYi+%moOMNe;k^F zd1R1}4Dyjd{*D;r37|YI;E;*&ZJ8|6>U9Y`(uCE`B$qKBHK+CHaJjnG*S!>mZo!+nA5V|oo$T&^e0OqSZ4H?hh`ev9 zRG8b}^p!C4r!*aofQ(-C!xTqCS1yagpNd>!fR~U2ZY|xcW{k#Rk@f} z-PEqS`|pTt(wyV^sXb=ba39Q#)4RsrjJ{#U0w}v6UE3~Ehl|~n_mi{z#k`+NJZ1)y z$+(r5aqubjm&tAC+~_fKarEUl&6r!qeK^gO^tgb@L$-SVPBX5a=Q4IodgD{n%uH-% zGbS=AAgDT$n~C!KG5?qKxrBNC66Swr-*Zhh&gGAeiRSu+yPf{~GH#akx&#XNGR94J z?+=v-9)?BfUrtk6^E_s#@ZF+<4yVJaHmgds0M5D0%=yYQo0)S>+_{;#pvM_iBh~sZ ztVe83Ss*|q%X-Cl^b$ckOK*&8_nHH(6ZOfz2EM^y@gg z6;J)$TEb?sf-N}yzy@R{JlrjtugBs!?>0xD9mnT7w9hp0%kmwl?Kd#@jN5R&sXgpD zJPKy@Ca5uuz&L9|@^tR`T-?c^o`b=h(`bP={~T7i9?ax+Fz4(!VW>HERkrFHSsmtQ z9nj}#5%;KXATQi(>&>8M)my&MVKhFU;NjdRY-?d{Il5AYu3Au1$NfzzD;I}Elvuc! z4u=t+;^u72tkeOa!1+MYgE4?n&=04hZT2P@#fcd+tFX^B{53cX@i1$hMAQdE7g?iX zm1yjk>N+_`on@pX-c`CHW95Ql zv{;I1F{$zl(eZK;;0ug@Nyc*%)H*ddVs8dPv=>!S|*>y)?|0+aV__3?w6}gZnK3;{%NN$0tuHoh5 zM?il*j~1cT)Lt*g3%aP!BJJ4voI7{7Rs+#M)MbzW6y3L1Jq|Z}Qm*^FUph2E$S>U;KfsFQMiTysTjWu=<^*pvS9I_Mqkg}720)h6umDA(B|I<1QdqI>2tpg--IPnFr;&+aB zc7NJ$ZFuV|-?o9!SFrh~0}M^O{i;eoryW3Wkx*3%5n5c;aw}5RG$)PX%2N^M-t3Q; zkeWTLuDn-Pb8-FnV%KhOa>bttF>I&=SwIP1lRt~a6Ite zPDeu0u=;-L$fSV$qtVcytCgpuw8i2@B1t3lMt(%D#!g6 zc|Cw-2=W%?haR{pp?kt0j^gCls)y#|c}eLhMrJLXM_u`21Q1`fV}%>hfAgQlH%)hs zuS!A_m$v5`%eZVDdOTv&vvM^G3X}BZntsTHX*rS>QlwR`m=r5`c0&;B_)aNQ1{*06 za?E%nYG*De4S0Y9z^RxOt_QX>KwS63i>&Bso+@ijZ{Q~rTgcIw>?ubwK? zu+RTxsu2E-Cob@DN~9S;`0ml??BK?09p-c(h85tspO(kTuyJIV;bk)>~ z=UdCKuTAD%BGfC2AD#(~5@4#$#j`m!F0Ts|bSm3*=)adaE`#E6!IC?f=z|rE? zWe^1kFbPWLp+-VVY8FnOrDO<@Oq2O3yTR)00@g~5F;Z2P)NM&w#%9XQ(DQu!#^zc* z^1NV~tsy#ezJ~t7=+ZFAmeEzK)w)w&+Dfl`hyxYS@{zedGS_=yu3|xS`GWAmSo1ek zELX6EB(qPmhHy`96s$(xdR9DZwWtPan)qrbos+xJSYA(gDiYAR3Q6UVtN)_jrK~ke zow>G{X%ZS{{tPkOZH4>oZ_@wz2LId6V90mC5mk(dJR3CjvU_7{!W9Ci<&m#F^0i04 zwhX>zUS_Ppo~q?WWDx3Dn&*nyw;G~nDVzI7V{qSC7c(li3pjj0Zr2d8yF!=r(@z57urspSNF-fN7q4;zy`pFjI5&{ zT!c}jbkoX8(;f3ww-jS|W!8sAHIIC(l}EDd73X$r=c(1M%M^HAA*{C1fgd&V0SGHqPt=TBlW83qZ ztsSlXv~teMCjiGMr{IUb?7g+oEhvKxpvz&9rc$7+L^d!g2kvUG1voQ#vQH=K2E!TM z!S4)34vM@lZ0Ury5XRgTI~UeY0^kA>4F}E@AcE<*q99+i@T!I;0*UcHatA8zvWusuLQcl53K3NQ7R)$)WnBA06fgQ^kr3xi#lO!RhY=!(j<@2G$#dSXIY1;NX5o1 zb=2hvR|5sb1p;P7hB}1TMo4DDu8$Dai$0!=pQAgLcHyO}k*7@)Np_&J*?KE(yAY#4 z!ZM=eCiNOzC0Dt}>HZV-ktgr^i1oV&3azH95IIlJb^0l6sJ{GIf0A4#SPY3)Ln3jx zLm4wydL(sWqAq%?siNA=OSU&Ju~hdU=2fX~T{N^rI2VA*?xW$NiiTNFr!1RlOX}C7 zN^(%0jUMyi`5qi4=KOP)NQ1eyXQaOQVVfC}Qp(A>v&=#^xb4qIHMy9L_e3vNyF)}bP5X^%P%9Z2E&+X8QAWPdSgpi33l^O^N6Tg3=2Ux4?KdJYc7!) zR)&;r(Qy-fmxL@PG-)gy#M5D)bps*Y_A3>`hR>zaUe0`lMb&iOVOi3>3Wr?8eR6Aa zuElRf%yj)=u{#zI`&NX;S2Hi}Wpdai2QyDlRo5t1=k_XZ)>e(LF4JkSX)nKN>oA_CHiNJOP>A8Z8^g&m`0u!j;ex25oV7GhqpL>Hh8itc^ugxG8Hg4F zTV%wOUFqsV_)5k_e^{wKni9;de`rAKF>4CM%h^Yq+h*F|vNLNar1l7Iva@=aGJfr$ z&oBfT@H@2DeA~`7X2jr_9k!>X(&=0Ta;L-9)aw$I)*Ks9m6p9!%9?RN!l*R}h9hp1 z_Ru>TjSys|o+b&p7m3zL_vNQ7oUj4>gC*i&KWo>4l9|kuPSK{$V#Cobyw0GJk|m}# zfJL~=%njtX**lrB{ zt@+e%hS$=4>Q#)4y!DjorPWLI>&k9h*}@$m%P@t0MTj$O`3qoADmxsudo${579tVX zUTuTC6?xTx)Z!SiwRGUc)$3`(TgEVFbPkw9P*w=3D=Vw{Q2fb&xb{R);|e=DX?oQW zBy-ldzxKXWG&N;M4mneEldX((yBzv0h@4x4j`P8P=c4C)Gd1bj3MPD41n!s#q-ECCONJ}vjRz}+~TZKdbvruh6^8r!;bMdvzMd}S{Ip9cLch*X2{>R{Hn zLAz1AD5{v|X+|e5;SMI{AHUbbWCOx^c!kxI8+UlELOy2}6FAYv;&AJ&?V)J39f@d& z4px^Xs1iRKwHld+>@s?z+zmiML{s`Pau$lBMhrs0@Xl^a(H#chpF`=VTrrY>0!72I zIsBFPC*xz!yvsVGm6ziZI=>);nW+(QXhs^B=NSANHUwg7K>Og0C5i#@5E0vF%x!4l$H`NpnzP zH;{4vu>4hUTV0$R;Gu?`&EI|lj1QQ~a>`m`rbSG(au#ZO1)cVmg2Zx!Ad2rCDUA#( ziy_6hGA@dz>Sk=)@-j!ysyGcNF&qF0m49Qci^3FElhL^CsXYO(^LO_7v#8=IZWgt{ zv#u?~qj5L{sc|@DH+~XffDgbO2#-EX;ScyUg7pd@uMxB9VZjSO99Hf=*8l)&uKBRF zMK0P)afy@5vh#~S9wVgLyP~yi_Cs9Gkc!>}lR?}!Hi@NlG)~9GjiRg=Pbb>TMjiuL z@E>s)vBn2DnPx@B?C`tAvaGBux62RF`<=sg`=<>4IoR7dJ2-l~{UM?oTe8Y$ny4CF z{k<3&&jg#|iQ+h;@D@9`p!hQ0G#TQa2@JT8yFkQl~UNZYvb5F|8nNpnnB4^e9vT-TUkU4%oRY zeola%Yjw`?Ru3K7@Q^YIQIx}G#0^ey!5$XiGzx!#ZTZH(CFgO04<3n#S9udBC@o&Z z*d%5ZOt;(Zo0}V5-5e*E?S9-#+rn1UMlXgI!84eQhKtk2@#(QHN}ask+0~9u^7SN+ zXCwkzVMXeTbSw{{hOQ#%`aSU>KZ9ANHKr9LOcp>L=7tCBHBS0KDH8J!(E3%kfV=8&-Ls4_79K{I zk^-bW{?GdQ=Eid~{?EqxMrZvo{?C0pxZLkJ|A(3KiD+vt7!TuH89>l+%eyj7P~n~I zN47Q7qYERrRpC(#XP~`H8`n}Y09CC2PRCyVotG~+Hy+pjeLPk~Nt6vPW$=7=dt9*>nyJ(dgu|GH9&$0S*!`tk5n=Ac*Td?Te4m%9H3A%awgf6v2 zW288^OycPnB^*wDY#3jLJrGhSHvtGWLUQIVxG8ED&M?Lhj|vv@kkfr)DJb=z*KRmT zgW-j@GKewMp;SDGb}&-pO4vhyKaPrIOcKF9XZr)sbyIUsMZw8Q}52aln*f}y1Nz6w8Ocry4Y8d({EXvcNw zRGr6WtiyycKH@MN!DToZOuINVbOY12|73q>@6En9>M!0$Isbq8qO+Cb|C{TZkNp2Wo+s?JV6MBI^GXIy zym@71hy9IMwekAztCDx%%?K|Ya3$^}9*6LcU<)4wJw3rP%^F<(wZ-LEo;-o$?7#1w ztgM`$pQ9uKn+Y1Hf(SLgxLv74*_Dl;x_v$z)m*og>H)arPB3SESX zXu;zI*Mu&qU)Uznco?kE9T3(3E#p^VR51e283|jteN+U)E(-gI60NK?x+VE{n>Aok zhB5m&PKNyk2jVb}uMoE)N+$4mwB^YglW4_6Q%ZkI8_=KF8BFHi-oLrQ;C*L)^y?J1 zbyb?}LWeLr*+gqwXB*`GAQ>lN8Z_60FZO5@B)!ldvY;PQ1HubW%_(n>PwGK5QGl$h zXj{23^`zu^Mie6VPO+P&>}CKHgw`e0U~$V^4AXKQ4C67~S>e+MpAZ*ByE$rYG7w;{ z&?Lw&wIm#@<0K$m3Xn>WuD}MVg*)tc8=K$3D_RSi-uK@6cf5|Dz$^heQXB||I)Z7r zp=lsd1kcc^yhB6-@UQ)FNZ&%P!V=3xj0~qE?ZsnmS?Pxt7lFJ}M$<^3K4Ci-7i85& zKtCf?6p>7w#F+VfW_y_w#%e#G+5V?7^b4>YMC0kCeobB-AZwLBtg^&1t7X*zvU9QC ze1%Cb!koii8edG#aqvRT=rer7@T0)`E!QcAE^I=>{GduUhQ<#^M-6dk&2mS*-Z^RD zfB&b^Ja^!%5Hg_bz6lw=c>s`|t1c8|HGwiAL$MP2M|?Nx!&v|s>_#!LoZCMKx92)! z+WRWNG{~|_0wx zCvn>Iht(&aB@E622s^oZp^i(WszPuI_<(ILu z^UhOLjTTNzejp;aY@e%Cud{Q>rnmXs%;Q+fV(*K%|He%VYm)K<*>P5zds^;pAm*~E z&9&T(-OE768L<}+rS3qEiyB+zK)8k|9*v=C8piF+Dxy2r3O$f#LF)oFF{9S%&J|S; zKV`va;Jk=w zcz`@w*By4cyPIhhVQ9NjhnTx`Y@x?iHxdutPm(y9Nk9M9@ZBXaPFJh-dFfZfM(+kt zWBCR|XWn#tnfU!cMJY`^x=V{I_W*-x4u=eu5MEYQ*ELj8viV)3$kS^-b`*aeIx$Mm$W`2(gL776| z>OdO9o?0K)*YmGBO;)V@7pMv&;#&bF3)9UPpV=7!%^@aKz(MmejNpKlVRH$;N8pAtD z-X!**KTqXgYgSr61>@o{)MH%t&a&=?^%QqkhbP@TH@fEzjWwhv`a8F#CZCoMS3P=? z>A4D*H#x>CF!K!NmOz$a@ag#)tjK` zHBWLvwhE$C5}@;Yf1AALDv}M#l+w+ac|m8b_}fOaC=sKYY6li_z@5*Hg#aZux>qPf zIgDT}Z-q5$)579dsAQEnEL`U`PcAHQVwBpZB|kH+2-?)41RW*L+04sAZW!E#G@G8` zO9;F7ym>6+nMuL$CB)*dMikE;bM*=XXx9hs6uI=^pP9UXjow~-6$_GQq85nO$jfra z-<{)w`IfqN3?+LFza!qbh~tKOcTDlo8(lwXh#d{P{F?+#ZYbC{Nas)?b3-=cpLB=u zWqV`2^T+o3%Xa7cwm%MAT5Svi*y^-8FWMRtxCLqWlkyJk0jG8$Rchvsf%lxnUrXoZFDvnz1Oz6T2_&BxECXQN781KV?W26TLfxa|Q}l=S4^jt%6HM?(YPt z7@6P=_r*Vf^KpI99oTEc~&X%`u3onRn@G`x&NITbO19PB~l5#IN5z3SD~ z^4DG#?BvoJBrAAcGW5GdD#-$eem59aiUP~s{9W66#lE@RUCY&)L1jb23c4Ugec{`j z35S|{r!2sDg_&@WUxp3{M(82Q=Lp#a|D`VML_l`rq2;B3PcIFIfYmrLYfPHx#Uz> z;s^ww1ZMo)Z0Bxwjr?%reKre>U71NqS`)C`E&FBa50OCoXwT)rolCpB_uEeqU-O)_-^pemGVtIQQ zjzSen4ug63QE>5v{ff6h_85xXihpsD8%1&`9`+bWT4avFeGKhZqrm6sIpl?2k zW|~j;!P-h~EA!jn{={=z0wWmRs-OCfLt6BGqyFDlpU4JN5%;jj%yh4hYeuf{y0V2h zH6tP-B$QbOxC_k)zcma0C7I!?LVL;Mnvu+>gX@~k9W$abrNa2&?<5Sa14;smK~jf2 z9fXLtS~g!n~uK%C>j zPsp6iE|PdeE=htZi0N44PKP&PN}5^pMid5UMtL{n`G#J3Gp`X*{k;erDOlU+mVW#` zCQ!=4lQ;qXR3Z!45P-joC|fKo){x}BjId{}1CDyHhgKf#1f}~Pi0NTu3ChSAqXNNt zKr^0u=hX^0+-Q!oudv^0Wy^d9&c;LD(Y>mLAMh#ZKK02D_-ta(v>k)@Ttk=eN5@EA zf>Wc@x61RR!giG-U2{2|77%})ORtF@Lw#!zQSCXP+^1c_u%^^>7o^09Cc|4H z&S49HC4>4sHy%boa%ri2-{3320tsGlPSSng^XTv|CP5;g6pGW4XW^mLdx~Clia*tG zo|<`G6yTwSy6l@IoRC2LyT=3Z&3BWq+Pq5TACw{!pQSP1y9?`fhtJX=GFHSC$-$#z z%UQf1UBCA^ll;tHPs1VlC0&Pb_`!%9upp9pP!8+x8#4#!X}-=$NWYx z#=|f*0@|fO+zGJnZ0I;ry8Pvfq%3`bBG`0!S)phowojUqA&cUuB}*@1_<1D>yzH1r zA=QWrE&1X120`yCosQ1L&PIkg@eX4Rstr$c*wC}N`0QJrQ}Wx)&a$e)#jc%dIy1EE zqg^EK>z+z$aFoXAvC;-B1I)5jJQ_Ude82hfh32Bp0cf;UR&NULU!MfTCiq@m`k5pC z-51#yAMnBBzMq)`{e6+W{NV`@U+giMPIlie^@J?WIf%S&$mdut(LilNqiH%JYb)k* z#G*^zc}?N{&&Eb0&Ug0i>sh<6E|Wl+X;<8k(0`hi9tK%(`v?~L#=(&laCKKB<-%J_OfGc zll74EAfKukL)pFG%`8FZbXhe_4eBYn;7fDz2AkM>P1L7u>&`lnc`;T%*2-2^wp6&o zw7FvB`cOXTa*=RTj7$wkAsi40vg?F0v_Oi!9yfyk#(=4gY1Am02q=U)MLH;Q+}x3f zFJzIQMUL)Tg2*ytkA*~uI-Bn@w!VehFcyQ%f$k;oa0oxn{L6EJ2Z4I#cQL*LkBBQu zv59|KFG7&I?RGxE^m?3*^JV3i;LzCsFx3=^0+#eJ(y66 zaKY-*x;h+)vm+b?kTYcAJT2BBE-Yy09C^UOla17N8s{3!WpD~~jVT;LqU#inHX}dP zE~Vpuvs@Q4g$Aaj;OT%cm`8L8e;O&D#Jy8lT(A%F7^Q2Fz=MV{-ModG!6zRMOwg9f z%CWMdW$&oy>O_?!9+X{r)?1dDj3SutPb(`xsa4JLcA%i%OvmfI@H%iZ?S_8T>a4et z7n>_9cq5sIWvgnGkbMDw^5&0KMeiAJMmV-*?f6OJ-@;!Z94hH`c?U;=u@7%qD*K9f zBY4bKy~-QS>hf>jy!Gp?tiT|r8kV|oJPiCOH=8e8Htd+5adZ zcrxOb<9HgFRL3ptIz*B?5%()b)7OpIoVHhsL}^xVIcPSwY(a>WtCe{ASvV;0@=UYXc{&mhk1H#}u1F@sDw6tf z5W{+>^S=Uc!rsTj=_oLbk82o7B<1tfXf@DBBkSfZWu|E~L_(5&m?E4h>)%p#9G;{F z9j5^V17c`tWyS9SUrXP_{h$im9V*FyWe19L7Q$_GbQ1XepV9DeL=%vPzgBj33T0;K z;rky$ScSkatJ*yl`G>pNMD~rAWa6Asa+ajSC;f>r9UQMun1)yj0Z<+>KtMksRif6d zqFK{5DO_+w1=WRsQf|jW$Kp4fXB8?Dmo#7mY>J%fXA!ms=M8;W zBH%(H#>m38NP@h!S=08d&aP<;1Du2yegPV|5w6g9J{9-E5^kvk&X$TGVZ2)*9E9-Q z!UpCDlC%3Hoos+hPv$9E{eYyGNx3XjJAFR%(^@7ZiXGnu_l z9IZaH?u+NZJK@JK(-@A=m~>Vjbbma|M(las3YU0-=z4oaia#=o*zwNk>CZ%7P(&9?v_@zTeC}`;L;vR(LZB{(o`VyG1M#K z53nnaqYACoiZT~})oNmfzCbLnw?}9Dr{3h##7M!0DSn;0iS1WLdSHk0X?;DfetK(a zpCq6yD4Qi|cd0$<^w*i8zmDk7l?b^~f21aMIa(Z=XPO%j5pHN_#>;CUS}1)K-|l$0 zG2~ekRt#i2Vr3XV8Lwm5ne)W;FuMH#l)yCwtyW6Dv&KZdYBbg?T*51&_-9tpXL;S& z8XNPQcYeRmp1C`CDi``kYyxFgk;4vl9gm%HtslLcCJDFQV$!`8wd!EMiUEABnO9F& zF{(pvGNj7F#@T9R*WjcVV@3qZLDvs zuZbAb8#3Yo?6VXkU_{3wOcIq2f_@gDL(7PZ#49qRKEbpT)3g;|v;Z?Lkb_aypcIG{ zQ1c(K(Q9$6#mOB9ECiS=aKQjmqNhARBTr$7-?f~`e2vio0e%8gh!X%@UdqVQ0$v0@ zDjHIpYkqZ#4|S7EP}B;pLm~32(3@0Bj6fty#zU2#C|jaL+5XX?C%E#@IcKZ#QmM%5+>HTzsq4xYHENGL z6h_YgDag9~2z>@q^q8&PC702~IueAM_Ta3Sgt5vBrb57=(nYV`kAoDumn=)JM}Pz; zG6N$U1@|>J7n?pbzGx8avy3}s zn;%EvC7fincGmFS8xw*H6gdlK{p z*ss?Wnbe7yPt$e~wXa8*^tLs@q_=G?MQ%$ZscqAjmg|d%{RG4Ddo5@mtEa8W6wZBs zjkJZD+TnH3!l7@_e>Yp0VY`(LlT%R(7G&_r8%##S|J32GgEMCZiXz3!1TDlD_1?PP zXiZ}Aa}#Rf=6MJsi166D8;@@jOe@d+tp`8<9XHD@m3y8?Rh5v(aN7l;G!kNC32WSIU9Z=J1Tfrj|hX#V>O z()bAY2QHT>8F9muv!UQqFBlV>!l2m0kRSEs7AKnUR{km)h`Z=`Lm{-sw{lMwrI-&# z4MHm5^ycQq^9e*RPA=O+UL72sn1IQ5(Gc}lYEy^3M34+6{`GIrfaYH& z0pEzAXLObg@hTK1o7y1i>`o^JdL&FRPQ3u`9}#QpoU(({27A48dT_e7^7Fyj zPe82Tu+`SGiW`a=75!x#4&#Upm+M z3p0Ly7z5mk^Yr1uyjP2WO=XhuLC*qE@%}5Ijrzgt$PMHUEhPivt z-rWQ47!UbfP&kpGmK*mCDF0vSRXCQ~Wv(82@?=H-ds_tR<#O*MgPHv)T>o^)2rna$ zH5cupYW=@h@8s71=8MPme;>~iG+}{|$367P z6rk7KFzzx6Qb)o~5?~OhYaUHhdyfLb6BY%R{3yH%BFIJfNglrbFF1T3!5N$mW@8Q# z$`>fxgXrMxoqj|^9a_6~i9p%=-BS>cl61wp3@2^+F9(0c>;9Uw>A&*D;IfVXk$=k1UChetnpqyCrXS%vFAGogG`jw_S# zT(Us<`tNk$rM3R?pU3roAJ0Gk+5UEAHW8qX0Pt;_+cVnVvg>fPP0j|mz)-jK3N_Yv zy4q$`j6kUJCR=%z0uS1DnMfQyY9(>k(TfRf9HQ%%xQbHVny}8a=u!&EDXZ#AV?&BL z&}HU-(qyZ6TwV15boLoI>nYjQ;b#(D2A_UFbzGyBeE87+?&;@r;BRcb*lsk*_5ugF zw+C;3+-4`)=(99ht7rtx`hg1D_y)`T_FGeFYod4}uH^D;Pdo+(aMvWjVdA|@I+Xg? zaoEpeJLMX@EDD!?lg(TC4wZ=P3#(e_d`S^oTLSVHLf%rDVssq~%^gMx7juq3TarRo zX7v%QLyM*-qB7FWn=z(Yi)8Iil@ZgwxFGuBAJBeD{j2X|ys!5LByT+5!&mP2t3Jz- zXVpCc8c$x}?PPb7L_!8tIl*0z>2DdMM$!0B4U!=U+9V(@P%xl>8{eeuZ_@2=Qd(KU zdr+r+r23}TBCJhXbZTu{U0B0Vc61kpUZE}rsl@&;7j&Wz1Iw?tr(6cUq)GcX49l#E zbo>APKmRW~-QRt8a&Y!nrdY;NE?rW}20n;lq^w81+$5Y#Cp=@4G@JC4*`y!vfZ1zg zrNw`d*GY>pDWKHdge*fG=N^8hC_-KzUnm_vwsD5bob6=GobB>uu0iUtQwbo&@Fz`` zHjt1fqXQ-ejH~CPDzG1r2D`5^;qgOckdTcd>IG{In9>!DU3k3|YZIQQ%&TbqtTgHXv@Byy1MM~FW#gXd!3)6xTu1_2Ri*(aV zLlF!r@kAJ_L}XW>AJ^oacbsV$;awxo6VKFjQRx}RU949$qWMs>Wpmte$jr+-APl^?DhZ zT5!qP2hil4=?D~@lAxFGt(vcxm~#2TE0_W&oybZ~LJ_DrP*E(YC>pVaB4`t%Npu6g=|@v??pc>6^a&5E zhz4{&;w7_Ww+6lh@>egNbfduq=QL=i$=35|X@6_b2>VX}2oR8< zP5roy230+-^$*71M*RF#9puxXKzi|ey*QCRF}Kh;rCJ`s37nQs>XcB`WE9J&=GorN zR7Fk+Ac>&WJrz+Trot(KhemBFL}=&mcA?<=rIlZgPc8j#7WivE1#pG^XY=Lrm$v@* zW#{Fi{`Wqfhob+j8BY+-_5**Ma+^`wi^qXXvnKvf-iu|&VLP%R97@^jkqB9n^Uk|! zzClBRUzl#CQ~{Xp&^$w;SdN%KNGZ&7h+&?&D>y2rfy4W#u;MRPvk&oK{n4<=vfVS& zdjIpEFsMoV-zf^}BC>;icJ%mk7@Iag$yR$92?_tA8GP!U2y5Xfl?SjJ66Qv>D}kW6 zN-+neHo>UX5WO|kF;xyN$4y7UywH76*eI~mAE)e+1X_Gq4%bonaTuxt} zY|jgr7}$Q0_`{Y67eK@NH1vU6v^V&_6d53GDv+K^{75DT@(qVwj7NxT z4cWaKXKy@(VZtIAVbrN$6eoD+wX^a4n-KoZ!2<8L*5OlAHpqu(H@9Uq_s5OG<~Dv5 zt>J$fqp+9M1$x8kWn=5bB0z7x+%lTm{Qh}Opi{hWSQF?i3)5SjV${?C`lW?~moJJZ z&cbxi52vF!q3*126eDL2usd6u#dBxEP4r)vti5%EK-QlxM%LbzK_Kfd>yq`S1)whN zrVd@3&AsS6uS?e(UlQyWW^ z@1L9;ooq9%6sGtk&%Q~YL5t*5;qiR2G72*1{p3(WU9A2ihHa^_2ID2WnS`K8;-Mc7 zojUGWRjYO6KOX9MPUp`*RLXx_>n~r}^56RA)}#D)AI}4j{|JQI5#vjaoX&Xkx(CVv zq2Ei7mIWPaO1LBvPHt|PjT8dXrY}M);L%Pt@Y`?IQ5FiL?WiJ+O!bw8%^_ouXb%2z z1!lk!JaFi5zuA!)@;VKb0s)<>99a)4C(=SnasGnpsj-^Uy%6yWrIjR)3h7#LNIpuf zh8(a~V>%uO33`0|kgKYEYgP=m2dfXm^`xk^%#vDEVwKSsAQs&TQq2MW$@muXu~FQL zQyrDhUmzWiXa*~1&dQ0PmSB1Mff`FJ*RWb*G;6qvn=^lm(CgP$@e8GhSo|6zyq*6Whlt!TK`7WEc z7}zTy3n8#%z?c-GeUQ#NPDw)9>#uSCgr`75s^_Ui=Aa!j`GdsaqNyM1kerr>LZ`Sn zGCtI!M58}NOIMr*#qgEK6MciJ50Vt)MbQad{3%MARHhfTC%JGNDP4DN)uU;)(a1q@ zC>+o$8QZPQy&`k)Xz*GE6!O6GV^m8Nc9IW;*BS{mYLHUC`ONX2vm^-?_o2zWL1hi^ zH82(mMM*01roO)`MY2#6$EZxBh8tN38cdi8Y=aI`97T=ni1V588!9+uj}9Eav!~Aa zPbRmV_XJpR{_AXP=FWeco12g4zx#L|!1>S6{}(1qb+Kn7>uP3KoHL_pDXX@+%H&Qh zR4>Ja^muzs&-n6Ld8F!{7nR8x5pPc23w3%_D`0cS{z?LeADk(y>a780-L+D`@Ek*GuI+S+$cWz_J^ttJ&7Uzlsk{ zy*YVDRW@ieq%N>#TU;>q6Cd7@+GJaUOvkp_j&<){zdw)jWtB8M2l-6tCs4hcSE7Ko zqo&gCsPYnk*1Mo1y|-+*FxvmJZi-o;LrqiJ&qKn1&zlK%To;eLt1FoD& zD>de)bj%FR$j8ugLpCmtvVDLlIt{5(=cKRYG?)yuGtcCwOxma1SayM+uq-0-vRb1nwc>B+Y35$yb(XXNm8Oc*-YfmFtvQZSErra?Hsp)+8rLi_@Xy!!cjKfIY(T0S!%{n&vHvilhWx% zBM+>uH{9g&Y|s9-eZ5kL2BX#0?U{kni`OlEx0eudF}%J zi}9~zPr>h-3v#(%2F(WCUB(!QpT9;UAHHvcfRhB=@JBZ*|E>I3QYPA5OI`hJ2b0+k zAgUuJ=+lQYkkl)sd~rkiGWay+mSaawg*VylgLWE>pgRm--ED7dUTk&x!RGh=`u8th z^tOVF_3lOQ`9^p1;^pR6@8xD^{bl#%`g&Uc=v~H74xe{ghtHogHKg!@cmDyLl`uDSZb6TkkX|7_9so!0;OtzSR1{{Ef#mc3|x+r%2*)hhS&T*H-daFBF@ z=4l#oK|v*>_dWW*hN|klq09=|pJAl`c}(Lim;D`Qr&7`WODiWHpF4l*=>I4o@!jeF zHnuv?ZT;WI`twKq-+ep}MgMm;Na7oj`r1nR!2^tOfk0*8Hm=C!Go-I%M*S>@7x24% z?0{zstC4AodmmVNwj3$HBvcD$8B-8;?h3+r@{&1362THg3Xv=^dL9yeSnBZRn2|gR z@@F-W!tC7YsITJba9hmo4PHsfgvpv7JIwA_iqazMrOa%`6SA*SoF0mv$w?1>-^7@a zWs;GqNBzpPT?@0c{VOTUgN5tEfEB*H<1T zCtOzb4$^lbJB%U45R3w+s0{uP$pm1hOgOt!whdg5B9IynP9oEYW$V|@>_c~+c;}7c zPDPYR^Cf`6?B}hDfEtj`skkz$M_E~8@vzTe1rQ7Khbd(V>GJqPG9yoLE8kv;TqDYU zI}!2USa0ZuqjV`?|1eD`?xCJE3KKYW1ChQ2;hHF+n#Wz`ffh-$#)M0WKWq=;o7N<5 zPm*|w(F_N$dGRbTJV6vsLeqYd4Zg+6uXQs3F5Ob|BFc-IvE!!I8R9oqB_ZG%#qtDM z=Ia zDso?YuygVpH5658#b+YM9~+6NLzFNPx`SD{iKI?j-?56gZD{p)moa4vy40*lB5Qk5 zNLh*J;_q;Jye$6CajvwB=vuMrm8r@crmj1QTU%(3Q${!&cHP}vk4XDA7DsBs^kbWS zY@3{`C9@HVO6%&14(vI%<)VPOyiP7^uBC;vS}-kEMRgS_O(M$FQuY^XxOshJ4iX)0 z)o>WJs)J#CD22kQ)+#{Lsx?{pz`>*`*rH^iF)&+}tR1|XJA6kNTkkRof2C;waM>P(Q01F8Zohn1@zMcaB%o07 zKAaf796}pK!!MGZK1A>IVRDMz)|(MVBn-+2Vqu3$TT=;G zDU+4Oo6vTE?1HlcOXd{rLv*|i@a*!eh7kHp388Zc>N6faG=09x^u-m3;&8FK{{7av zL-sd2&I|g2(-gCj87M*R6eYCa0IFb{%tNMcwi#pu7fSucN!fy^oZZQ$tGBFWY*h1X zP7-|Omxs}&rg)$xK01wpPh%cHwj02>!Z#*onYGG3T=d9)mQhyQt!B z`AWYxZ{=F|Ke#pZj_!YKtiRaG`TuS{f4u*HFV92K|Nlset4Sblv~$1T5}k-7N`8%B zs6y!SEG*Mgq2YEZL(DQDfbjD>U5TDvT!f$KcDed#tDuiHg|pREi|rdP+DiL;G)Arp zyElb;qWiug%(iSoj9bN-*3z$vYx5u=;0Iu?39&dMd==@Cb6i-xTq)dRM^x`YEMYhP|O14pI0Z2q;$cb`6POk_15;(;T$T zq@Yja=y|_{)7*6ETKo);JeTN{2Lr>D^B3$U;2UU)S7%{ovM`$B2{=o$V5{!;DCVE0 zx5t^1pJg&+bzfEbx1l3>;gunSLX}cVMuNzB+wdq2cw*-@vS_CI;oNf|$Y@&E1hq&L zM6Vt7KBKG{_1KF|`WcSId%P*+)D4iVYcC+px0#e2xj?g-LXOv09+x`Io_l9C(omHb z)d4lGA{+hLfeuWra~0gyVmepa~7IGhsT*(YceyK@>~Q}fq%?3$Qr_4*?(_D zrOjE;I}WQlg^z$E;U$Ix=&aKm#yE#OuMBs=6W!>)2XPc9$5y|xPKn$hqyFTn8Gh>I z{EW}?vn+l>mZeDHpYlV-xD?9PiPf@WpCpmZ5`FswBe zc$eEKp22xVVZY(ApV6c?4trN3?nb6IaRnXWpz`@*ElL@aaZ-ete-R_S6m;9iBWFJz zDa7$+_*BD>*KrI-^~h}M=RrV970k8S@3ZuF)WuxqV#pOuD^hWZK-PS4g#a;aGJ>Xl z*cUIuRERwyNe=2#xgTJq_X7O1-50P5s%(y;x4})(uw`PI%qDulQ<=n^yw|uwj2jsd zGdQ*g+M`G)P|DuD1g@dd>le=hkXfobrjGQU5t23FwfcrcwpiggB%)bC>&fkSB9{a9 z`4@_jwMdr~Wh+&tUl=h?MiNhfMcQk~!4>B3J6k~_$gSQgiOT~x-vyY8CX7knO?k=S z|Ewt>b2*SEX$XPF!rp-Eqb@*%B%M(rBLDa{hexK>K}Q(rvm>B@qoJRZNiZIx0|7h@ z5Z~2SVUV~{WUMG`ey_m23lF#m3UmD}mws zM+yG-JWb=qq5~F+cc_}x z=d7UZf+?E^9BmG*BeCUiB2s6XX^1k$@k-|wj$?1m&8du1lxGf?oR2l8WqW12*P%S7 zO7f5n*+eD{33Nk)-xbd<=hg7z*tu+m*r?)43CrJ z*=i4K`aN;X%TW^0ryAfYM)OYCDsHSh!|}jp9rT34Ki^JAK@#@h1zY7|Tdh)`vskTe zv;wtKjkFhrxvi_X3SNaf;TH6r;qZ`h0}p|hefU{J5yiRq%zbPUwpG7*J)%TI;Ug=d zT@J)3g42tOp}EquHC!-W#1LSw2qta5D&u}oHU52Arfa!CuVOA|&q!wMY#u z8y5j))vbws-ka&~Mz`#}gc+fE`(}mkpj!9G2M~ZTUv%8GX!vbREXi zG=B*6b!9_ZXR-5=Rg3+Lt=<`_yNbBwXe~NkE)C}oJ=xZ!5?$Y;gHa9tA5AoDe;lDV zYri{_FHlAN*OxC}Y*_w3&tG&N{eSM|c_{urLP`(V8ziv3*DSHGdZsGcU+#);ozsjY zT-96=TN5HncHnXwrlU^QV)6m-D7DniS{x>b68l ztG=J~25zCbvx))f)%Vbq(T5M=IQE!Oc3xA?Nh_()zQwo zG*D?~v^J*Vb!Ghm7{+z`$781OA1)eBzC^xN^R6;oI3Z<|?GAz37@No%6sfo=KMHbb z;Ktg>U7<3S5#|-dd^vlTa!MWNbQWrL8iGbzIK^5jn+h29n_l)eH>l=l2uPh!=20W( z2rlD|Rt9cMea2%t88GN~@n%t><%qT#(|IH>is+mwtVnxEn=yn%)|tK|&9blx>MdzzrOp-C?+i1WiP>M}tY+X>%&5*KT{EV?uT&z< zs{hfq;<`GZ3jNRXmoM!5Ut3#``G4-`d2sq4p$@4T(k&p4ggN1OP`Ba;XxH5Kv`~09 zt+&o{dn*aG<`901WL`74nt>bF5F7#4%xSQrtI;UkYOdv3x(YOE87nT-sJZ*qEFtas z3O14UwW@z!%rFizv{JddXw0Ntvrahn)n`v>NR5jj3)I=A%r0`*vI8v001B%L| zLu2V`uO3rz{6kwsS-B*Qyu!K!q5XVZH21q`Eb*-BDugOZ-ptzg^th2BI{Bi^c2R5? ztXw-eYw(Hp$C(~bjPw(Q!3oFH->tZw5pk*Sx0+<6oj zOp!)5``SB+z#JyZxv(bx%_{%*U;zPei9dq{xI+Hlc(L))mjB_;NBRFgo`)j;-{qhu zR~q6>EZvVGj+WZ%e@lYPf4_tER^J+@EvPxHad&F@wphr$S-$UfY8E$t%PG5i^Y@qD z-YcJ3<-e?aeG#xi{(JGfle7Om?>yRn@8x;e@?SsbVG1F6yGcY{RMD{oLnz~c8oi3r zz>Pea$y?XL<9}Q6MN57wV}ymV|@r3mwa^4hBza|%y)n&X=uSSgg#kc$1y@eoJyDM8K(ec z@?0BtD#>cxZ3%L0?GfDx_sELAQ#XRDIS3hv^GmaYZkfKuvY&Ev*{)z_DK9G-6_R#t z7jpM_2b@H|%rqY>(OdYcsn|K5<#9J1(KB*7yVZ)Omdx+;-p2E@Jm;;sub5m4$5cTw z1tc-Hmbp$3W#zuc)B5tCne9KfGadcusj~k(&-s5p-`sk%|J=*-u+RU?u>ag@tmlrc zKewlUIb4duvl8Zcw4T)H*KPN?tva_En48z;<7{J5i_h&>a8RplSTN}G-pmSjv%D6z zCh&Fjm0zX1wFI=gx8L2#Y}Pm1DQ>v%6}qqbnN|Kvf_^w_Cb%m9->sMHIr*>i{89e9 zkLOP1zdIBF#YW1>v5d3v5{pR$?lu;b2za9pQX&}hXgqW^N$w(i^tCZfnoVlvPInRN za~Zpc4ScGgFP>&zN1wBD)b9?0-c=T>k?>9MQ;W}1iBTTdRpb*6f90%~tzHL7iqrL< zuUO}Kv-V7=c3|y_CKjT9XM38`_^DQ4XUgO1n{k437jBA??=@M&-p}n0_$rt;n2`-_ z!4<5G1PZ#>SF8%;`0z!DI{9CReisOQ?)0yf_y0RD?f8!yoyYtC_wzge{SRl3f1Y-O zBnmJ~lFFpc3D3ph1WC%08AF4nqsHO8l+K^jalcFV}}$!!k6Th&Jq5-8PLc zqBAX+fHaM(Fkb$6>|LqhdtuxbNo|Yc%j(>zF$jhuFCDbS?pjbyeAVjZ`^9{ZexygP z^-co1vFOnaTSpb1uchDW-a0Cs0?7cOsIl_|pt961B_Z1xkKc=(O`+%`A_r+kA}K|NRSC%i$yp<~#$G%l{zx zzu2_o|IPJB|L^;G9*q3YGg)Br(D(v_X7qy{PIk9^F9}f#;@Z45>cm-W1{I zx-DKQK#nP#-)LXl|1?H7Kved~_rlklGgd9}9e?66M(BwF^9Paknh$eZdt-!+;-Afy zupe7qv#YBtON`wFNE6$v;}G7|JPuHpSY#dt`oZo_=5e5obuyM#{b|G&PKB&?79=Bq z@YdHfmnE~ez{IIJ!^Mt~vuL&4$qZWBSpb5j}|fJDK?#fo7Pqlc4} zB(paDhRtMlvV)2i+lMmaID)N7kzD0L4&>*=f1@8zvMUSb7Z+kh{o`yn#1yPX?9Zas zTbVH^TM{aX5GEW_e{3|G`4eZ2+0+oEbSw%*xkS0@LsnKlCuzkA+fC~nrE2By=BSw~ zW7f_gOm6})C7Z|L+&i-R7^0JX5wkZ zQI=7T$q4UqMU>yz*cCKqf0?C0Q9;u#s;oKrlc1`!%_xmoMoPKvG;zY-)}F#vv8IL8 z%M+3f(Nn&k(%vfSId{KY$I_-_Q-CUWx4O_ETN|}t|4dZ z|02r=IS&7=NnV}Vas$P%5MR+WvChw)KWKq7lqv3`ty!ie_dJWi!Ffyk?5;f1ErGCf@{Db zr4*2VE%~Qh`uW|GehqPEX_CzSC@gtTOVC$p%ADn?D>Z?oN?ZA*O;(T35}!KuUy!@d zm@&5lXx06Xm$~?_8=H^u-|y#nF!tXcMZOW%^?O&yYM|Xjx;v$q6k+U7Li7To-OvZf zG?&qx;{zr<&&scZNLm)XXi31yZAxT=5np90aQ^K$rn?K`oA}Gl1!JZKS8b|jVOb^= zKT_A2Wl1@9y*BovYPS!fiO1*_<$b{H$kB8JXlQ21U8tJJ4y2D*z(CfC(jkQ+!=P&6 z1(}~G3=_(r_n2@)gUur(Wcw`Tp#cmD^Y+c~PG|%;oCKFa;t^O<=xeb(M4X+@Mt+vp zMwA<-h=xYdQi^uV+~nYPBL+k^SLvkW>8Pd63jNx)bukPn?EvIeK9Yv2LdetPGNV2H zqrRtN5^C9C7){L9n%TFDVeC&{Jh#8g?r5g+9ezd(J4CHQ*@x~I09d^snY6eMCtigS z>7e`w^P#er0J1FqBVeAHKGr^ z^gc^jufoPlzCqJdka?=Fs z$Feqg#&@zdDi^Ng_jrJZ{a6!!WaGPHr_J{7m@{&TERVguRhn16v5$P%Y2g3)T}gTe zfe{*@pK|(l5YHo zOKtqhBW}q!t*z@0JKf!_KaX6WacUw!6XAO5Rd(}-kJqkLU#VXU0;T)*`~Db4CVN3u zQwEltSR$d-)m&&y9?Y(3cG=8`sA*~saDT$XzB@3h33o<_1Yr&vFzwt~ojmcE)v7<8 z#MtO8t5bTs>&e!DJ)Cg*gO;+DP=g3Tw#mrYqDY!?xs%#6_lm_MSReN5#zp!gb_kMx zGL3dpo)L!s79aGBphEYl(E$8B45R5Mew8^j2YH|b@QCM&V{!K%zya5^6$l0JN`hQU zm?RXIz7)H=LC>G2REmD2Dz_tEq(Olrop0PG*ybp!vI zn$-YRxU=BZ8Dywx4a+Mf_1BoYgQmD}UpRmIl3WCpMaU92o`|kdUhju#5?uO8AEQbS zf`|d-MdAt(Q_K#@ew>8Yu;E@(x{SVw8w(^)D?9sedfHa#8ceI8o{FQOZmB#f8t?z9 zCI9Qf&Kyd>iu)fM8=D>ShaT&<8!IWN9cIwexJaPffvW8gkcC3|Z>IW?@j&jSf zqvNI)U%vE6CL@)YDSXXSn9%hNwBqj37B^94wB$1nZNiRA`Oh;dKuz5^?&AhOCCQ*W z?O$SS%UaVlj)!6IRzqVpVPsdw@ZVU%IE2mvBCJOgtVMtIfD?6&D`6A`QB8jJeQKYX z=K=R0aknOrSh57PY&k6gPA`e0`XI^DcMewH2Wb-yvG0WXsV0uOLV^`biprhMMt}ef&9B!Ri;em{+TVKfj`@1| zW6sH1x~rZp*@C2%1|txQc2RMXK;!?>@iw+v>kYG9DfT+uVB+g2l<_DEE4UR3F4U(= zyawD#O(YjNObNT5Je7_;Rb}azCe3Xg$Frb#4(T()&`y68(&gmcIJyWgr-{@L&Wa}A z+0b2Ju4KG3F-bST3yt=xTax3Dj`9g|JQzpD(s6lqDwbzq<@!stYLc`CJkKiNZ z69oflDN~{%zIkS?W=r#MTM;Cx&wdJqBdMrCRAZaTsQ?0KVA zbjaIH0u)zx0B`~mVU}Fh7a=RVw;AaCdREl{L{Lv-)23+IExhk!421udTgYe1aE6;j z#0YuGgLnsm^ zY?OP3;V2YN$P?UrR|Tr}-kxGv@`N4-y{5NvfZGh+iQC-S4B6_89{q2AK z)~_Ev^gn!P{k{2J>rZ%dDZU93GdyF{01q9CC7L;OMo-S}XCAgi+eTx&(^)9ETvV;3 zZ0*_9KR{JL?+W6gkfeO+`Vxm#WKk^=q`-w5BcjC%Q%Ri5=e=IDVb zri0H29n@M`AuU!h9(aH@y5x?^h0i)WJ+f`hHJU)-l|aG*|h)w literal 0 HcmV?d00001 diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt new file mode 100644 index 000000000..4b79179ff --- /dev/null +++ b/helm/templates/NOTES.txt @@ -0,0 +1,24 @@ +Thank you for installing {{ .Chart.Name }} {{ .Chart.AppVersion }}. + +Release name : {{ .Release.Name }} +Namespace : {{ .Release.Namespace }} + +To access nao locally, run: + + kubectl port-forward --namespace {{ .Release.Namespace }} \ + svc/{{ include "nao.fullname" . }} 5005:{{ .Values.service.port }} + +Then open http://127.0.0.1:5005 in your browser. + +Context mode: {{ .Values.config.contextSource }} +{{- if eq .Values.config.contextSource "local" }} + → Make sure a PVC containing a valid nao project is mounted at {{ .Values.config.contextPath }}. +{{- else if eq .Values.config.contextSource "git" }} + → The project will be cloned from {{ .Values.config.contextGitUrl }} (branch: {{ .Values.config.contextGitBranch }}). +{{- else if eq .Values.config.contextSource "api" }} + → Deploy projects via: nao deploy --url http:// +{{- end }} + +To check the release status: + helm status {{ .Release.Name }} --namespace {{ .Release.Namespace }} + helm get all {{ .Release.Name }} --namespace {{ .Release.Namespace }} diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 000000000..2225df8fb --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -0,0 +1,104 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nao.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this. +*/}} +{{- define "nao.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nao.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nao.labels" -}} +helm.sh/chart: {{ include "nao.chart" . }} +{{ include "nao.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Common annotations — applied at metadata.annotations on top-level objects +(Deployment, StatefulSet, etc.), as required by org-wide admission policies. +*/}} +{{- define "nao.annotations" -}} +{{- with .Values.commonAnnotations }} +{{- toYaml . }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nao.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nao.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Service account name +*/}} +{{- define "nao.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nao.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Full image reference (repository:tag) +*/}} +{{- define "nao.image" -}} +{{- $tag := .Values.image.tag | default .Chart.AppVersion -}} +{{- printf "%s:%s" .Values.image.repository $tag -}} +{{- end }} + +{{/* +PostgreSQL host — uses the bitnami subchart service name when postgresql.enabled=true, +otherwise falls back to a user-supplied host embedded in secrets.dbUri. +*/}} +{{- define "nao.postgresqlHost" -}} +{{- if .Values.postgresql.enabled -}} +{{- printf "%s-postgresql" (include "nao.fullname" .) -}} +{{- end -}} +{{- end }} + +{{/* +Build the DB_URI from subchart values when postgresql.enabled=true. +*/}} +{{- define "nao.dbUri" -}} +{{- if .Values.postgresql.enabled -}} +{{- printf "postgres://%s:%s@%s:5432/%s" + .Values.postgresql.auth.username + .Values.postgresql.auth.password + (include "nao.postgresqlHost" .) + .Values.postgresql.auth.database -}} +{{- else -}} +{{- .Values.secrets.dbUri -}} +{{- end -}} +{{- end }} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml new file mode 100644 index 000000000..34aa6505c --- /dev/null +++ b/helm/templates/configmap.yaml @@ -0,0 +1,60 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +data: + SERVER_PORT: {{ .Values.config.serverPort | quote }} + FASTAPI_PORT: {{ .Values.config.fastapiPort | quote }} + NODE_ENV: {{ .Values.config.nodeEnv | quote }} + MODE: "prod" + DOCKER: "1" + BETTER_AUTH_URL: {{ .Values.config.betterAuthUrl | quote }} + NAO_CONTEXT_SOURCE: {{ .Values.config.contextSource | quote }} + {{- if ne .Values.config.contextSource "api" }} + NAO_DEFAULT_PROJECT_PATH: {{ .Values.config.contextPath | quote }} + {{- end }} + {{- if eq .Values.config.contextSource "git" }} + NAO_CONTEXT_GIT_URL: {{ .Values.config.contextGitUrl | quote }} + NAO_CONTEXT_GIT_BRANCH: {{ .Values.config.contextGitBranch | quote }} + {{- end }} + {{- if .Values.config.refreshSchedule }} + NAO_REFRESH_SCHEDULE: {{ .Values.config.refreshSchedule | quote }} + {{- end }} + {{- if .Values.config.posthogKey }} + POSTHOG_KEY: {{ .Values.config.posthogKey | quote }} + POSTHOG_HOST: {{ .Values.config.posthogHost | quote }} + {{- end }} + {{- /* + NAO falls back to the editors' own public PostHog key/host when + POSTHOG_KEY is unset (see apps/shared/src/posthog.ts) — so this needs + to be sent unconditionally, not just when posthogKey is provided, + otherwise analytics tracking silently fires toward eu.i.posthog.com + even with no key configured here, which fails on networks without + direct internet egress (FailedToOpenSocket). + */}} + POSTHOG_DISABLED: {{ .Values.config.posthogDisabled | quote }} + {{- if .Values.config.openaiBaseUrl }} + OPENAI_BASE_URL: {{ .Values.config.openaiBaseUrl | quote }} + {{- end }} + {{- if .Values.config.anthropicBaseUrl }} + ANTHROPIC_BASE_URL: {{ .Values.config.anthropicBaseUrl | quote }} + {{- end }} + {{- if .Values.config.geminiBaseUrl }} + GEMINI_BASE_URL: {{ .Values.config.geminiBaseUrl | quote }} + {{- end }} + {{- if .Values.config.mistralBaseUrl }} + MISTRAL_BASE_URL: {{ .Values.config.mistralBaseUrl | quote }} + {{- end }} + {{- if .Values.config.openrouterBaseUrl }} + OPENROUTER_BASE_URL: {{ .Values.config.openrouterBaseUrl | quote }} + {{- end }} + {{- if .Values.config.ollamaBaseUrl }} + OLLAMA_BASE_URL: {{ .Values.config.ollamaBaseUrl | quote }} + {{- end }} \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 000000000..7fc489570 --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,128 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "nao.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nao.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nao.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if and (eq .Values.config.contextSource "local") .Values.persistence.enabled .Values.config.gitSync.url }} + initContainers: + - name: git-sync + image: {{ .Values.config.gitSync.image | quote }} + command: + - sh + - -c + - | + set -eu + if [ -n "${GIT_TOKEN:-}" ]; then + AUTH_URL=$(echo "${GIT_URL}" | sed "s#https://#https://oauth2:${GIT_TOKEN}@#") + else + AUTH_URL="${GIT_URL}" + fi + if [ -d "{{ .Values.config.contextPath }}/.git" ]; then + echo "Existing checkout found, pulling latest..." + cd "{{ .Values.config.contextPath }}" + git remote set-url origin "${AUTH_URL}" + git fetch --depth 1 origin "${GIT_BRANCH}" + git reset --hard "origin/${GIT_BRANCH}" + else + echo "No checkout found, cloning..." + rm -rf "{{ .Values.config.contextPath }}"/* + git clone --depth 1 --branch "${GIT_BRANCH}" "${AUTH_URL}" "{{ .Values.config.contextPath }}" + fi + env: + - name: GIT_BRANCH + value: {{ .Values.config.gitSync.branch | quote }} + - name: GIT_URL + value: {{ .Values.config.gitSync.url | quote }} + {{- if .Values.secrets.contextGitToken }} + - name: GIT_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "nao.fullname" . }} + key: NAO_CONTEXT_GIT_TOKEN + {{- end }} + volumeMounts: + - name: context + mountPath: {{ .Values.config.contextPath }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: {{ include "nao.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.config.serverPort | int }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "nao.fullname" . }} + - secretRef: + name: {{ include "nao.fullname" . }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + {{- if and (eq .Values.config.contextSource "local") .Values.persistence.enabled }} + - name: context + mountPath: {{ .Values.config.contextPath }} + {{- end }} + {{- if and (eq .Values.config.contextSource "api") .Values.projectsPersistence.enabled }} + - name: projects + mountPath: /app/projects + {{- end }} + volumes: + {{- if and (eq .Values.config.contextSource "local") .Values.persistence.enabled }} + - name: context + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (printf "%s-context" (include "nao.fullname" .)) }} + {{- end }} + {{- if and (eq .Values.config.contextSource "api") .Values.projectsPersistence.enabled }} + - name: projects + persistentVolumeClaim: + claimName: {{ .Values.projectsPersistence.existingClaim | default (printf "%s-projects" (include "nao.fullname" .)) }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml new file mode 100644 index 000000000..609b61b45 --- /dev/null +++ b/helm/templates/hpa.yaml @@ -0,0 +1,37 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "nao.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/templates/pdb.yaml b/helm/templates/pdb.yaml new file mode 100644 index 000000000..83c3e6ee0 --- /dev/null +++ b/helm/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + selector: + matchLabels: + {{- include "nao.selectorLabels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/helm/templates/pvc.yaml b/helm/templates/pvc.yaml new file mode 100644 index 000000000..7d2f4815c --- /dev/null +++ b/helm/templates/pvc.yaml @@ -0,0 +1,45 @@ +{{- if and (eq .Values.config.contextSource "local") .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "nao.fullname" . }}-context + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} +{{- if and (eq .Values.config.contextSource "api") .Values.projectsPersistence.enabled (not .Values.projectsPersistence.existingClaim) }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "nao.fullname" . }}-projects + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with .Values.projectsPersistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - {{ .Values.projectsPersistence.accessMode }} + {{- if .Values.projectsPersistence.storageClass }} + storageClassName: {{ .Values.projectsPersistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.projectsPersistence.size }} +{{- end }} diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml new file mode 100644 index 000000000..c11c34fa1 --- /dev/null +++ b/helm/templates/secret.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +type: Opaque +stringData: + BETTER_AUTH_SECRET: {{ .Values.secrets.betterAuthSecret | quote }} + DB_URI: {{ include "nao.dbUri" . | quote }} + {{- if .Values.secrets.openaiApiKey }} + OPENAI_API_KEY: {{ .Values.secrets.openaiApiKey | quote }} + {{- end }} + {{- if .Values.secrets.anthropicApiKey }} + ANTHROPIC_API_KEY: {{ .Values.secrets.anthropicApiKey | quote }} + {{- end }} + {{- if .Values.secrets.azureApiKey }} + AZURE_API_KEY: {{ .Values.secrets.azureApiKey | quote }} + {{- end }} + {{- if .Values.secrets.azureResourceName }} + AZURE_RESOURCE_NAME: {{ .Values.secrets.azureResourceName | quote }} + {{- end }} + {{- if .Values.secrets.azureOpenaiBaseUrl }} + AZURE_OPENAI_BASE_URL: {{ .Values.secrets.azureOpenaiBaseUrl | quote }} + {{- end }} + {{- if .Values.secrets.azureApiVersion }} + AZURE_API_VERSION: {{ .Values.secrets.azureApiVersion | quote }} + {{- end }} + {{- if .Values.secrets.awsBearerTokenBedrock }} + AWS_BEARER_TOKEN_BEDROCK: {{ .Values.secrets.awsBearerTokenBedrock | quote }} + {{- end }} + {{- if .Values.secrets.awsRegion }} + AWS_REGION: {{ .Values.secrets.awsRegion | quote }} + {{- end }} + {{- if .Values.secrets.awsAccessKeyId }} + AWS_ACCESS_KEY_ID: {{ .Values.secrets.awsAccessKeyId | quote }} + {{- end }} + {{- if .Values.secrets.awsSecretAccessKey }} + AWS_SECRET_ACCESS_KEY: {{ .Values.secrets.awsSecretAccessKey | quote }} + {{- end }} + {{- if .Values.secrets.contextGitToken }} + NAO_CONTEXT_GIT_TOKEN: {{ .Values.secrets.contextGitToken | quote }} + {{- end }} + {{- if .Values.secrets.naoLicense }} + NAO_LICENSE: {{ .Values.secrets.naoLicense | quote }} + {{- end }} + {{- if .Values.secrets.smtpHost }} + SMTP_HOST: {{ .Values.secrets.smtpHost | quote }} + SMTP_PORT: {{ .Values.secrets.smtpPort | quote }} + SMTP_SSL: {{ .Values.secrets.smtpSsl | quote }} + SMTP_MAIL_FROM: {{ .Values.secrets.smtpMailFrom | quote }} + SMTP_PASSWORD: {{ .Values.secrets.smtpPassword | quote }} + {{- end }} + {{- if .Values.secrets.googleClientId }} + GOOGLE_CLIENT_ID: {{ .Values.secrets.googleClientId | quote }} + GOOGLE_CLIENT_SECRET: {{ .Values.secrets.googleClientSecret | quote }} + {{- end }} + {{- if .Values.secrets.googleAuthDomains }} + GOOGLE_AUTH_DOMAINS: {{ .Values.secrets.googleAuthDomains | quote }} + {{- end }} + {{- if .Values.secrets.githubClientId }} + GITHUB_CLIENT_ID: {{ .Values.secrets.githubClientId | quote }} + GITHUB_CLIENT_SECRET: {{ .Values.secrets.githubClientSecret | quote }} + {{- end }} + {{- if .Values.secrets.githubAllowedUsers }} + GITHUB_ALLOWED_USERS: {{ .Values.secrets.githubAllowedUsers | quote }} + {{- end }} + {{- if .Values.secrets.notionApiKey }} + NOTION_API_KEY: {{ .Values.secrets.notionApiKey | quote }} + {{- end }} \ No newline at end of file diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 000000000..4b4b61e5d --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "nao.selectorLabels" . | nindent 4 }} diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..285b97d33 --- /dev/null +++ b/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nao.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..c4a4511db --- /dev/null +++ b/helm/templates/tests/test-connection.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "nao.fullname" . }}-test-connection" + labels: + {{- include "nao.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: curl + image: curlimages/curl:latest + command: + - curl + - --fail + - --silent + - --max-time + - "10" + - "http://{{ include "nao.fullname" . }}:{{ .Values.service.port }}" diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 000000000..2c39c3389 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,314 @@ +# ============================================================================= +# nao Helm Chart — values.yaml (SINGLE FILE, ready for helm install) +# Target namespace: nao +# +# METHOD — automatic git-sync via initContainer (HTTPS + Git access token): +# config.gitSync.url points to the nao/example repo. The initContainer +# clones (or pulls if already cloned) into the PVC `context` on every pod +# startup — no manual action required after that. +# +# Prerequisites before `helm install`/`helm upgrade` : +# 1. Create a Git access token (scope: read_repository) for the repo +# (GitHub, GitLab, etc.), or use a dedicated service account. +# 2. Set secrets.contextGitToken with this token (see below). +# 3. Ensure config.gitSync.url is the HTTPS URL of the repo +# (https://..., NOT git@... — no SSH here; the token is injected +# directly into the HTTPS URL by the initContainer). +# +# If the pod restarts/crashes, the initContainer automatically re-clones/pulls +# on the next startup — no manual copy needed. +# ============================================================================= + +# -- Required because postgresql.image points to bitnamilegacy/postgresql (due to +# Bitnami's October 2025 migration, see comment in the postgresql block below). +# The subchart blocks any image outside its expected "official" registry by default — +# this flag lifts that restriction. +# Safe here: same tag/image, just a different Docker Hub repository. +global: + security: + allowInsecureImages: true + +# -- Override chart name +nameOverride: "" +# -- Override fully qualified app name +fullnameOverride: "" + +# -- Annotations applied to metadata.annotations of every top-level object +# this chart creates (Deployment, Service, ConfigMap, Secret, PVC, etc.). +# Required by the Kyverno component annotation check. +# IMPORTANT: also set the identical value under postgresql.commonAnnotations +# below — Helm does not propagate top-level values to subcharts automatically. +commonAnnotations: {} + +# -- Number of replicas +replicaCount: 1 + +# -- Container image configuration +image: + repository: "ghcr.io/getnao/nao" # official image from GitHub Container Registry + pullPolicy: IfNotPresent + tag: "" # empty defaults to .Chart.AppVersion + +# -- Image pull secrets (e.g. for private registries) +imagePullSecrets: [] + +# -- Service account configuration +serviceAccount: + create: true + # -- commonAnnotations (above) is already merged automatically here by the chart. + # Leave empty unless you need an annotation specific to the ServiceAccount only. + annotations: {} + name: "" + +# -- Extra annotations added to the Pod template (spec.template.metadata). +# Does NOT cover metadata.annotations of the Deployment itself — see +# commonAnnotations above for that (required by Kyverno). +podAnnotations: {} + +# -- Pod-level security context. +# NOTE: supervisord inside the image starts as root (uid 0) and drops privileges +# to the "nao" user (uid 1000). Leave empty unless you have a custom image. +podSecurityContext: {} + +# -- Container-level security context +securityContext: {} + +# ============================================================================= +# Application configuration +# ============================================================================= +config: + # -- Listening port for the Fastify backend + serverPort: "5005" + # -- Internal FastAPI sidecar port (not exposed externally) + fastapiPort: "8005" + # -- Public URL of the app — used for auth callbacks and trusted origins. + betterAuthUrl: "" + # -- Node environment + nodeEnv: "production" + + # contextSource: local => project mounted as a volume at contextPath, + # populated by the gitSync initContainer (see gitSync below). + contextSource: "local" + + # -- Path inside the container where the nao project is located. + # Matches the official `docker run` doc convention + # (-v /path/to/your/nao-project:/app/project -e NAO_DEFAULT_PROJECT_PATH=/app/project). + contextPath: "/app/project" + + # -- Native git mode fields (only used when contextSource=git) + contextGitUrl: "" + contextGitBranch: "main" + refreshSchedule: "" + + # --------------------------------------------------------------------------- + # Git sync — populates the `context` PVC via an initContainer git-clone. + # Set this to the HTTPS URL of your nao context repository. + # --------------------------------------------------------------------------- + # --------------------------------------------------------------------------- + # Git sync — DISABLED by default (use kubectl cp for manual population). + # Empty url => the chart does not generate a git-sync initContainer (see condition + # in deployment.yaml line 35); the PVC `context` is created EMPTY. + # + # To re-enable git-sync later, set: + # url: "https://github.com/your-org/your-nao-project.git" + # --------------------------------------------------------------------------- + # --------------------------------------------------------------------------- + # Git sync — initContainer git-clone, HTTPS + access token. + # No SSH here: the initContainer injects the token directly into + # the HTTPS URL (https://oauth2:@host/...), handled automatically via + # secrets.contextGitToken below. No SSH keys or known_hosts to manage. + # The URL should be the "Clone with HTTPS" URL of the repo (not git@...). + # --------------------------------------------------------------------------- + gitSync: + url: "" + branch: "main" + image: "alpine/git:latest" + + # -- Optional PostHog analytics key + posthogKey: "" + posthogHost: "https://eu.i.posthog.com" + # -- "true" disables PostHog tracking entirely (editor analytics for NAO). + # Required in internal deployments: without a provided key, NAO defaults to + # their own public PostHog key hardcoded in apps/shared/src/posthog.ts, which + # triggers an outbound connection to eu.i.posthog.com — + # blocked on internal networks without direct internet access + # (FailedToOpenSocket error seen in logs). + posthogDisabled: "true" + + # --------------------------------------------------------------------------- + # LLM provider base URLs + # --------------------------------------------------------------------------- + openaiBaseUrl: "" + anthropicBaseUrl: "" + geminiBaseUrl: "" + mistralBaseUrl: "" + openrouterBaseUrl: "" + ollamaBaseUrl: "" + +# ============================================================================= +# Secrets +# ============================================================================= +secrets: + # -- Required. Generate with: openssl rand -base64 32 + betterAuthSecret: "" + + # -- LLM provider API keys + openaiApiKey: "" # Set your OpenAI API key here + anthropicApiKey: "" + + # -- Azure OpenAI (optional) + azureApiKey: "" + azureResourceName: "" + azureOpenaiBaseUrl: "" + azureApiVersion: "" + + # -- AWS Bedrock (optional) + awsBearerTokenBedrock: "" + awsRegion: "" + awsAccessKeyId: "" + awsSecretAccessKey: "" + + # -- Git token for private repositories (used by the initContainer git-sync) + # -- Git access token (minimal scope: read_repository) used by + # the initContainer git-sync to clone the context repo via HTTPS. + # Generate this in your Git provider: Settings > Access Tokens (at the project + # level or a dedicated service account), then paste the value below. + contextGitToken: "" + + # -- Enterprise license key or path to license file (EE features only) + naoLicense: "" + + # -- SMTP credentials (optional) + smtpHost: "" + smtpPort: "" + smtpSsl: "false" + smtpMailFrom: "" + smtpPassword: "" + + # -- Google OAuth (optional) + googleClientId: "" + googleClientSecret: "" + googleAuthDomains: "" + + # -- GitHub OAuth (optional) + githubClientId: "" + githubClientSecret: "" + githubAllowedUsers: "" + + # -- Notion integration (optional) + notionApiKey: "" + + # -- Database URI. Leave empty: managed automatically via postgresql.enabled=true. + dbUri: "" + +# ============================================================================= +# Kubernetes Service +# ============================================================================= +service: + type: ClusterIP + port: 80 + targetPort: 5005 + annotations: {} + +# ============================================================================= +# Resources +# ============================================================================= +resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + +# ============================================================================= +# Probes +# ============================================================================= +livenessProbe: + tcpSocket: + port: 5005 + initialDelaySeconds: 30 + periodSeconds: 15 + failureThreshold: 3 + +readinessProbe: + tcpSocket: + port: 5005 + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 3 + +# ============================================================================= +# Autoscaling (HPA) +# ============================================================================= +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + +# ============================================================================= +# Pod Disruption Budget +# ============================================================================= +podDisruptionBudget: + enabled: false + minAvailable: 1 + +# ============================================================================= +# Node scheduling +# ============================================================================= +nodeSelector: {} +tolerations: [] +affinity: {} + +# ============================================================================= +# Persistence — used when config.contextSource = "local" +# Populated at pod startup by the gitSync initContainer. +# ============================================================================= +persistence: + enabled: true + storageClass: "" + accessMode: ReadWriteOnce + size: 1Gi + existingClaim: "" + annotations: {} + +# ============================================================================= +# Projects volume — used when config.contextSource = "api" +# ============================================================================= +projectsPersistence: + enabled: false + storageClass: "" + accessMode: ReadWriteOnce + size: 5Gi + existingClaim: "" + annotations: {} + +# ============================================================================= +# PostgreSQL subchart (bitnami/postgresql) +# ============================================================================= +postgresql: + enabled: true + # -- IMPORTANT: identical to commonAnnotations at the top of this file — Helm does + # not automatically propagate parent chart values to subcharts. + # This resolves the Kyverno error on the nao-postgresql StatefulSet. + commonAnnotations: {} + # -- Bitnami migrated their versioned images from docker.io/bitnami to + # docker.io/bitnamilegacy (October 2025): older tags are no longer + # accessible on the free "bitnami" repo. Same tag, just a different repo. + # bitnamilegacy no longer receives security patches — migrate to a + # long-term solution (paid Secure Images, or the official postgres image) + # as soon as possible. + image: + repository: bitnamilegacy/postgresql + tag: 17.5.0-debian-12-r12 + auth: + database: nao + username: nao + password: "" # Change in production + postgresPassword: "" # Change in production + primary: + persistence: + enabled: true + size: 8Gi \ No newline at end of file From e1bd2e44abd7c05b5d7d0e8f251ed93d61bb9c86 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:46:40 +0200 Subject: [PATCH 02/25] feat: add Helm chart for Kubernetes deployment --- helm/ARCHITECTURE.md | 2 -- helm/README.md | 13 ++----------- helm/templates/secret.yaml | 3 --- helm/values.yaml | 4 ---- 4 files changed, 2 insertions(+), 20 deletions(-) diff --git a/helm/ARCHITECTURE.md b/helm/ARCHITECTURE.md index 88f7b5f65..b012813ae 100644 --- a/helm/ARCHITECTURE.md +++ b/helm/ARCHITECTURE.md @@ -319,8 +319,6 @@ All values under `secrets` are stored in a Kubernetes `Secret` object. In produc | Key | Env var | Description | |-----|---------|-------------| | `secrets.contextGitToken` | `NAO_CONTEXT_GIT_TOKEN` | Personal access token or deploy key for cloning a private git repository (`git` mode) | -| `secrets.naoLicense` | `NAO_LICENSE` | Enterprise license key or path to license file. Unlocks EE-only features (SSO, etc.). Leave empty for OSS mode. | - #### Email — SMTP (optional) | Key | Env var | Description | diff --git a/helm/README.md b/helm/README.md index c04997b68..f8a0539af 100644 --- a/helm/README.md +++ b/helm/README.md @@ -4,8 +4,6 @@ Helm chart for deploying [nao](https://github.com/mfournioux/nao) — an open-so ## Prerequisites -- Kubernetes **1.24+** -- Helm **3.10+** - (Optional) A running PostgreSQL instance, or enable the bundled bitnami/postgresql subchart ## Chart structure @@ -51,7 +49,7 @@ helm dependency update ./helm helm install nao ./helm \ --namespace nao --create-namespace \ --set secrets.betterAuthSecret="$(openssl rand -base64 32)" \ - --set secrets.openaiApiKey="sk-..." \ + --set secrets.openaiApiKey="" \ --set config.betterAuthUrl="https://nao.example.com" ``` @@ -128,7 +126,7 @@ projectsPersistence: | `secrets.anthropicApiKey` | `""` | Anthropic API key | | `secrets.dbUri` | `""` | Database URI (overridden when `postgresql.enabled=true`) | | `secrets.contextGitToken` | `""` | Git token for private repos | -| `secrets.naoLicense` | `""` | Enterprise license key (EE only) | +| `secrets.contextGitToken` | `""` | Git token for private repos | | `service.type` | `ClusterIP` | Kubernetes service type | | `service.port` | `80` | Service port | | `resources.requests.cpu` | `500m` | CPU request | @@ -173,13 +171,6 @@ secrets: dbUri: "sqlite:./db.sqlite" ``` -## Exposing nao with an Ingress - -```yaml -config: - betterAuthUrl: https://nao.example.com -``` - ## Upgrade ```bash diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml index c11c34fa1..209bd0956 100644 --- a/helm/templates/secret.yaml +++ b/helm/templates/secret.yaml @@ -46,9 +46,6 @@ stringData: {{- if .Values.secrets.contextGitToken }} NAO_CONTEXT_GIT_TOKEN: {{ .Values.secrets.contextGitToken | quote }} {{- end }} - {{- if .Values.secrets.naoLicense }} - NAO_LICENSE: {{ .Values.secrets.naoLicense | quote }} - {{- end }} {{- if .Values.secrets.smtpHost }} SMTP_HOST: {{ .Values.secrets.smtpHost | quote }} SMTP_PORT: {{ .Values.secrets.smtpPort | quote }} diff --git a/helm/values.yaml b/helm/values.yaml index 2c39c3389..a2bacddd8 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -81,8 +81,6 @@ config: serverPort: "5005" # -- Internal FastAPI sidecar port (not exposed externally) fastapiPort: "8005" - # -- Public URL of the app — used for auth callbacks and trusted origins. - betterAuthUrl: "" # -- Node environment nodeEnv: "production" @@ -175,8 +173,6 @@ secrets: # level or a dedicated service account), then paste the value below. contextGitToken: "" - # -- Enterprise license key or path to license file (EE features only) - naoLicense: "" # -- SMTP credentials (optional) smtpHost: "" From a83ca00e252464543055a1ec0833a0860eadf57d Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:05:16 +0200 Subject: [PATCH 03/25] Remove betterAuthUrl from helm install command Removed betterAuthUrl configuration from helm install command. Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> --- helm/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helm/README.md b/helm/README.md index f8a0539af..373630ab1 100644 --- a/helm/README.md +++ b/helm/README.md @@ -50,7 +50,6 @@ helm install nao ./helm \ --namespace nao --create-namespace \ --set secrets.betterAuthSecret="$(openssl rand -base64 32)" \ --set secrets.openaiApiKey="" \ - --set config.betterAuthUrl="https://nao.example.com" ``` ## Context modes @@ -202,4 +201,4 @@ helm uninstall nao --namespace nao > **Note:** PersistentVolumeClaims are not deleted automatically. Remove them manually if no longer needed: > ```bash > kubectl delete pvc -l app.kubernetes.io/instance=nao --namespace nao -> ``` \ No newline at end of file +> ``` From 4cd177bb0d780f98770be090a1700f2b22e7b046 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:14:33 +0200 Subject: [PATCH 04/25] Remove link to nao GitHub repository in README Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> --- helm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/README.md b/helm/README.md index 373630ab1..89611095f 100644 --- a/helm/README.md +++ b/helm/README.md @@ -1,6 +1,6 @@ # nao Helm Chart -Helm chart for deploying [nao](https://github.com/mfournioux/nao) — an open-source analytics agent that transforms natural language into SQL queries — on Kubernetes. +Helm chart for deploying nao — an open-source analytics agent that transforms natural language into SQL queries — on Kubernetes. ## Prerequisites From b04b481b8ee05bdf04af1b4771939f419cbe9d17 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:07:15 +0200 Subject: [PATCH 05/25] Update helm/README.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> --- helm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/README.md b/helm/README.md index 89611095f..481d5598f 100644 --- a/helm/README.md +++ b/helm/README.md @@ -49,7 +49,7 @@ helm dependency update ./helm helm install nao ./helm \ --namespace nao --create-namespace \ --set secrets.betterAuthSecret="$(openssl rand -base64 32)" \ - --set secrets.openaiApiKey="" \ + --set secrets.openaiApiKey="" ``` ## Context modes From 34b47ee53cf701d7873a3b4a8a7d88c1d65a4729 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:07:29 +0200 Subject: [PATCH 06/25] Update helm/templates/tests/test-connection.yaml Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> --- helm/templates/tests/test-connection.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml index c4a4511db..baeb3a89e 100644 --- a/helm/templates/tests/test-connection.yaml +++ b/helm/templates/tests/test-connection.yaml @@ -11,7 +11,7 @@ spec: restartPolicy: Never containers: - name: curl - image: curlimages/curl:latest + image: curlimages/curl:8.12.1 command: - curl - --fail From bc4e4e727d282f2ba5ba856149b01791d514f120 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:07:41 +0200 Subject: [PATCH 07/25] Update helm/values.yaml Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> --- helm/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/values.yaml b/helm/values.yaml index a2bacddd8..be6b5c386 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -263,7 +263,7 @@ affinity: {} # Populated at pod startup by the gitSync initContainer. # ============================================================================= persistence: - enabled: true + enabled: false storageClass: "" accessMode: ReadWriteOnce size: 1Gi From 23f289671be1cda0c87d775c6a5ef7d958e2cc1b Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:18:46 +0200 Subject: [PATCH 08/25] fix: secure git-sync credential handling to prevent token leakage in .git/config --- helm/templates/deployment.yaml | 42 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 7fc489570..98d480781 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -36,27 +36,6 @@ spec: initContainers: - name: git-sync image: {{ .Values.config.gitSync.image | quote }} - command: - - sh - - -c - - | - set -eu - if [ -n "${GIT_TOKEN:-}" ]; then - AUTH_URL=$(echo "${GIT_URL}" | sed "s#https://#https://oauth2:${GIT_TOKEN}@#") - else - AUTH_URL="${GIT_URL}" - fi - if [ -d "{{ .Values.config.contextPath }}/.git" ]; then - echo "Existing checkout found, pulling latest..." - cd "{{ .Values.config.contextPath }}" - git remote set-url origin "${AUTH_URL}" - git fetch --depth 1 origin "${GIT_BRANCH}" - git reset --hard "origin/${GIT_BRANCH}" - else - echo "No checkout found, cloning..." - rm -rf "{{ .Values.config.contextPath }}"/* - git clone --depth 1 --branch "${GIT_BRANCH}" "${AUTH_URL}" "{{ .Values.config.contextPath }}" - fi env: - name: GIT_BRANCH value: {{ .Values.config.gitSync.branch | quote }} @@ -68,7 +47,28 @@ spec: secretKeyRef: name: {{ include "nao.fullname" . }} key: NAO_CONTEXT_GIT_TOKEN + - name: GIT_ASKPASS + value: /bin/true + - name: GIT_TERMINAL_PROMPT + value: '0' {{- end }} + command: + - sh + - -c + - | + set -eu + rm -rf {{ .Values.config.contextPath }}/* + # Configure credential helper so the token is never embedded in .git/config + if [ -n "${GIT_TOKEN:-}" ]; then + git config --global credential.helper '!f() { echo "username=oauth2"; echo "password='"${GIT_TOKEN}"'"; }; f' + fi + # Clone using GIT_ASKPASS so the token is never stored in .git/config + git clone --depth 1 --branch "${GIT_BRANCH}" "${GIT_URL}" {{ .Values.config.contextPath }} + cd {{ .Values.config.contextPath }} + echo "Checkout found, pulling latest..." + git remote set-url origin "${GIT_URL}" + git fetch --depth 1 origin "${GIT_BRANCH}" + git reset --hard "origin/${GIT_BRANCH}" volumeMounts: - name: context mountPath: {{ .Values.config.contextPath }} From b2be4f7a8a453585e12732f8599a5615ff1d3c19 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:24:04 +0200 Subject: [PATCH 09/25] fix: add BETTER_AUTH_URL default to values.yaml matching backend zod validation --- helm/ARCHITECTURE.md | 2 +- helm/values.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/helm/ARCHITECTURE.md b/helm/ARCHITECTURE.md index b012813ae..6fc91ccec 100644 --- a/helm/ARCHITECTURE.md +++ b/helm/ARCHITECTURE.md @@ -270,7 +270,7 @@ These values are rendered into the `ConfigMap` and injected as environment varia |-----|---------|---------|-------------| | `config.serverPort` | `"5005"` | `SERVER_PORT` | Fastify backend listening port | | `config.fastapiPort` | `"8005"` | `FASTAPI_PORT` | Internal FastAPI sidecar port — not exposed outside the pod | -| `config.betterAuthUrl` | `""` | `BETTER_AUTH_URL` | **Public URL** of the app. Must match the URL users access. Used for OAuth callbacks and trusted-origin checks. | +| `config.betterAuthUrl` | `"http://localhost:5005"` | `BETTER_AUTH_URL` | **Public URL** of the app. Must match the URL users access. Used for OAuth callbacks and trusted-origin checks. | | `config.nodeEnv` | `"production"` | `NODE_ENV` | Node.js environment | | `config.contextSource` | `"local"` | `NAO_CONTEXT_SOURCE` | Context loading mode: `local` \| `git` \| `api` | | `config.contextPath` | `"/app/project"` | `NAO_DEFAULT_PROJECT_PATH` | Absolute path inside the container where the nao project is mounted or cloned | diff --git a/helm/values.yaml b/helm/values.yaml index be6b5c386..769ab65c5 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -83,6 +83,10 @@ config: fastapiPort: "8005" # -- Node environment nodeEnv: "production" + # -- Public URL of the app — used for auth callbacks and trusted origins. + # Matches the backend default (zod validation fallback). + # Override with the URL users access in their browser. + betterAuthUrl: "http://localhost:5005" # contextSource: local => project mounted as a volume at contextPath, # populated by the gitSync initContainer (see gitSync below). From 7bdf0b0c781135a33528b900458bed10c827f044 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:33:26 +0200 Subject: [PATCH 10/25] fix: correct DB_URI printf format in _helpers.tpl --- helm/templates/_helpers.tpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 2225df8fb..8509bda10 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -93,9 +93,10 @@ Build the DB_URI from subchart values when postgresql.enabled=true. */}} {{- define "nao.dbUri" -}} {{- if .Values.postgresql.enabled -}} +{{- $password := required "postgresql.auth.password must be set when postgresql.enabled=true" .Values.postgresql.auth.password -}} {{- printf "postgres://%s:%s@%s:5432/%s" .Values.postgresql.auth.username - .Values.postgresql.auth.password + $password (include "nao.postgresqlHost" .) .Values.postgresql.auth.database -}} {{- else -}} From d1074ea3dd51af3f4a06789aea5a8be71322dddb Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:35:56 +0200 Subject: [PATCH 11/25] fix: add commonAnnotations to ServiceAccount and Service templates --- helm/templates/service.yaml | 9 +++++++++ helm/templates/serviceaccount.yaml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml index 4b4b61e5d..cb62d04f1 100644 --- a/helm/templates/service.yaml +++ b/helm/templates/service.yaml @@ -5,7 +5,16 @@ metadata: namespace: {{ .Release.Namespace }} labels: {{- include "nao.labels" . | nindent 4 }} + {{- $ann := dict }} + {{- with include "nao.annotations" . }} + {{- with . }} + {{- $ann = merge $ann (fromYaml .) }} + {{- end }} + {{- end }} {{- with .Values.service.annotations }} + {{- $ann = merge $ann . }} + {{- end }} + {{- with $ann }} annotations: {{- toYaml . | nindent 4 }} {{- end }} diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml index 285b97d33..eea69be92 100644 --- a/helm/templates/serviceaccount.yaml +++ b/helm/templates/serviceaccount.yaml @@ -6,7 +6,16 @@ metadata: namespace: {{ .Release.Namespace }} labels: {{- include "nao.labels" . | nindent 4 }} + {{- $ann := dict }} + {{- with include "nao.annotations" . }} + {{- with . }} + {{- $ann = merge $ann (fromYaml .) }} + {{- end }} + {{- end }} {{- with .Values.serviceAccount.annotations }} + {{- $ann = merge $ann . }} + {{- end }} + {{- with $ann }} annotations: {{- toYaml . | nindent 4 }} {{- end }} From 3a710af613fa7a49032c06d491a38e8f185159c3 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:36:52 +0200 Subject: [PATCH 12/25] fix: add git clean after git reset in git-sync initContainer --- helm/templates/deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 98d480781..96d6befef 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -69,6 +69,8 @@ spec: git remote set-url origin "${GIT_URL}" git fetch --depth 1 origin "${GIT_BRANCH}" git reset --hard "origin/${GIT_BRANCH}" + # Remove untracked files (e.g., leftover from previous revisions) + git clean -fdx volumeMounts: - name: context mountPath: {{ .Values.config.contextPath }} From f41058035a5f762dccb61fa1cddac43ca8f5fe26 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:37:51 +0200 Subject: [PATCH 13/25] fix: add checksum annotations to Deployment for ConfigMap/Secret changes The Deployment uses envFrom with configMapRef and secretRef but does not change when only the referenced ConfigMap or Secret data changes, causing helm upgrade to silently skip rolling out updated env values. Adding checksum annotations for both templates triggers an automatic rollout when configuration or secret data changes. --- helm/templates/deployment.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 96d6befef..651d52eef 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -18,10 +18,12 @@ spec: {{- include "nao.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/config: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} labels: {{- include "nao.selectorLabels" . | nindent 8 }} spec: From fe113239df365be08d3d1865e69f6287aeb9e19e Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:39:55 +0200 Subject: [PATCH 14/25] fix: remove entire context directory before git clone to avoid hidden files --- helm/templates/deployment.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 651d52eef..2ef624e48 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -59,7 +59,8 @@ spec: - -c - | set -eu - rm -rf {{ .Values.config.contextPath }}/* + rm -rf "{{ .Values.config.contextPath }}" + mkdir -p "{{ .Values.config.contextPath }}" # Configure credential helper so the token is never embedded in .git/config if [ -n "${GIT_TOKEN:-}" ]; then git config --global credential.helper '!f() { echo "username=oauth2"; echo "password='"${GIT_TOKEN}"'"; }; f' From 6e66c6185f2d94e9089395aa2e20b2cc3d4f1266 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:42:29 +0200 Subject: [PATCH 15/25] fix: set NAO_PROJECTS_DIR for api contextSource mode When contextSource=api, the chart set NAO_CONTEXT_SOURCE=api but did not configure NAO_PROJECTS_DIR, which the backend uses to locate dynamically deployed projects in /app/projects. This caused context endpoints to fail in api mode. Adding NAO_PROJECTS_DIR=/app/projects when contextSource=api ensures the backend can find deployed projects on the PVC --- helm/templates/configmap.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 34aa6505c..30ea0c8b5 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -17,7 +17,9 @@ data: DOCKER: "1" BETTER_AUTH_URL: {{ .Values.config.betterAuthUrl | quote }} NAO_CONTEXT_SOURCE: {{ .Values.config.contextSource | quote }} - {{- if ne .Values.config.contextSource "api" }} + {{- if eq .Values.config.contextSource "api" }} + NAO_PROJECTS_DIR: /app/projects + {{- else }} NAO_DEFAULT_PROJECT_PATH: {{ .Values.config.contextPath | quote }} {{- end }} {{- if eq .Values.config.contextSource "git" }} From 6dc858b6cd8df23dc94d1629e0b3983b211763de Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:43:23 +0200 Subject: [PATCH 16/25] docs: document allowInsecureImages as temporary migration setting The bitnamilegacy/postgresql image requires allowInsecureImages: true to pass Bitnami's container image verification. This is a temporary setting during the migration from the Bitnami postgresql subchart. Document it clearly in values.yaml so users understand it's a short-term workaround and know how to remove it when the chart switches to the official postgres image. --- helm/values.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/helm/values.yaml b/helm/values.yaml index 769ab65c5..a69d89136 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -26,6 +26,9 @@ # Safe here: same tag/image, just a different Docker Hub repository. global: security: + # Required: bitnamilegacy/postgresql is an archived image (see values.yaml:303). + # Set to false and update image.repository to the official postgres image + # when the chart migrates away from the Bitnami postgresql subchart. allowInsecureImages: true # -- Override chart name @@ -295,11 +298,10 @@ postgresql: # This resolves the Kyverno error on the nao-postgresql StatefulSet. commonAnnotations: {} # -- Bitnami migrated their versioned images from docker.io/bitnami to - # docker.io/bitnamilegacy (October 2025): older tags are no longer - # accessible on the free "bitnami" repo. Same tag, just a different repo. - # bitnamilegacy no longer receives security patches — migrate to a - # long-term solution (paid Secure Images, or the official postgres image) - # as soon as possible. + # docker.io/bitnamilegacy (October 2025). Same tag/image, just a + # different repository. This will be replaced with the official + # postgres image or a paid Secure Images subscription in the next + # major chart version. See TODO-POSTGRESQL-MIGRATION.md for details. image: repository: bitnamilegacy/postgresql tag: 17.5.0-debian-12-r12 From 75bf2ffd419770bcd72c5ea45e11206815648d57 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:44:14 +0200 Subject: [PATCH 17/25] fix: remove duplicate secrets.contextGitToken row from README table --- helm/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/helm/README.md b/helm/README.md index 481d5598f..50183f2c2 100644 --- a/helm/README.md +++ b/helm/README.md @@ -125,7 +125,6 @@ projectsPersistence: | `secrets.anthropicApiKey` | `""` | Anthropic API key | | `secrets.dbUri` | `""` | Database URI (overridden when `postgresql.enabled=true`) | | `secrets.contextGitToken` | `""` | Git token for private repos | -| `secrets.contextGitToken` | `""` | Git token for private repos | | `service.type` | `ClusterIP` | Kubernetes service type | | `service.port` | `80` | Service port | | `resources.requests.cpu` | `500m` | CPU request | From ab53f42ef17e52d57ab65dcbd54860b7b9a9756b Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:45:17 +0200 Subject: [PATCH 18/25] docs: add missing LLM base URLs and gitSync.image to ARCHITECTURE.md config table Add 7 missing values to the Application config (ConfigMap) reference table: config.gitSync.image, config.openaiBaseUrl, config.anthropicBaseUrl, config.geminiBaseUrl, config.mistralBaseUrl, config.openrouterBaseUrl, config.ollamaBaseUrl. These are rendered into the ConfigMap but were previously undocumented. --- helm/ARCHITECTURE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/helm/ARCHITECTURE.md b/helm/ARCHITECTURE.md index 6fc91ccec..459bcec1c 100644 --- a/helm/ARCHITECTURE.md +++ b/helm/ARCHITECTURE.md @@ -279,9 +279,16 @@ These values are rendered into the `ConfigMap` and injected as environment varia | `config.refreshSchedule` | `""` | `NAO_REFRESH_SCHEDULE` | Cron expression for periodic git pulls, e.g. `"0 * * * *"`. Empty = disabled. | | `config.gitSync.url` | `""` | — | HTTPS URL of the context repo (initContainer git-sync, `local` mode only) | | `config.gitSync.branch` | `"main"` | — | Branch to sync (initContainer git-sync) | +| `config.gitSync.image` | `"alpine/git:latest"` | — | Container image for the git-sync initContainer | | `config.posthogKey` | `""` | `POSTHOG_KEY` | PostHog project API key (optional usage analytics) | | `config.posthogHost` | `"https://eu.i.posthog.com"` | `POSTHOG_HOST` | PostHog ingest endpoint | | `config.posthogDisabled` | `"false"` | `POSTHOG_DISABLED` | Set to `"true"` to opt out of usage analytics entirely (recommended for on-prem deployments to prevent outbound connections to external analytics) | +| `config.openaiBaseUrl` | `""` | `OPENAI_BASE_URL` | Custom OpenAI API base URL (e.g. for proxy or self-hosted models) | +| `config.anthropicBaseUrl` | `""` | `ANTHROPIC_BASE_URL` | Custom Anthropic API base URL | +| `config.geminiBaseUrl` | `""` | `GEMINI_BASE_URL` | Custom Gemini API base URL | +| `config.mistralBaseUrl` | `""` | `MISTRAL_BASE_URL` | Custom Mistral API base URL | +| `config.openrouterBaseUrl` | `""` | `OPENROUTER_BASE_URL` | Custom OpenRouter API base URL | +| `config.ollamaBaseUrl` | `""` | `OLLAMA_BASE_URL` | Custom Ollama API base URL | ### Secrets From bd1c2dd350a4a21e5e8fbea6f540ec1706d4f7bb Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:46:11 +0200 Subject: [PATCH 19/25] docs: remove stale NAO_LICENSE reference from ARCHITECTURE.md NAO_LICENSE was previously removed from templates/secret.yaml and values.yaml but the Secret vs ConfigMap split section in ARCHITECTURE.md still references it. Remove it to keep docs consistent. --- helm/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/ARCHITECTURE.md b/helm/ARCHITECTURE.md index 459bcec1c..cb9c72777 100644 --- a/helm/ARCHITECTURE.md +++ b/helm/ARCHITECTURE.md @@ -224,7 +224,7 @@ Environment variables are split between a `ConfigMap` (non-sensitive) and a `Sec - `BETTER_AUTH_SECRET` - `DB_URI` (auto-built from subchart values when `postgresql.enabled=true`) - All LLM provider keys: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, Azure, AWS Bedrock -- `NAO_CONTEXT_GIT_TOKEN`, `NAO_LICENSE` +- `NAO_CONTEXT_GIT_TOKEN` - SMTP credentials, OAuth client secrets (Google, GitHub) - `NOTION_API_KEY` From dbf94399327392be664896fcea2523826c8f3f08 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:47:02 +0200 Subject: [PATCH 20/25] fix: remove dead git pull code from git-sync initContainer --- helm/templates/deployment.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 2ef624e48..f72a3fe2d 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -67,13 +67,7 @@ spec: fi # Clone using GIT_ASKPASS so the token is never stored in .git/config git clone --depth 1 --branch "${GIT_BRANCH}" "${GIT_URL}" {{ .Values.config.contextPath }} - cd {{ .Values.config.contextPath }} - echo "Checkout found, pulling latest..." - git remote set-url origin "${GIT_URL}" - git fetch --depth 1 origin "${GIT_BRANCH}" - git reset --hard "origin/${GIT_BRANCH}" - # Remove untracked files (e.g., leftover from previous revisions) - git clean -fdx + echo "Context repository cloned successfully" volumeMounts: - name: context mountPath: {{ .Values.config.contextPath }} From 03c18bf3a093dbdad7c661816ef7d657661615a6 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:54:02 +0200 Subject: [PATCH 21/25] Update helm/values.yaml Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> --- helm/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/values.yaml b/helm/values.yaml index a69d89136..95d0d060d 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -301,7 +301,7 @@ postgresql: # docker.io/bitnamilegacy (October 2025). Same tag/image, just a # different repository. This will be replaced with the official # postgres image or a paid Secure Images subscription in the next - # major chart version. See TODO-POSTGRESQL-MIGRATION.md for details. + # major chart version. image: repository: bitnamilegacy/postgresql tag: 17.5.0-debian-12-r12 From c08545531681d9a4989ac9c6dd79c0652922fd54 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:48:49 +0200 Subject: [PATCH 22/25] fix: preserve PVC mount point in git-sync initContainer --- helm/templates/deployment.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index f72a3fe2d..bff7f029b 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -59,8 +59,8 @@ spec: - -c - | set -eu - rm -rf "{{ .Values.config.contextPath }}" - mkdir -p "{{ .Values.config.contextPath }}" + rm -rf "{{ .Values.config.contextPath }}"/* + rm -rf "{{ .Values.config.contextPath }}"/.[!.]* "{{ .Values.config.contextPath }}"/..?* 2>/dev/null || true # Configure credential helper so the token is never embedded in .git/config if [ -n "${GIT_TOKEN:-}" ]; then git config --global credential.helper '!f() { echo "username=oauth2"; echo "password='"${GIT_TOKEN}"'"; }; f' From 8662e7d1a77c506ca4b5a48d72b7fd94d08288bf Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:50:22 +0200 Subject: [PATCH 23/25] fix: correct annotation merge precedence in Service and ServiceAccount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helm Sprig's merge keeps the destination dictionary's keys — the source only fills in missing keys. The original code used merge serviceAnnotations, which made commonAnnotations silently override resource-specific annotations. Changed to merge serviceAnnotations so that service/serviceAccount-specific annotations take precedence over commonAnnotations, allowing users to override defaults on individual resources. --- helm/templates/service.yaml | 2 +- helm/templates/serviceaccount.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml index cb62d04f1..7bc2a08e8 100644 --- a/helm/templates/service.yaml +++ b/helm/templates/service.yaml @@ -12,7 +12,7 @@ metadata: {{- end }} {{- end }} {{- with .Values.service.annotations }} - {{- $ann = merge $ann . }} + {{- $ann = merge . $ann }} {{- end }} {{- with $ann }} annotations: diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml index eea69be92..289a87de1 100644 --- a/helm/templates/serviceaccount.yaml +++ b/helm/templates/serviceaccount.yaml @@ -13,7 +13,7 @@ metadata: {{- end }} {{- end }} {{- with .Values.serviceAccount.annotations }} - {{- $ann = merge $ann . }} + {{- $ann = merge . $ann }} {{- end }} {{- with $ann }} annotations: From 7a559f45a0f303e2c9b476a6748ff2bd6f4f8825 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:51:24 +0200 Subject: [PATCH 24/25] fix: remove required guard from DB_URI helper, add existingSecret support The required guard on postgresql.auth.password breaks valid Bitnami workflows like auth.existingSecret or global password override. Replace with a graceful fallback: when existingSecret is set, use secrets.dbUri from values.yaml instead. Document that existingSecret requires manual dbUri configuration. This aligns with Bitnami's secret management patterns while keeping the inline password path working as before. --- helm/templates/_helpers.tpl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 8509bda10..26746cf87 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -87,18 +87,22 @@ otherwise falls back to a user-supplied host embedded in secrets.dbUri. {{- printf "%s-postgresql" (include "nao.fullname" .) -}} {{- end -}} {{- end }} - {{/* Build the DB_URI from subchart values when postgresql.enabled=true. +Does not support auth.existingSecret — when using existingSecret, +set secrets.dbUri manually with the full connection string. */}} {{- define "nao.dbUri" -}} {{- if .Values.postgresql.enabled -}} -{{- $password := required "postgresql.auth.password must be set when postgresql.enabled=true" .Values.postgresql.auth.password -}} +{{- if not .Values.postgresql.auth.existingSecret }} {{- printf "postgres://%s:%s@%s:5432/%s" .Values.postgresql.auth.username - $password + .Values.postgresql.auth.password (include "nao.postgresqlHost" .) .Values.postgresql.auth.database -}} +{{- else }} +{{- .Values.secrets.dbUri }} +{{- end }} {{- else -}} {{- .Values.secrets.dbUri -}} {{- end -}} From cca793129d68f2c925b35f1934296dcef8b7b296 Mon Sep 17 00:00:00 2001 From: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:04:38 +0200 Subject: [PATCH 25/25] Update helm/templates/deployment.yaml Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com> --- helm/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index bff7f029b..b1c32d0d1 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -60,7 +60,7 @@ spec: - | set -eu rm -rf "{{ .Values.config.contextPath }}"/* - rm -rf "{{ .Values.config.contextPath }}"/.[!.]* "{{ .Values.config.contextPath }}"/..?* 2>/dev/null || true + rm -rf "{{ .Values.config.contextPath }}"/.[!.]* "{{ .Values.config.contextPath }}"/..?* # Configure credential helper so the token is never embedded in .git/config if [ -n "${GIT_TOKEN:-}" ]; then git config --global credential.helper '!f() { echo "username=oauth2"; echo "password='"${GIT_TOKEN}"'"; }; f'