Security
Tenancy
Every business table carries organization_id as its second column, and reads go through TenantContext — the only source of "current organization" (decision D-002). The organization is never accepted from the client: the dashboard binds it from the session, the API binds it from the API key. Every tenant-scoped resource has a test proving org A cannot read, update or delete org B's row through every entry point.
RBAC
spatie/laravel-permission with teams keyed on organization_id (decision D-003). Permissions are <resource>.<verb> — recording.delete, assistant.publish. Roles are per organization, so being an admin of one tenant grants nothing in another.
Keys and secrets
- Private API keys are stored hashed. The plaintext exists once, in the create response.
- Public keys are stored in the clear, because they are public by definition, and carry origin and assistant restrictions since they ship inside browser bundles.
- Provider credentials use a separate encryption key (
CREDENTIAL_ENCRYPTION_KEY, decision D-004), notAPP_KEY, so rotating the application key does not require re-encrypting every tenant's provider secrets. - Nothing logs a credential, an
Authorizationheader or a raw webhook body containing one:App\Support\Redactorandpackages/contracts/src/redact.tsexist for this.
SSRF
Every tenant-supplied URL — tool endpoints, webhook targets, MCP servers, knowledge sources — is checked before it is dereferenced, and again on every redirect hop. Private ranges, link-local addresses and the cloud metadata endpoint are refused.
Sandboxing
bash, computer and textEditor tools never execute in the gateway process. They run in a container with no network by default, a CPU and memory cap, a read-only root filesystem and a bounded output size.
Prompt injection
Untrusted content — tool results, MCP results, retrieved chunks, caller speech — is wrapped before it reaches the model, so text inside it cannot be read as an instruction from the operator.
Client auth
Browsers never hold a private key. See Authentication.
Audit log
audit_logs is insert-only and records who did what to which resource, from where. Credential values are excluded from the recorded diff.
Data retention
Per-organization retention, set separately for recordings, transcripts, events, logs and structured outputs, plus a zero-data-retention mode that keeps no audio or transcript beyond the call. Retention runs nightly; zero-data-retention with no delay runs inline at finalisation.
Recording consent is an announcement, not consent
An announcement can be played. Recording starts before it is spoken, and a refusal cannot be honoured — the gateway can only ever report announced, because nothing collects an acknowledgement. Read consent_state: "announced" as "a notice was made". See AI disclosure & consent.
Compliance
WARNING
SOC 2, HIPAA and PCI modes are designed and the controls above are the technical half of them. No certification is claimed.
SAML SSO is not implemented — the SSO service says so in as many words, and there is deliberately no route — for want of an XML-DSig library. OIDC is implemented. SCIM provisions users but not groups.