Vorschau — Bereitstellung folgt (M7)Preview — provisioning lands in M7
Einen Mandanten in Betrieb nehmenStanding up a tenant
Diese Ansicht stellt die Konfiguration eines neuen Mandanten zusammen und prüft jede Eingabe gegen die Regel, die der Code tatsächlich durchsetzt. Sie legt nichts an: es gibt heute keinen Endpunkt, der einen Mandanten erzeugt — die Registry wird im Prozess aus der Umgebung befüllt (TenantRegistry.from_env) oder in Tests gesetzt. Der Tenant-Store der Steuerebene ist ein M7-Thema.This view assembles a new tenant's configuration and checks every entry against the rule the code actually enforces. It provisions nothing: there is no endpoint today that creates a tenant — the registry is seeded in-process from the environment (TenantRegistry.from_env) or set in tests. The control plane's tenant store is an M7 item.
Das Ergebnis ist die Datei unten: eine .env im 12-Faktor-Stil, die Sie in Ihren Deployment-Pfad legen. Geheimnisse stehen darin ausschließlich als Variablennamen mit _FILE-Pfad — diese Oberfläche erzeugt und zeigt niemals einen echten Geheimniswert.The result is the file at the bottom: a 12-factor .env you drop into your deployment path. Secrets appear in it only as variable names with a _FILE path — this interface never generates or displays a real secret value.
Mandanten-KennungTenant id
Die Kennung wird zu einem Pfadsegment und zu einem DNS-Label. Deshalb ist ihr Format eine Sicherheitsgrenze und keine Kosmetik: ein Slug, der ausbrechen könnte, wird bei der Konstruktion abgelehnt, nicht erst bei der Benutzung.The id becomes a path segment and a DNS label. Its format is therefore a security boundary, not cosmetics: a slug that could escape its root is rejected at construction, not at use.
- Erlaubte ZeichenPermitted characters a–z 0–9 -
- Muster (genau diese Regex)Pattern (exactly this regex) ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$
- LängeLength 1 … 63
- Normalisierung vor der PrüfungNormalised before validation strip() · lower()
- Reservierte KennungenReserved identifiers api admin console www static health internal acp
Daraus folgt: DatenwurzelDerived: data root
—
Daraus folgt: DNS-LabelDerived: DNS label
—
Region & DatenresidenzRegion & data residency
Ehrlich gesagt: ACP_REGION ist heute ein deklaratives Feld. Es wird in Settings.region gehalten und von GET /v1/tenant zurückgegeben — es verschiebt von sich aus keine Daten. Die tatsächliche Residenz ergibt sich daraus, wo dieser Prozess läuft, wo ACP_DATA_ROOT liegt und welche Engine-Endpunkte Sie in Schritt 3 wählen.Plainly: ACP_REGION is a declarative field today. It is held in Settings.region and returned by GET /v1/tenant — it does not move data by itself. Actual residency follows from where this process runs, where ACP_DATA_ROOT lives, and which engine endpoints you pick in step 3.
Jeder Mandant hat seine eigene Datenwurzel. Transkripte, Wissen und Geheimnisse liegen unter <ACP_DATA_ROOT>/<mandant>/ — Isolation ist ein Pfad, keine Konvention.Each tenant owns its data root. Transcripts, knowledge and secrets live under <ACP_DATA_ROOT>/<tenant>/ — isolation is a path, not a convention.
EnginesEngines
Alle Engines stehen ab Werk auf mock. Das ist kein Platzhalter, den man vergisst zu ersetzen, sondern die Voreinstellung mit Absicht: die gesamte Plattform läuft und testet grün mit null bezahlten Zugangsdaten. Ob ein echter Engine-Client überhaupt konstruiert werden darf, entscheidet ACP_ENV — nicht diese Felder.Every engine ships as mock. That is not a placeholder somebody forgot to replace, it is the deliberate default: the whole platform runs and tests green with zero paid credentials. Whether a real engine client may be constructed at all is decided by ACP_ENV — not by these fields.
- cascaded — STT → LLM → TTS. Drei austauschbare Teile, jedes einzeln messbar.cascaded — STT → LLM → TTS. Three swappable parts, each measurable on its own.
- realtime — Sprache-zu-Sprache in einem Modell. Schneller, aber der Anbieter hört den Rohton.realtime — speech-to-speech in one model. Faster, but the provider hears the raw audio.
- RESTRICTED sticht diese Wahl — Gespräche mit data_class=restricted werden zwangsweise auf parakeet + piper im cascaded-Pfad gelegt und verlassen die eigene Infrastruktur nie. Es gibt keinen Fallback über diese Grenze hinweg: eine sensible Spur, die on-prem nicht bedient werden kann, scheitert, statt zu einer Frontier-API abzufließen.RESTRICTED overrides this choice — conversations carrying data_class=restricted are forced onto parakeet + piper on the cascaded path and never leave your own infrastructure. There is no cross-boundary fallback: a sensitive lane that cannot be served on-prem fails rather than leaking to a frontier API.
- CONFIDENTIAL — darf eine Frontier-API nur bedienen, wenn ACP_CONFIDENTIAL_FRONTIER=true gesetzt ist, was eine EU-Region samt Zero-Retention-DPA voraussetzt. Sonst: ebenfalls on-prem.CONFIDENTIAL — may be served by a frontier API only when ACP_CONFIDENTIAL_FRONTIER=true, which presumes an EU region plus a zero-retention DPA. Otherwise: on-prem as well.
Freigabe-Schwelle & ZugbudgetApproval threshold & turn budget
Aktionen über diesem Betrag warten blockierend auf einen Menschen. Der Agent antwortet erst, wenn entschieden wurde.Actions above this amount block on a human. The agent does not answer until somebody decides.
Das Latenzbudget eines Wortwechsels — die Physik natürlicher Sprache. Wird es überschritten, entsteht das Ereignis turn.budget_exceeded.The latency budget for one turn — the physics of natural voice. A breach raises the turn.budget_exceeded event.
ERP-Anbindung (Odoo)ERP connection (Odoo)
Drei Felder sind Konfiguration und stehen hier. Das vierte ist ein Geheimnis und steht bewusst nicht hier: ODOO_API_KEY wird niemals in eine Weboberfläche getippt, sondern als Datei bereitgestellt.Three fields are configuration and belong here. The fourth is a secret and deliberately does not: ODOO_API_KEY is never typed into a web interface, it is supplied as a file.
Das GeheimnisThe secret
ODOO_API_KEY_FILE=/run/secrets/odoo_api_key
Die Datei schlägt den Inline-Wert. Ein Docker- oder Kubernetes-Secret taucht so weder in docker inspect noch in einem Crash-Dump auf. Leerer Inhalt gilt als nicht gesetzt.The file beats the inline value. A Docker or Kubernetes secret then appears in neither docker inspect nor a crash dump. Empty contents count as unset.
Ohne ERP-Anbindung bleibt der Agent auskunftsfähig, aber handlungsunfähig: er kann nachschlagen, aber nichts buchen.Without an ERP connection the agent can still inform but cannot act: it can look things up, but book nothing.
API-Schlüssel ausstellenIssue an API key
Vorschau — Bereitstellung folgt (M7)Preview — provisioning lands in M7Es gibt keinen Endpunkt, der einen Schlüssel ausstellt, und diese Seite erzeugt auch keinen im Browser — ein clientseitig gewürfelter „Schlüssel“ wäre eine Zeichenkette ohne Eintrag im Store und würde bei der ersten Anfrage mit 401 abgewiesen. Ausgestellt wird heute im Prozess, über ApiKeyStore.issue().No endpoint issues a key, and this page does not mint one in the browser either — a client-side “key” would be a string with no record in the store and would be refused with 401 on its first request. Issuance happens in-process today, through ApiKeyStore.issue().
So wird ausgestellt (auf dem Server, einmal)How issuance happens (server-side, once)
Der Klartext existiert genau in dieser einen Ausgabe. Gespeichert wird nur ein SHA-256-Hash; ist der Klartext fort, ist er fort, und ein neuer Schlüssel muss ausgestellt werden.The plaintext exists only in that one output. Only a SHA-256 hash is stored; once the plaintext is gone it is gone, and a new key must be issued.
Webhook-EndpunktWebhook endpoint
Hier nur die Vorprüfung: die Regeln, die der Server ohnehin anwendet, vorab im Browser. Registrieren können Sie den Endpunkt drüben unter Webhooks, sobald ein Schlüssel vorliegt.A dry run only: the rules the server applies anyway, checked here in the browser first. You register the endpoint over in Webhooks, once you have a key.
ErgebnisResult
Konfiguration — VorschauConfiguration — preview
Jede Zeile mit _FILE erwartet eine Datei mit dem Geheimnis darin. Diese Vorschau nennt nur Namen und Pfade — sie kennt keinen einzigen Ihrer Werte und soll auch keinen kennen.Every _FILE line expects a file containing the secret. This preview names variables and paths only — it knows none of your values, and is not meant to.
MandantTenant
Ein Aufrufer kann nicht sagen, welchen Mandanten er lesen möchte. Der Schlüssel sagt es. GET /v1/tenant ist deshalb die ehrlichste Antwort auf die Frage „wofür gilt dieser Schlüssel eigentlich?“ — er gibt zurück, als wer Sie sprechen.A caller cannot name the tenant it wants to read. The key names it. GET /v1/tenant is therefore the most honest answer to “what does this key actually cover?” — it returns who you are speaking as.
ErreichbarkeitReachability
GET /v1/health ist der einzige unauthentifizierte Endpunkt der Produkt-API. Er beweist, dass der Dienst lebt — und sonst nichts. Schlägt er fehl, während der Mandantenabruf 401 liefert, liegt es am Schlüssel und nicht am Deployment.GET /v1/health is the only unauthenticated endpoint in the product API. It proves the service is alive — and nothing else. If it succeeds while the tenant fetch returns 401, the key is at fault, not the deployment.
Was 401 hier bedeutetWhat 401 means here
- Die API antwortet auf jeden Fehlschlag mit demselben Text: invalid API key. Missgebildet, unbekannt, widerrufen, falsches Geheimnis — ununterscheidbar, damit ein Angreifer nichts über existierende Schlüssel lernt.The API answers every failure with the same text: invalid API key. Malformed, unknown, revoked, wrong secret — indistinguishable, so an attacker learns nothing about which keys exist.
- Ein Gespräch eines anderen Mandanten ist 404, nie 403. Der Unterschied würde seine Existenz bestätigen.Another tenant's conversation is 404, never 403. The distinction would confirm it exists.
- Fehlt einem gültigen Schlüssel nur die Berechtigung, kommt 403 mit der genauen fehlenden Scope — das ist keine Information über fremde Daten, sondern über Ihren eigenen Schlüssel.When a valid key merely lacks a scope the answer is 403 naming the missing scope — that discloses nothing about other people's data, only about your own key.
API-SchlüsselAPI keys
Konsolen-Sitzung und API-Schlüssel sind absichtlich zwei verschiedene Mechanismen. Eine Sitzung gehört einem Menschen (kurzlebiges Cookie, Argon2-Passwort, Rolle); ein Schlüssel gehört einem System (langlebiges Bearer-Token, keine interaktive Anmeldung). Keiner der beiden funktioniert auf der Oberfläche des anderen, und die Testsuite prüft genau das.A console session and an API key are deliberately different mechanisms. A session belongs to a person (short-lived cookie, argon2 password, role); a key belongs to a system (long-lived bearer credential, no interactive login). Neither works on the other's surface, and the test suite asserts it.
Aufbau eines SchlüsselsAnatomy of a key
acp_sk_<key-id>_<secret> │ │ └─ 32 Byte token_urlsafe │ └─ 8 Byte Hex · 8-byte hex └─ Prefix
- Das Präfix acp_sk_ macht einen ausgelaufenen Schlüssel in Logs und für Secret-Scanner erkennbar. Es ist kein Schutz, es ist ein Frühwarnsystem.The acp_sk_ prefix makes a leaked key recognisable in logs and to secret scanners. It is not protection, it is early warning.
- Die Key-ID steckt im Klartext, damit die Auflösung O(1) ist und nicht gegen jeden bekannten Schlüssel gehasht werden muss.The key id is embedded in the plaintext, so lookup is O(1) instead of hashing against every known key.
- Gespeichert wird ein SHA-256-Hash, nicht Argon2 — und das ist Absicht. Argon2 kostet Rechenzeit, um menschlich gewählte Geheimnisse mit wenig Entropie gegen Brute Force zu schützen. Ein Schlüssel hat 256 Bit aus secrets.token_urlsafe; ihn zu raten ist unabhängig von der Hash-Geschwindigkeit aussichtslos, eine langsame KDF wäre nur eine selbstgebaute Ratenbremse. Verglichen wird in konstanter Zeit.A SHA-256 hash is stored, not argon2 — on purpose. Argon2's cost exists to defeat brute force against low-entropy, human-chosen secrets. A key is 256 bits from secrets.token_urlsafe; guessing it is infeasible regardless of hash speed, and a slow KDF would just be a self-inflicted rate limit. Comparison is constant-time.
- Der Klartext wird genau einmal gezeigt — bei der Ausstellung. Danach existiert nur noch der Hash.The plaintext is shown exactly once — at creation. After that only the hash exists.
- Widerrufen setzt active=False; der Eintrag bleibt bestehen, damit dieselbe Key-ID nie neu vergeben oder auf einen anderen Mandanten umgebogen werden kann.Revocation sets active=False; the record stays, so the same key id can never be re-issued or repointed at a different tenant.
BerechtigungenScopes
| ScopeScope | ÖffnetUnlocks | Ohne sieWithout it |
|---|---|---|
| read | /v1/tenant, /v1/conversations, /v1/knowledge, /v1/knowledge-search, /v1/webhooks, /v1/events, WebSocket-Stream/v1/tenant, /v1/conversations, /v1/knowledge, /v1/knowledge-search, /v1/webhooks, /v1/events, WebSocket stream | 401 — ohne gültigen Schlüssel geht gar nichts außer /v1/health401 — without a valid key nothing works except /v1/health |
| write | Nachricht senden, Wissensentwurf anlegen, Webhook registrierenSend a message, create a knowledge draft, register a webhook | 403 mit der Angabe, welche Scope fehlt403 naming the scope that is missing |
Ein über die API angelegter Wissenseintrag ist immer ein Entwurf. Veröffentlichen bleibt eine menschliche Entscheidung in der Konsole — auch mit write.Knowledge created through the API is always a draft. Publishing stays a human decision in the console — even with write.
Schlüssel testenTest a key
Zwei Prüfungen, die man nicht verwechseln sollte: die Formprüfung passiert hier im Browser und beweist nur, dass die Zeichenkette wie ein Schlüssel aussieht. Die Serverprüfung ruft GET /v1/tenant auf und beweist, dass er gilt.Two checks, not to be confused: the shape check runs here in the browser and proves only that the string looks like a key. The server check calls GET /v1/tenant and proves that it works.
Ausstellung & WiderrufIssuance & revocation
Kein Endpunkt — folgt mit M7No endpoint — lands in M7Weder Ausstellung noch Widerruf noch das Auflisten der Schlüssel eines Mandanten haben heute einen HTTP-Endpunkt. ApiKeyStore ist ein In-Memory-Store, den das Deployment befüllt; die Steuerebene, die daraus eine Selbstbedienung macht, ist ein M7-Thema. Diese Seite täuscht keinen Erfolg vor: sie zeigt Ihnen, was der Server tun würde, und überlässt Ihnen die Ausführung dort, wo der Store lebt.Neither issuance nor revocation nor listing a tenant's keys has an HTTP endpoint today. ApiKeyStore is an in-memory store the deployment populates; the control plane that turns this into self-service is an M7 item. This page fakes no success: it shows what the server would do and leaves execution where the store lives.
# ausstellen — der Klartext erscheint hier ein einziges Malissue — the plaintext appears here exactly once plaintext, key = app.state.api_keys.issue( "<tenant-id>", scopes=("read", "write"), label="<label>", ) # widerrufen — der Eintrag bleibt, nur inaktivrevoke — the record stays, merely inactive app.state.api_keys.revoke(key.key_id)
Bis M7: Klartext direkt in den Secret-Store des Integrators legen, niemals per E-Mail oder Chat weitergeben, und den Widerruf beim Personalwechsel einplanen.Until M7: put the plaintext straight into the integrator's secret store, never send it by email or chat, and plan revocation into staff changes.
WebhooksWebhooks
Wie die Plattform Ihren Systemen erzählt, was passiert ist. Der Umschlag trägt Kennungen und Typen, keine Inhalte — wer mehr braucht, ruft mit seinem Schlüssel zurück und bekommt genau das, wozu er berechtigt ist. Zustellung ist „fire and forget“ mit begrenzten Wiederholungen: ein hängender Empfänger kann ein laufendes Gespräch weder verzögern noch scheitern lassen.How the platform tells your systems what happened. The envelope carries ids and types, not content — a receiver that needs more calls back with its key and gets exactly what it is entitled to. Delivery is fire-and-forget with bounded retries: a hanging receiver can neither delay nor fail a live conversation.
Registrierte EndpunkteRegistered endpoints
Höchstens 20 Endpunkte pro Mandant. Zustellung fächert pro Endpunkt und Ereignis auf — eine unbegrenzte Liste wäre ein Verstärker. Ein identischer Eintrag wird mit 409 abgelehnt.At most 20 endpoints per tenant. Delivery fans out per endpoint per event — an unbounded list would be an amplification primitive. An identical entry is refused with 409.
Endpunkt registrierenRegister an endpoint
POST /v1/webhooks · writeEreignisse (nichts auswählen = alle)Events (select nothing = everything)
SignaturSignature
X-ACP-Event: turn.completed
X-ACP-Signature: t=<unix>,v1=<hmac-sha256>
HMAC-SHA256( secret, "{timestamp}.{body}" )
- Der Zeitstempel liegt im signierten Material. Eine mitgeschnittene Zustellung kann deshalb nicht später erneut gegen einen Empfänger abgespielt werden, der die Frische prüft — alles älter als 300 Sekunden gehört abgelehnt.The timestamp sits inside the signed material. A captured delivery therefore cannot be replayed later against a receiver that checks freshness — anything older than 300 seconds should be rejected.
- Das Signaturgeheimnis gehört einem Endpunkt eines Mandanten. Das Geheimnis des einen validiert nie die Zustellung des anderen.The signing secret belongs to one tenant's endpoint. One tenant's secret never validates another's delivery.
- Der Rumpf wird mit sortierten Schlüsseln und ohne Leerzeichen serialisiert, damit die Signatur reproduzierbar ist. Signieren Sie den Rumpf wie empfangen, nicht ein neu serialisiertes Objekt.The body is serialised with sorted keys and no whitespace so the signature is reproducible. Verify against the body as received, never a re-serialised object.
- Wiederholungen: bis zu drei Versuche mit exponentiellem Backoff. 4xx außer 429 wird nicht wiederholt — der Empfänger hat uns abgelehnt, ein weiterer Versuch wäre sinnlos.Retries: up to three attempts with exponential backoff. 4xx other than 429 is not retried — the receiver rejected us, another attempt would be pointless.
Prüfung auf EmpfängerseiteVerification on the receiving side
from acp.api.webhooks import verify_signature
verify_signature(
secret, body, request.headers["x-acp-signature"], now=time.time(),
)
Egress-RichtlinieEgress policy
Wenn ein Mandant eine URL wählt, wählt er ein Ziel, das die Plattform aus ihrer eigenen Vertrauenszone heraus anspricht. Ohne Richtlinie wäre das Server-Side Request Forgery: Cloud-Metadatendienste, Loopback-Adminports und interne Service-Meshes sind von einem Worker aus erreichbar, aus dem Internet nicht.When a tenant chooses a URL, it chooses a destination the platform will connect to from inside its own trust boundary. Without a policy that is server-side request forgery: cloud metadata services, loopback admin ports and internal service meshes are reachable from a worker but not from the internet.
- Nur https — kein http, kein anderes SchemaOnly https — no http, no other scheme
- Keine Loopback-, privaten, Link-Local-, Multicast- oder reservierten Adressen — auch nicht als ::ffff:127.0.0.1 verkleidetNo loopback, private, link-local, multicast or reserved addresses — not even disguised as ::ffff:127.0.0.1
- Kein localhost, metadata, *.internal, *.local, *.svc, *.cluster.localNo localhost, metadata, *.internal, *.local, *.svc, *.cluster.local
- Keine bekannten internen Dienstports (22, 23, 25, 135, 139, 445, 3306, 5432, 6379, 9200, 11211, 27017)No known internal service ports (22, 23, 25, 135, 139, 445, 3306, 5432, 6379, 9200, 11211, 27017)
- DNS wird bei jeder Zustellung erneut geprüft, nicht nur bei der Registrierung. DNS ist angreiferkontrolliert: ein Name, der bei der Registrierung öffentlich auflöst, kann später auf 127.0.0.1 zeigen. Nur bei der Registrierung zu prüfen hieße, gar nicht zu prüfen. Jede zurückgegebene Adresse muss öffentlich sein — eine private Antwort unter mehreren genügt einem Angreifer.DNS is re-checked at every delivery, not only at registration. DNS is attacker-controlled: a name that resolves publicly when registered can point at 127.0.0.1 later. Validating only at registration is validating nothing. Every returned address must be public — one private answer among several is enough for an attacker.
- Die einzige Ausnahme ist ACP_WEBHOOK_ALLOW_HOSTS: eine kommagetrennte Liste exakter Hostnamen, die trotz privater Auflösung erlaubt sind. Opt-in, niemals Voreinstellung, und wirklich nur für Deployments, die einen privaten Endpunkt brauchen.The single exception is ACP_WEBHOOK_ALLOW_HOSTS: a comma-separated list of exact hostnames permitted despite resolving privately. Opt-in, never the default, and genuinely only for deployments that need a private endpoint.
Eine abgelehnte URL — das ist die Funktion, nicht der FehlerA refused URL — that is the feature, not the fault
So sieht die Antwort aus, wenn der Server ein Ziel ablehnt. Der Text nennt die verletzte Regel und hängt die vollständige Richtlinie an, damit niemand raten muss:This is the response when the server refuses a destination. The text names the rule that was broken and appends the whole policy, so nobody has to guess:
HTTP/1.1 400 Bad Request
{"detail": "webhook url must use https. Policy: https only;
no loopback, private, link-local, multicast or reserved
addresses; no *.internal / *.local / localhost; no known
internal service ports; DNS re-checked at delivery time
(rebinding); override with ACP_WEBHOOK_ALLOW_HOSTS"}
Der erste Satz wechselt mit der verletzten Regel — etwa host 'foo.internal' is internal oder host 'x' resolves to non-public address 10.0.0.5. Die Richtlinie dahinter bleibt gleich.The first sentence changes with the rule that was broken — for example host 'foo.internal' is internal or host 'x' resolves to non-public address 10.0.0.5. The policy that follows stays the same.
Ereignis-KatalogEvent catalogue
Konfiguration & SicherheitConfiguration & security
Zwei Bereiche, absichtlich getrennt. Plattform ist die Haltung des gesamten Deployments und wird einmal beim Start gelesen. Mandant wird pro Mandant über ein explizites Mapping aufgelöst — nichts verändert os.environ, und viele Mandanten leben in einem Prozess mit eigener Region, eigenem Routing und eigenen ERP-Zugangsdaten.Two scopes, deliberately separated. Platform is the whole deployment's posture, read once at boot. Tenant is resolved per tenant from an explicit mapping — nothing mutates os.environ, and many tenants coexist in one process with independent region, routing and ERP credentials.
Die eine Regel, die alles andere entscheidetThe one rule that decides everything else
Ein Deployment gilt als live, solange ACP_ENV einen Wert trägt, der nicht exakt mock, test oder ci lautet; leer oder nicht gesetzt fällt auf mock zurück. Die Beweislast liegt beim Offline-Sein — früher hieß die Regel „live, wenn ACP_ENV == live“, und damit las sich ausgerechnet ACP_ENV=production als nicht live: der Geheimnis-Check beim Start entfiel, ACP_ALLOW_ANON war erlaubt, ein flüchtiger Signierschlüssel wurde erzeugt und das Sitzungscookie verlor sein Secure-Flag. Ein Tippfehler darf keine Produktionskonsole aufsperren.A deployment counts as live whenever ACP_ENV carries a value that is not exactly mock, test or ci; empty or unset falls back to mock. The burden of proof is on being offline — the rule used to be “live if ACP_ENV == live”, which made the most natural production value, ACP_ENV=production, read as not live: the boot-time secret check was skipped, ACP_ALLOW_ANON was permitted, an ephemeral signing key was minted and the session cookie lost its Secure flag. A typo must never unlock a production console.
Fail-closed: was passiert, wenn etwas fehltFail closed: what happens when something is missing
- Fehlende Plattform-Geheimnisse in live → kein Start. verify_boot_secrets() verlangt ACP_CONSOLE_SECRET (mindestens 32 Byte) und ACP_INGEST_TOKEN. Es sammelt alle Probleme und meldet sie gemeinsam, damit ein Deployment einmal repariert wird und nicht fünfmal.Missing platform secrets in live → no start. verify_boot_secrets() requires ACP_CONSOLE_SECRET (at least 32 bytes) and ACP_INGEST_TOKEN. It collects every problem and reports them together, so a deployment is fixed once instead of five times.
- ACP_ALLOW_ANON in live → Abbruch. Die Variable schaltet die Konsole unauthentifiziert — also anonymen Admin-Zugriff. In live wirft der Start AuthError: ACP_ALLOW_ANON must never be set when ACP_ENV=live. Offline wird sie als Warnung protokolliert, damit sie sichtbar bleibt.ACP_ALLOW_ANON in live → refused. The variable makes the console unauthenticated — that is anonymous admin access. In live, boot raises AuthError: ACP_ALLOW_ANON must never be set when ACP_ENV=live. Offline it is logged as a warning so it stays visible.
- Unkonfigurierter Ingest-Token → 503, nicht „offen“. Fehlt ACP_INGEST_TOKEN, schließt das die Voice-Worker-Brücke (503 ingest bridge is not configured), statt die Prüfung abzuschalten. Ein vergessener Token darf keine offene Tür sein.An unset ingest token → 503, not “open”. Without ACP_INGEST_TOKEN the voice-worker bridge closes (503 ingest bridge is not configured) instead of disabling the check. A forgotten token must never be an open door.
- Unkonfigurierter Metrik-Token → 503. Ohne ACP_METRICS_TOKEN antwortet /metrics mit 503 metrics endpoint is not configured — Betriebskennzahlen werden nicht ungeschützt veröffentlicht.An unset metrics token → 503. Without ACP_METRICS_TOKEN, /metrics answers 503 metrics endpoint is not configured — operational metrics are not published unguarded.
- Leer gilt als nicht gesetzt. FOO= und FOO= lesen sich beide als fehlend — auch eine leere Geheimnis-Datei. Eine versehentlich leere Zuweisung darf nie als „konfiguriert“ durchgehen.Blank counts as unset. FOO= and FOO= both read as missing — including an empty secret file. A stray empty assignment must never read as “configured”.
- Geheimnisse drucken sich nicht selbst. repr(Secret) ergibt Secret(ANTHROPIC_API_KEY=***). Der echte Wert kommt nur über .reveal() — grep-bar, damit ein Audit jede Stelle findet, an der ein Geheimnis die Hülle verlässt.Secrets refuse to print themselves. repr(Secret) gives Secret(ANTHROPIC_API_KEY=***). The real value comes only from .reveal() — grep-able, so an audit can find every place a secret leaves the wrapper.
Die _FILE-KonventionThe _FILE convention
ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key # gewinntwins ANTHROPIC_API_KEY=<…> # wird ignoriertignored
- Jede Variable dieser Seite darf als <NAME>_FILE geliefert werden. Die Datei schlägt den Inline-Wert — immer.Every variable on this page may instead be supplied as <NAME>_FILE. The file beats the inline value — always.
- Der Grund ist banal und wichtig: Docker- und Kubernetes-Geheimnisdateien landen nicht in docker inspect, nicht in der Prozessliste und nicht in einem Crash-Dump — Umgebungsvariablen schon.The reason is mundane and important: Docker and Kubernetes secret files never land in docker inspect, the process list or a crash dump — environment variables do.
- Eine nicht lesbare Datei ist ein harter Fehler (SecretError), kein stiller Fallback auf den Inline-Wert.An unreadable file is a hard error (SecretError), not a silent fallback to the inline value.
Referenz — alle ACP_*-VariablenReference — every ACP_* variable
| VariableVariable | UmfangScope | BedeutungMeaning | StandardDefault |
|---|---|---|---|
| ACP_ENV | PlattformPlatform | Haltung des Deployments. Entscheidet, ob echte Engine-Clients gebaut werden dürfen und ob Geheimnisse Pflicht sind. Alles außer mock/test/ci ist live; leer fällt auf mock zurück.Deployment posture. Decides whether real engine clients may be constructed and whether secrets are mandatory. Anything but mock/test/ci is live; empty falls back to mock. | mock |
| ACP_LOG_LEVEL | PlattformPlatform | DEBUG | INFO | WARNING | ERRORDEBUG | INFO | WARNING | ERROR | INFO |
| ACP_LOG_FORMAT | PlattformPlatform | console für Menschen, json für die Log-Pipeline in Produktion.console for humans, json for the log pipeline in production. | console |
| ACP_CONSOLE_SECRET | PlattformPlatform | HMAC-Signierschlüssel der Konsolen-Sitzungscookies. Mindestens 32 Byte; in live Pflicht. openssl rand -base64 48HMAC signing key for console session cookies. At least 32 bytes; mandatory in live. openssl rand -base64 48 | — (in live Pflicht)— (required in live) |
| ACP_INGEST_TOKEN | PlattformPlatform | Bearer-Token der Voice-Worker-Brücke (/api/ingest/*). Nicht gesetzt schließt die Brücke mit 503.Bearer token for the voice-worker bridge (/api/ingest/*). Unset closes the bridge with 503. | — (in live Pflicht)— (required in live) |
| ACP_METRICS_TOKEN | PlattformPlatform | Bearer-Token für /metrics (Header X-ACP-Token). Nicht gesetzt schließt den Endpunkt mit 503.Bearer token for /metrics (header X-ACP-Token). Unset closes the endpoint with 503. | — (Endpunkt geschlossen)— (endpoint closed) |
| ACP_CONSOLE_USERS | PlattformPlatform | Pfad zu console_users.json mit argon2-Hashes. Es gibt kein „keine Nutzer → Admin“.Path to console_users.json holding argon2 hashes. There is no “no users → admin”. | — |
| ACP_ALLOW_ANON | PlattformPlatform | 1 = Konsole ohne Anmeldung. Nur lokale Entwicklung; in live wird der Start verweigert.1 = console without login. Local development only; refused at boot in live. | — |
| ACP_DATA_ROOT | PlattformPlatform | Wurzel aller Mandantendaten: <root>/<mandant>/transcripts, /knowledge.Root of all tenant data: <root>/<tenant>/transcripts, /knowledge. | var/tenants |
| ACP_WEBHOOK_ALLOW_HOSTS | PlattformPlatform | Kommagetrennte Hostnamen, die trotz privater Auflösung als Webhook-Ziel erlaubt sind. Opt-in.Comma-separated hostnames permitted as webhook destinations despite resolving privately. Opt-in. | — |
| ACP_TENANT_ID | MandantTenant | Kennung des Mandanten im Einzelmandanten-Deployment. DNS-sicherer Slug.The tenant's id in a single-tenant deployment. DNS-safe slug. | default |
| ACP_TENANT_NAME | MandantTenant | Anzeigename des Mandanten. Rein kosmetisch.The tenant's display name. Purely cosmetic. | = Kennung= the id |
| ACP_REGION | MandantTenant | Deklarierte Region / Datenresidenz. Wird von GET /v1/tenant zurückgegeben; verschiebt selbst keine Daten.Declared region / data residency. Returned by GET /v1/tenant; does not move data by itself. | eu-central-1 |
| ACP_APPROVAL_EUR | MandantTenant | Betrag in EUR, ab dem eine Aktion blockierend auf menschliche Freigabe wartet.Amount in EUR above which an action blocks on human approval. | 100 |
| ACP_TURN_BUDGET_MS | MandantTenant | Latenzbudget eines Wortwechsels. Überschreitung erzeugt turn.budget_exceeded.Latency budget for one turn. A breach raises turn.budget_exceeded. | 800 |
| ACP_CONFIDENTIAL_FRONTIER | MandantTenant | Dürfen Frontier-APIs CONFIDENTIAL-Verkehr bedienen? Nur unter EU-Region + Zero-Retention-DPA.May frontier APIs serve CONFIDENTIAL traffic? Only under an EU-region + zero-retention DPA posture. | false |
| ACP_STT | MandantTenant | Spracherkennung: speechmatics | parakeet | mockSpeech-to-text: speechmatics | parakeet | mock | mock |
| ACP_TTS | MandantTenant | Sprachausgabe: azure | elevenlabs | cartesia | fish | piper | mockText-to-speech: azure | elevenlabs | cartesia | fish | piper | mock | mock |
| ACP_TRANSPORT | MandantTenant | Telefonie-Transport: livekit | mockTelephony transport: livekit | mock | mock |
| ACP_VOICE_MODE | MandantTenant | cascaded (STT→LLM→TTS) | realtime (Sprache zu Sprache)cascaded (STT→LLM→TTS) | realtime (speech to speech) | cascaded |
| ACP_REALTIME | MandantTenant | Realtime-Anbieter, nur bei ACP_VOICE_MODE=realtime: openai | gemini | mockRealtime provider, only when ACP_VOICE_MODE=realtime: openai | gemini | mock | mock |
Anbieter-Zugangsdaten — nur NamenProvider credentials — names only
Nur die Datei .env nennt jemals einen Anbieter beim Namen; die eigenen Schichten der Plattform tun es nie. Jeder Eintrag darf als _FILE geliefert werden — und sollte es in Produktion auch.Only the .env file ever names a vendor; the platform's owned layers never do. Every entry may be supplied as _FILE — and in production should be.
| BereichArea | VariablennamenVariable names | Benötigt, wennNeeded when |
|---|---|---|
| SprachmodelleLanguage models | ANTHROPIC_API_KEY · ANTHROPIC_BASE_URL · OPENAI_API_KEY · GOOGLE_API_KEY · XAI_API_KEY · OSS_BASE_URL | immer in live; der Router wählt pro Geschwindigkeit × Sensitivität. OSS_BASE_URL bedient die on-prem-Spur.always in live; the router picks per speed × sensitivity. OSS_BASE_URL serves the on-prem lane. |
| SpracherkennungSpeech-to-text | SPEECHMATICS_API_KEY · DEEPGRAM_API_KEY · PARAKEET_URL | wenn ACP_STT nicht mock istwhen ACP_STT is not mock |
| SprachausgabeText-to-speech | AZURE_SPEECH_KEY · AZURE_SPEECH_REGION · AZURE_SPEECH_ENDPOINT · ELEVENLABS_API_KEY · ELEVENLABS_BASE_URL · ELEVENLABS_VOICE_ID · CARTESIA_API_KEY · CARTESIA_BASE_URL · CARTESIA_VERSION · FISH_API_KEY · FISH_MODEL · FISH_VOICE_ID | wenn ACP_TTS nicht mock oder piper istwhen ACP_TTS is neither mock nor piper |
| RealtimeRealtime | OPENAI_REALTIME_URL · OPENAI_REALTIME_MODEL · GEMINI_LIVE_URL · GEMINI_LIVE_MODEL | wenn ACP_VOICE_MODE=realtimewhen ACP_VOICE_MODE=realtime |
| TelefonieTelephony | LIVEKIT_URL · LIVEKIT_API_KEY · LIVEKIT_API_SECRET | wenn ACP_TRANSPORT=livekitwhen ACP_TRANSPORT=livekit |
| ERP (Odoo)ERP (Odoo) | ODOO_URL · ODOO_DB · ODOO_USER · ODOO_API_KEY | sobald der Agent handeln und nicht nur auskunft geben sollas soon as the agent should act and not merely inform |
Diese Seite kennt keinen dieser Werte und fragt keinen ab. Sie nennt Namen — die Werte gehen von Ihrem Secret-Store direkt in das Deployment.This page knows none of these values and asks for none. It names variables — the values travel from your secret store straight into the deployment.