/<org-slug>/…, and the events stream is the org home (/<org-slug>/). Page names in this guide (for example /sessions, /queries) refer to those org-scoped routes.
Viewing Logs
AgentEye does not bundle a logging or monitoring stack. Both the server and dashboard write structured logs to stdout, so you can read them directly withkubectl or docker; no aggregator required.
Kubernetes
Follow live logs for the server and dashboard:| Goal | Command |
|---|---|
| Last 200 lines (no follow) | kubectl logs -n agenteye -l app=server --tail=200 --timestamps |
| Logs from the previous crash | kubectl logs -n agenteye <pod-name> --previous |
| Tail all replicas at once | kubectl logs -n agenteye -l app=server --max-log-requests=10 -f |
| Postgres (StatefulSet) | kubectl logs -n agenteye postgres-0 -f |
Docker Compose
Correlating a single request across dashboard and server
Every dashboard request is tagged with arequest_id and propagated to the server via the x-request-id header. The server echoes it in its response headers and in every log line it emits for that request. To trace one request end-to-end:
- Capture the id from the response header, e.g.:
- Grep both pods’ logs for that id:
proxy passthrough, withAuth: authorized, and upstream response lines alongside the server’s http request received / http request completed pair, all sharing the same request_id.
JSON logs and jq
Set AE_LOG_JSON=1 on the dashboard (it is on by default when NODE_ENV=production) to emit one JSON object per line. Then filter structurally:
key=value pairs that grep well without jq:
Turning up verbosity
| Component | Env var | Example |
|---|---|---|
| Server | RUST_LOG | RUST_LOG=debug or RUST_LOG=agenteye_server=debug,info |
| Dashboard | AE_LOG_LEVEL | AE_LOG_LEVEL=debug |
debug on the server adds a per-auth api key authenticated line. debug on the dashboard adds upstream request, session validated, and proxy passthrough lines.
Log retention
Container stdout is ephemeral; kubelet rotates log files (default ~10 MiB per container) and keeps a small number on disk. Once a pod is deleted the logs are gone. If you need longer retention or cross-pod search, point your cluster at a log collector (Loki, CloudWatch, Cloud Logging, Datadog, etc.) that tails/var/log/containers/. AgentEye does not require or prescribe any specific choice.
Authentication Issues
docker pull fails with “unauthorized”
Ensure you have authenticated Docker against GHCR with your AGENTEYE_TOKEN:
read:packages permission on the agenteye-enterprise org. Contact support@exosphere.host if your token does not work.
gh release download returns 404 or 401
- Confirm
AGENTEYE_TOKENis exported in your shell:echo $AGENTEYE_TOKEN - Confirm you are using
GITHUB_TOKEN=$AGENTEYE_TOKEN gh release download ...(theghCLI readsGITHUB_TOKEN) - The token needs
contents:readonagenteye-enterprise/releases
Server Issues
Server fails with “invalid port number”
ThePOSTGRES_PASSWORD (or another credential) contains URL-special characters (/, +, =) that break DATABASE_URL parsing. Regenerate the password using hex encoding:
.env for Docker Compose), and restart the server. See the full steps in enterprise-docs/kubernetes-deployment.md § “PostgreSQL credentials”.
Server exits immediately on startup
Check the container logs:DATABASE_URLnot set or malformed: the server will log the error and exit.- Postgres is not reachable: confirm the Postgres container or managed DB is running and the host/port are correct.
- Migrations failed: check logs for SQL errors.
GET /health returns non-200 or times out
The server may still be running migrations on first start. Wait a few seconds and retry:
docker logs agenteye-server for errors.
GET /ready returns 503
/ready is the readiness probe: it returns 503 when the server cannot reach Postgres or ClickHouse. The body names the failing dependency:
down: is the ClickHouse/Postgres pod Running? Is CLICKHOUSE_URL / DATABASE_URL correct and reachable? On Kubernetes the pod reads NotReady until /ready recovers; that is expected and is exactly the signal health monitoring alerts on. Redis is never a cause: it is reported but does not fail readiness.
Collector returns 401 Unauthorized
The collector’s API key does not haveevents:add permission, or the key has been disabled. Create a new key with the correct permission:
Authed requests suddenly got slow (~200ms instead of ~5ms)
This is the symptom of Redis being down whileREDIS_URL is set. Every cache call times out after 100ms then falls through to Postgres; on auth and OTP paths the request makes two such fall-throughs.
Confirm in the server logs:
redis-cli -h <your-redis> pingto confirm Redis is reachable on the cluster network.- If Redis was briefly down and is now back, restart the server pods. The
redis::aio::ConnectionManagerdoes not reliably re-establish after the underlying connection drops; a pod restart picks up the new connection cleanly. The same applies to the dashboard. - If you don’t want to run Redis right now, unset
REDIS_URLin the deployment and restart. Both services run without the cache (correctness is preserved; latency reverts to pre-Redis baseline).
Server reports OTP request rate-limited in logs but the user says they only tried once
Check whether Redis was unreachable. The fallback path uses SELECT COUNT(*) FROM otp_codes WHERE created_at > now() - interval '15 minutes', which sees previously-generated OTP rows. If the user has been click-spamming “Resend” for an hour, the 15-minute window may still contain ≥5 codes. Resolve by either waiting for the window to roll over or DELETE FROM otp_codes WHERE user_id = $1 AND created_at > now() - interval '15 minutes' (operator console).
I changed ALLOWED_EMAILS / SESSION_TTL_SECS / OTP_TTL_SECS and restarted; nothing happened
These env vars are first-boot seeds only. Once the settings table has a row for the matching key, that row is the source of truth; the env var is read once at first boot and then ignored on every subsequent restart.
To change them after first boot, log into the dashboard and edit them under /settings. The change applies within seconds across all replicas; no restart required.
If you need to force a re-seed from env (rare, typically only useful in development), DELETE FROM settings WHERE key = '<key>' and restart the server. The bootstrap will pick up the current env-var value on the next boot. Editing via /settings is the supported path in production.
Collector Issues
Collector starts but events are not appearing in the dashboard
- Confirm the collector is running:
systemctl status agenteye-collector(Linux) or check the process. - Confirm
AGENTEYE_URLpoints tohttp(s)://your-server-host:8080/events(note:/eventspath). - Run a one-shot flush to see immediate output:
- Check that the Python SDK is actually writing files:
ls ${AGENTEYE_HOME:-~/.agenteye}/events/ - If files exist in
${AGENTEYE_HOME:-~/.agenteye}/failed/, the uploads are failing. Check the collector logs for the error, likely a 4xx (bad key or URL) or network issue.
Files are accumulating in $AGENTEYE_HOME/events/ and not being uploaded
- The collector may not be running. Start it:
agenteye-collector start; it automatically flushes pre-existing events on startup. - Check collector health:
agenteye-collector health - The collector may be running but unable to reach the server. Check firewall rules between collector and server hosts.
Files in $AGENTEYE_HOME/failed/
Files move to failed/ after all retry attempts are exhausted (default: 5 attempts with exponential backoff). This means either:
- The server returned a 4xx error (bad key, wrong URL, or payload issue)
- The server was unreachable for the entire retry window
Collector reports network error on every upload (TLS handshake fails)
If curl -k against AGENTEYE_URL succeeds but the collector binary fails every upload with error sending request for url (...), the AgentEye server is presenting a TLS certificate that isn’t signed by a publicly-trusted CA.
The production path is the ACME ingest hostname configured in deploy/base/certificates/domain.env (see kubernetes-deployment.md Phase 3.1 / 4.2). Once INGEST_DOMAIN resolves to the public Traefik LB and cert-manager has issued the Let’s Encrypt cert, collectors verify the server cert against the system trust store with no AGENTEYE_TLS_CA needed; clear it from your collector config if it was set against an older self-signed deployment.
Symptom: collector worked yesterday, fails today after a ~90-day gap. This means the deployment is still on the legacy selfsigned issuer for ingest-tls. The 90-day cert rotated and the pinned CA file is stale. Fix permanently by switching the cluster to the ACME issuer (Phase 3.1 of the deployment guide). Short-term unblock: re-extract the current server cert and update AGENTEYE_TLS_CA:
AGENTEYE_TLS_CA adds an additional trust anchor; the standard public roots are still trusted.
ingest-tls Certificate is stuck Ready: False after deploy
Events and the referenced Order / Challenge. Common causes:
- DNS not resolving to the public LB. The HTTP-01 validator can’t reach
INGEST_DOMAIN. Verify withdig +short INGEST_DOMAIN; it should resolve to the same address as thetraefik-publicLoadBalancer’sEXTERNAL-IP. cert-manager retries automatically once DNS propagates; no need to delete the Certificate. - Port 80 blocked at the load balancer / security group. HTTP-01 requires port 80 to be reachable from Let’s Encrypt’s public validators. If you have an upstream WAF or SG restricting
:80, open it (the Traefik config redirects to HTTPS, but Boulder follows the redirect and accepts the response). dnsNamesnot substituted. Ifkubectl get certificate ingest-tls -n agenteye -o jsonpath='{.spec.dnsNames}'showsINGEST_DOMAIN_PLACEHOLDER, you skipped thedomain.envstep; create it fromdomain.env.exampleand re-apply.- Rate limited by Let’s Encrypt. Repeated failed orders for the same hostname trip the duplicate-certificate or failed-validation limits. Wait at least an hour before retrying; check the Order status for the exact rate-limit message.
dashboard-tls Certificate is stuck Ready: False / browser still shows a warning
Same diagnosis flow as ingest-tls above (kubectl describe certificate dashboard-tls -n agenteye); the DNS, port-80, placeholder, and rate-limit causes all apply, plus two dashboard-specific ones:
DASHBOARD_DOMAINresolves to the wrong LoadBalancer. It must point at the dashboard Traefik LB, not the public ingest one.dig +shortthe hostname and compare against the dashboard LB address.- The dashboard Traefik instance can’t serve the challenge. It must be installed with the bundled dashboard values file, which enables a scoped Ingress provider for cert-manager’s HTTP-01 solver. Without it the solver is unroutable and the Order stays
pendingforever. Upgrade the instance with the provided values; the pending challenge then completes on its own. - The LoadBalancer was IP-restricted. Source ranges apply to port 80 too, which blocks Let’s Encrypt’s validators — both first issuance and every ~75-day renewal. Re-open the LB, or coordinate a DNS-01 solver with support before locking it down.
CLI still skips TLS verification after the dashboard got a trusted certificate
--insecure is persisted to cli.json at login. Once the dashboard serves a publicly-trusted certificate, log in again with agenteye --base-url https://<your-dashboard-domain> --secure login; verification is saved back on and the startup warning disappears.
Dashboard Issues
Can’t disable or edit the ADMIN_EMAIL user
By design. The user matching ADMIN_EMAIL is marked protected on every server startup: the dashboard hides the Disable button for that row, and the API rejects DELETE /users/:id and PUT /users/:id against it with 403 Forbidden. A database trigger also rejects direct UPDATE statements that would disable the protected row.
To rotate the bootstrap admin, change ADMIN_EMAIL in your environment and restart the server. The new email is upserted as protected. The previous admin retains the protected flag until cleared in the database (typically fine, since the previous email is still a valid admin until you explicitly remove them).
Dashboard shows no events
- Confirm the server URL and API key are correct in the dashboard’s environment variables (
AGENTEYE_SERVER_URL,AGENTEYE_API_KEY). - The dashboard API key needs
events:readpermission. - Confirm events have actually been ingested:
curl http://your-server:8080/events -H "Authorization: Bearer $ADMIN_KEY"
/errors is empty but /events shows red rows
Newer SDK versions emit failures as agent_end / tool_result / hook_completed events with outcome: "error" in the payload, rather than as a dedicated event_type: "error" row. The /errors page now matches both: any row the /events stream paints red (explicit event_type='error', payload outcome/status in the failure set, is_error: true, or a truthy error field) appears on /errors. If you previously saw “no errors in this window” while red rows were visible on /events, upgrade the dashboard + server together (the broadened filter is errored=true on GET /events) and the two views will agree.
/models, /tools, or /hooks is slow or fails to load on wide time ranges
Symptom: on a large events table (millions of rows), opening /models, /tools, or /hooks — or widening the time range to 7d, 30d, or all — the charts spin and then show a load error. The server logs a ClickHouse MEMORY_LIMIT_EXCEEDED (Code 241) or a query timeout for the latency_aggregate request.
Cause: older builds computed these pages’ latency and distribution rollups with a query that read the full raw event payload and paired request/response events with an in-memory sort-and-join. Peak query memory therefore grew with the size of the window, so on a busy tenant a wide range could exceed ClickHouse’s per-query memory ceiling.
Fix: upgrade to a build that includes this fix. The rollup now reads only the compact promoted columns and pairs events with a streaming aggregation, so peak memory no longer scales with the raw payload — wide windows stay well within the memory ceiling and return in a fraction of the time. The improvement is entirely query-side: it applies to all existing data on the next page load, with no re-ingest or backfill.
Dashboard fails to load / blank page
Check the dashboard container logs:AGENTEYE_SERVER_URL or AGENTEYE_API_KEY being missing or pointing to an unreachable server.
Dashboard analytics / telemetry
The dashboard sends anonymous product-usage analytics to PostHog by default, routed through the dashboard’s own/ingest path (a reverse proxy to https://us.i.posthog.com). Sending them first-party means browser ad-blockers don’t drop them. This is independent of the dashboard’s core functionality:
- The dashboard container (not the browser) is what reaches PostHog. If its outbound access to
https://us.i.posthog.comis blocked, telemetry silently no-ops; the dashboard works normally and no errors are surfaced to users. - No agent, session, or event data is ever included, only dashboard UI usage.
- To disable telemetry entirely, set
AE_ANALYTICS_DISABLED=1on the dashboard container and restart. See Telemetry & privacy in the deployment guide.
CLI analytics / telemetry
Theagenteye CLI sends anonymous usage analytics to PostHog by default: which commands run, success/exit status, and duration. This is independent of the CLI’s functionality:
- The machine running the CLI reaches
https://us.i.posthog.comdirectly. If its outbound access is blocked, telemetry silently no-ops (sending is time-bounded, so it never delays a command) and the CLI works normally. - No agent, session, or event data is ever included: command arguments and flag values (dashboard URL, token, email, session ids, query filters) are never sent.
- To disable it, set
AGENTEYE_ANALYTICS_DISABLED=1(or the cross-toolDO_NOT_TRACK=1) in the CLI’s environment. See Telemetry & privacy in the CLI guide.
AI Assistant Issues
See enterprise-docs/assistant.md for full setup.The assistant bubble doesn’t appear
The bubble is hidden unless all of these hold:- The signed-in user has the
agent:usepermission. AGENTEYE_AGENT_URLis set on the dashboard and theagentservice is reachable.- An LLM endpoint is configured on the
agentservice (ANTHROPIC_API_KEY, a gateway viaANTHROPIC_BASE_URL, or Bedrock/Vertex). With none set, the agent reports “not configured” and the bubble stays hidden.
curl http://agent:9100/health should return {"status":"ok","llm_configured":true,...}.
The assistant says it can’t read something
Tools are gated per user. If a user lacksevaluations:read (or events:read, dashboards:read), the matching tools aren’t offered and the assistant will say it can’t read that data. Grant the relevant read permission.
”assistant not configured” (HTTP 503) when sending
Theagent container has no LLM endpoint configured, or the dashboard’s AGENTEYE_AGENT_TOKEN doesn’t match the agent’s. Set both and restart.
The agent container restarts / OOMs under load
Each conversation spawns a short-lived child process. Ensure the container runs with an init process (the image uses tini; in Compose set init: true) and give it adequate memory limits. Reduce AGENTEYE_AGENT_MAX_STEPS if needed.
CLI Issues
agenteye fails to start with ModuleNotFoundError: No module named 'click'
A fresh install of the agenteye CLI at version 0.1.6 can crash on startup with:
click being installed indirectly by typer; current typer releases no longer
pull it in, so a clean environment ends up missing the package. Upgrade to 0.1.7 or newer,
which depends on click directly:
Python SDK Issues
No files appearing in $AGENTEYE_HOME/events/
The SDK buffers events and flushes every 500 ms by default. If your process exits before the flush, events may be lost. Call agenteye.configure(flush_interval=0.1) for faster flushing in short-lived scripts, or ensure your process runs long enough for a flush cycle.
If AGENTEYE_HOME is set, verify the SDK is writing to $AGENTEYE_HOME/events/ and not ~/.agenteye/events/ (requires SDK ≥ 0.0.1b5).
ValueError: Reserved field names cannot be used as custom fields
The names timestamp, type, and environment are reserved and cannot be used as custom fields. Passing any of them raises:
session_id and agent_id are explicit parameters of the event call, not custom fields; passing either again as a custom field raises TypeError.
Health Monitoring Issues
No alerts arriving in Slack (Robusta)
Robusta health alerting is opt-in; it sends nothing until installed and pointed at a Slack channel. Verify the release and its sink:api_key / slack_channel were not set (or the token was revoked); the api_key is a Robusta cloud-relay token (robusta integrations slack) but the bundled disableCloudRouting: true needs a self-hosted Slack bot token (xoxb-…), or set disableCloudRouting: false; the sink scope excludes the namespace your pods run in (the bundled values scope to agenteye); or no failure has occurred yet. Force a test alert by taking a pod down:
Server keeps flapping NotReady
The readiness probe hits /ready, which fails when Postgres or ClickHouse is unreachable. If the server cycles in and out of NotReady, a dependency is intermittently unavailable; check the ClickHouse and Postgres pods and the server’s CLICKHOUSE_URL / DATABASE_URL. Confirm what /ready reports:
/health, so flapping readiness will not restart the pod.
Certificate Monitoring Issues
CronJob is not sending Slack notifications
Thecert-renewal-check CronJob requires a Slack webhook URL stored in a Secret. Verify it exists:
Client certificate expired before a notification was received
The CronJob runs every 12 hours. If it has not been running, check its status:collector-mtls-secret.yaml in the cluster(s) running your collectors and restart them:
Backup Issues
agenteye-backup fails with “No space left on device”
The agenteye-backup CronJob dumps Postgres + ClickHouse into a backup-tmp emptyDir scratch volume (default 30Gi), then streams the tar archive straight to S3 — the compressed archive is never written back to scratch, so scratch only has to hold the raw dumps, not dumps + a second on-disk archive copy. A pod evicted / No space left on device therefore means the raw dumps exceed the scratch size (the ClickHouse events dump dominates and grows over time). Check the failed job’s logs:
backup-tmp emptyDir sizeLimit above your raw dump total, and make sure the node’s ephemeral storage can actually hold it (sizeLimit is a cap, not a reservation). If the dumps outgrow a single node’s disk, replace the emptyDir with a PVC (EBS/PD) for backup-tmp, or compress the dumps at the source.
Older releases wrote the.tar.gzinto the same20Giscratch as the dumps, sodumps + archiveoverflowed it and the pod was evicted before the upload ran — which looks like an S3 failure but is really disk. Streaming the upload removes that doubling.
agenteye-backup fails installing curl
The job runs on the postgres:16 image and installs curl at startup for the ClickHouse HTTP dump. On a cluster with no egress to the Debian package mirrors, the apt-get step fails. Either allow that egress from the backup pod, or bake curl into a mirrored/custom backup image and reference it in your overlay.
agenteye-backup runs but nothing lands in object storage
The base ships a real BACKUP_BUCKET (ts-prod-agenteye/backups) and the agenteye-backup ServiceAccount. The job streams the archive to S3 (tar cz … | aws s3 cp - s3://…). If the backup pod has no write access to the bucket, the upload errors — and because the script runs under set -euo pipefail, a failure anywhere in that pipe fails the whole job at the upload step rather than silently no-op’ing (the pod’s EXIT trap logs backup FAILED during step: upload). This is also the step you reach after fixing a scratch-space eviction, so if backups were previously evicted at the archive step, verify the upload now lands. Grep the failed job’s logs for the S3 access error:
BACKUP_BUCKET to a bucket you own and annotate the existing agenteye-backup ServiceAccount with write access (IRSA / Workload Identity / Pod Identity). See the Backups section of enterprise-docs/kubernetes-deployment.md.
ClickHouse-backed evaluations / sessions / queries
The /queries page sidebar is empty after upgrading
Three tables (events, evaluations, agent_sessions) are expected. If the SchemaBrowser sidebar is empty after upgrade, the server failed to apply the ClickHouse DDL at startup. Check the server logs for failed to apply CH DDL statement:
CrashLoopBackOff rather than a silently broken queries page, but a partial DDL apply (one statement OK, the next 5xx) leaves the schema half-baked. Restart the server pod after CH is verified reachable:
New evaluations are not appearing in /sessions or /queries
After the upgrade, new evaluations are written to ClickHouse, not Postgres, and surface under /sessions (gated on evaluations:read) and in /queries. If they don’t appear:
- Confirm the evaluator pipeline is enabled (
EVALUATOR_ENDPOINTset on the server) and producing terminal outcomes; check forevaluation_finalizedlog lines. - Confirm CH is reachable from the server:
kubectl exec -n agenteye deploy/server -- curl -fsS http://clickhouse:8123/ping. - Spot-check the CH table:
kubectl exec -n agenteye clickhouse-0 -- clickhouse-client -q 'SELECT count() FROM agenteye.evaluations'.
Queries fail under load with “Memory limit exceeded”, or ClickHouse is OOMKilled
Symptom: under heavy dashboard/query load, analytical pages (the events stream, /sessions, the models/latency view, the SQL editor) start failing or timing out; the server briefly flaps NotReady; and the ClickHouse pod shows a rising restart count. This is almost always memory, not CPU or disk.
Confirm it’s memory (not a throughput problem that replication would fix):
-
Check the pod for out-of-memory kills:
Reason: OOMKilled/Exit Code: 137with a climbing restart count is the tell. -
Ask ClickHouse what it’s rejecting:
A large
MEMORY_LIMIT_EXCEEDEDcount is the signature. The message reads “maximum: N GiB” — that N is0.9 × the pod's memory limit(themax_server_memory_usage_to_ram_ratioindeploy/base/clickhouse/configmap.yaml). If your heavy reads need more than N, they’re rejected. -
Rule out the things that aren’t the problem — if CPU, part count, and disk are all low, adding replicas/sharding would be wasted cost:
payload column, run JSONExtract* over it, and use FINAL — each can need several GiB. If the configured caches (mark_cache_size + uncompressed_cache_size) are larger than the pod, they compound it: caches are charged against the same budget and crowd out query memory.
Fix — scale ClickHouse’s memory:
- Raise the ClickHouse memory limit in your overlay by patching the
clickhouseStatefulSet’s containerresources(the same overlay mechanism used for the other components’resources). The usable server budget is0.9 × limit, so a6Gilimit gives ~5.4 GiB,16Gigives ~14 GiB. Setrequests.memoryto a real floor too, so the scheduler reserves it. Applying this recreates the CH pod (single replica → ~30–60s of analytics downtime); do it in a low-traffic window. - Keep the caches in
deploy/base/clickhouse/configmap.yamlproportionate to the limit — small caches (a few hundred MiB) are safe on a small pod; only raise them alongside a matching memory-limit increase. Per-querymax_memory_usageis set explicitly in theusers.xmlprofile (see the fixed-node section below) and is kept below the server-level cap (0.9 × limit) so no single query is allowed more RAM than the container has. - If the node itself is the ceiling, check the host memory ClickHouse can see:
If that’s only a little above the pod limit, move ClickHouse to a larger (memory-optimized) node — via a node selector/affinity in your overlay — before raising the limit further.
500 mid-flight while ClickHouse keeps grinding — keeping queries in RAM and rejecting the rare over-budget one fast (MEMORY_LIMIT_EXCEEDED, sub-second) is what restores loading. Note a ClickHouse gotcha for applying these:
- These are profile settings, and ClickHouse reads
<profiles>only fromusers_config(users.xml/users.d/*.xml) — never fromconfig.d. A<profiles>block placed inconfig.d/agenteye.xmlis silently ignored (max_execution_time,max_memory_usage, etc. simply don’t apply). The bundled config therefore ships them as ausers.xmlkey on theclickhouse-configConfigMap, mounted at/etc/clickhouse-server/users.d/agenteye.xml. - The shipped defaults:
max_memory_usage(per-query ceiling — one query can’t consume the whole server budget),max_bytes_before_external_group_by/max_bytes_before_external_sort=0(spill disabled) so queries stay in RAM instead of crawling on the slow disk, andmax_execution_time(runaway guard, aligned with the server’s client read timeout). - Verify they’re live (this is also how you detect the config.d gotcha):
Expect a non-zero
max_memory_usageandmax_bytes_before_external_group_by = 0. Ifmax_memory_usagereads0/default, the profile isn’t being applied — check the settings live in ausers.dmount, notconfig.d.
max_memory_usage is rejected (MEMORY_LIMIT_EXCEEDED) rather than completing slowly — on a slow disk that fast rejection is preferable, because a spilling query would exceed the client timeout and fail anyway. If your data disk is fast (SSD), you can instead raise the max_bytes_before_external_* thresholds to let big queries spill to disk and complete.
Multi-tenancy (organizations)
Errors during the upgrade that enables organizations (mixed old/new server pods)
Symptom: during a rolling deploy of the org-enabling release, some requests fail: server logs showthere is no unique or exclusion constraint matching the ON CONFLICT specification on the api_keys path, and/or alert/Slack/webhook channels stop firing while the rollout is in flight.
Cause: the upgrade replaces the old instance-wide unique index on api_keys(name) with per-org partial indexes, and moves the alert-channel settings (and default_user_permissions) out of the global settings table into per-org org_settings. An old server pod still issues ON CONFLICT (name) (now no matching constraint) and still reads channel config from the old settings rows (now empty). Old and new pods cannot safely coexist for these two paths.
Fix: do not slow-roll this particular upgrade across mixed versions. Cut over cleanly: scale the old server to zero (or use a brief maintenance window) and bring the new version up together with its migrations, rather than running old and new replicas side by side. Normal traffic and ingest resume immediately after cutover; this only affects the version-transition window.
Provisioning an organization fails on CREATE USER / CREATE ROW POLICY, or one org can read another org’s data
Symptom: creating an org returns an error mentioning CREATE USER, CREATE ROW POLICY, or “access management is disabled”; or, worse, members of one org see another org’s events/evaluations in the SQL editor or assistant.
Cause: per-org isolation is enforced by a dedicated ClickHouse user + row policy per org. That requires SQL access management to be enabled and users_without_row_policies_can_read_rows=false on ClickHouse. With access management off, provisioning can’t create the user/policy; with the row-policy default left at its permissive value, a user that has SELECT but no policy reads all rows (fail-open).
Fix: use the bundled deploy/base/clickhouse/ config, which sets both. If you run your own ClickHouse config, enable SQL access management on the server-internal user and set users_without_row_policies_can_read_rows=false (see deploy/base/clickhouse/configmap.yaml), then restart ClickHouse and re-create the org with the agenteye-orgctl CLI (see enterprise-docs/tenant-management.md).
Org users lose ClickHouse access after changing ORG_CH_SECRET
Symptom: the SQL editor and AI assistant suddenly return ClickHouse authentication failures for every organization, immediately after ORG_CH_SECRET was changed or set inconsistently across replicas.
Cause: each org’s ClickHouse password is derived as an HMAC of ORG_CH_SECRET. Rotating it (or running replicas with different values) invalidates every org’s stored ClickHouse credential; the derived password no longer matches the provisioned user.
Fix: set ORG_CH_SECRET to a single strong value before provisioning a second org and keep it stable and identical on every server replica. The server’s boot-time reconcile re-provisions each org’s ClickHouse user from the current secret on startup, so a server restart across all replicas (with the secret consistent) heals the orphaned users. Treat the value as a long-lived secret; don’t rotate it casually. As a safety net, if ORG_CH_SECRET is left at the built-in dev default (i.e. unset), the boot-time reconcile skips non-default organizations and logs an error rather than rewriting their ClickHouse credentials to the publicly-known dev value, so a single replica that restarts without the secret can’t break the other replicas. Set the secret consistently and restart to provision those orgs.
The AI assistant returns 400 / refuses to chat after enabling organizations
Symptom: the assistant dock loads but every message comes back as an error (HTTP400), and the agent logs a rejected org-less /chat request.
Cause: the agent is org-aware and fails closed; it rejects a /chat that carries no organization context. This happens during a transitional rollout where the agent has been upgraded but the dashboard sending the request is not yet org-aware.
Fix: finish the rollout so the dashboard sends org context (the normal end state, no flag needed). To bridge the gap while a not-yet-org-aware dashboard talks to an org-aware agent, set AGENTEYE_AGENT_ALLOW_NO_ORG=1 on the agent service so it falls back to the default org instead of refusing, and clear it once the dashboard upgrade lands. See the env reference in enterprise-docs/assistant.md.
Audits
An audit never runs (next run keeps slipping, no run history)
Symptom: the audit page shows last run: never, ornext run keeps moving into the future without a row appearing in run history.
Cause: the audit is disabled (disabled audits have no queue entry), or the server’s audit workers are failing to claim work.
Fix: confirm the audit is enabled (the run-now button requires it). Then check the server logs for audits pipeline started at boot and for audits: errors — a claim_due failed line points at Postgres connectivity. AUDIT_WORKERS defaults to 1; it must be ≥ 1 for any audit to run.
Audit runs succeed but find nothing
Symptom: run history showssucceeded with findings: 0 even though /errors clearly shows failures.
Cause: the scan window doesn’t cover the failures, or the scope filters them out.
Fix: check the run row’s window (window_from → window_to) against when the failures happened — in since_last mode each run only scans since the previous successful run, so older failures are only seen by the first run or a fixed-window audit. Widen scope (environments / agent ids). The run stats show policy_hits (how many deterministic policies fired) and improvements (how many the AI investigation recorded) — if both are 0, the window/scope genuinely saw nothing.
The run says analysis_unavailable and produces only policy findings
Symptom: run stats include analysis_unavailable and the only findings are kind: policy; no AI improvements appear.
Cause: the agentic investigation couldn’t run: the server can’t reach the agent service (AGENTEYE_AGENT_URL / AGENTEYE_AGENT_TOKEN unset on the server — the audit reuses the assistant’s connection), the assistant service has no LLM configured, or the call errored/timed out (the analysis_unavailable string has the detail). The deterministic policy pass is the floor — it always runs — so the audit still succeeds with its security findings.
Fix: set AGENTEYE_AGENT_URL (e.g. http://agent:9100) and AGENTEYE_AGENT_TOKEN on the server — the same values the dashboard assistant already uses (the bundled manifests/compose now wire them) — and configure an LLM on the assistant service (see assistant.md), then re-run. A large investigation may need a bigger AUDIT_LLM_TIMEOUT_MS (server) — keep it above the agent’s AGENTEYE_AUDIT_TIMEOUT_MS.
The audit code sandbox is disabled (sandbox_available: false)
Symptom: the agent’s /health shows sandbox_available: false, and audit runs note the sandbox is unavailable; the AI investigates with SQL only.
Cause: the in-pod bubblewrap sandbox needs unprivileged user namespaces, which the pod’s seccomp profile or the node kernel is blocking.
Fix: set seccompProfile: Unconfined (k8s) or security_opt: [seccomp:unconfined] (compose) on the agent, and confirm the node kernel allows unprivileged user namespaces (some managed images, e.g. GKE COS, disable them). Where you can’t enable it, this is expected and safe — the auditor degrades to SQL-only automatically. See deployment.md.
Audit email report not delivered
Symptom: an audit surfaced new findings but no email arrived. Cause: the audit has no email channel attached, email is disabled org-wide inalerts.enabled_channels, there are no recipients, or SMTP is unconfigured.
Fix: attach an email channel to the audit, ensure email is in alerts.enabled_channels, set recipients (on the channel or via alerts.email_default_recipients), and configure SMTP (the same transport the alerts + OTP emails use). Email is sent only when a run produces at least one new finding.
A muted or dismissed pattern keeps its old finding page but never re-ranks
Symptom: after muting a finding, later runs never surface that pattern again — even though it still occurs. Cause: that is the designed behaviour: mute/dismiss are durable suppressions keyed on the pattern’s fingerprint. Fix: open the finding and use reopen to clear the suppression; the next run will rank the pattern again. Use resolve (not mute) for “fixed” patterns you’d want to hear about if they regress.Getting Help
Contactsupport@exosphere.host with:
- Your AgentEye version (from the release tag)
- Relevant container logs (
docker logs <container>) - A description of the issue and what you have already tried

