Appearance
Guardrails
Each of the promises on the overview is a mechanism in code. This page lists them one at a time, with how each one works, for anyone reviewing Kiwire's AI for security or privacy.
Conversations are held server-side
Both chat surfaces — the reporting assistant and KAIA — keep the conversation in the tenant's own database (kiwire_ai_conversations), keyed by tenant, admin and a chat id the browser chooses. The browser sends only the chat id and the new prompt. A history field in the request is ignored, so nobody can forge a prior "assistant" turn and steer the next answer, and a chat id copied from another admin finds nothing because the row is looked up by all three keys. At most 30 turns are echoed back to the model; threads idle for 30 days are pruned.
Tool results are sanitised before the model sees them
Every result a tool returns is scrubbed before the model sees it — once by the shim that lets the in-app assistants call MCP tools, and again inside each tool itself, so a client on the MCP endpoints gets the same treatment. Zone names, profile names, error messages and usernames come from the tenant's database and from guests' devices, and a crafted value could try to read as an instruction. The sanitiser strips control characters, collapses whitespace, neutralises code fences (a fence inside a value could otherwise end the tool result and start "assistant" text), caps every string at 200 characters and every key at 60, and masks identifiers as below. A value stays a value.
Identifiers are masked
The same sanitiser masks anything that identifies a person or a device, wherever it appears in a string. A MAC address keeps its OUI and loses the rest (AA:BB:CC:**:**:** — which vendor, not which device; Cisco-style aabb.****.**** likewise). An email keeps its first character and its domain (j***@domain). A run of ten or more digits — a phone, an IC or a card number — keeps its last three (timestamps are recognised and left alone). A field known to hold a username is masked outright to its first two characters and its length (jo***), because no pattern can tell a name or a room number from a plain word. KAIA's kaia_* views apply the same masking at the view definition, so its SQL path cannot return an unmasked username or MAC either. The model is instructed to copy a masked value exactly rather than guess, extend or "restore" it.
No raw SQL from the reporting assistant
The reporting assistant cannot write a query. It has exactly the seven tools of the Reporting MCP server: device, dwell-time, login-error, general-login, profile, reject-reason and session reports, each with a fixed parameter set of a period, optionally a zone, and for the session report a view. The queries behind them are the same ones the report pages run. KAIA's chat has five tools and marketing mode six (three of them shared with KAIA); not one of them takes a tenant parameter. The reporting assistant also refuses to quote figures it did not get from a tool: a reply with numbers and no tool call is retried once with the call made mandatory, and marked unverified if that still fails.
KAIA's SQL is guarded by shape
KAIA is the one assistant allowed to compose SQL, and only against the kaia_* views — a fixed set of per-tenant views over the tenant's own tables, already masked. Every statement passes a guard first, which rejects by shape rather than by parsing: one statement only (no semicolon), no comments, no @ variables, no backslashes, SELECT or WITH only, at most 4,000 characters; a fixed list of forbidden words (every DML and DDL verb, GRANT, SET, INTO/OUTFILE, SLEEP, BENCHMARK, SHOW, EXPLAIN, INFORMATION_SCHEMA, and so on); every table reference must be a kaia_* view or a CTE declared in the same query; any identifier that starts like a base table (kiwire…, audits, permissions, roles, migrations, jobs…) is refused; and a db.table qualifier that is not an alias declared in the query is refused so another database cannot be named. What passes gets a 200-row cap and a 5-second limit. An automated test pins these rules, so they cannot be loosened without the test failing.
KAIA reads as a database user that can only read
The guard is the application-side wall; kaia:provision builds the database-side one. It creates one MySQL user per tenant (kaia_ro_<tenant>) holding SELECT on that tenant's kaia_* views and nothing else — no base tables, no other database. Once a tenant's credentials are active, KAIA's queries run on that user instead of the application connection, so even a statement that somehow got past the guard would have nothing to reach. The readers are provisioned on this install's tenants (kaia:provision --status lists them), and a daily kaia:provision --apply pass re-grants new monthly views and picks up new tenants without ever rotating a live reader's password. The query runner is pinned to one tenant for the life of a request by whichever entry point starts it — the MCP endpoint or the admin page — and has no parameter that could name another.
KAIA never touches what it must not
KAIA's proposals come from a fixed catalog of thirteen entries: a profile's speed, quota and session limits; a zone's active state, journey or forced profile; a campaign's status or end date; a controller's opening hours or Wi-Fi on/off; a guest or voucher account's password reset; and acknowledge, snooze, won't-fix or dismiss on KAIA's own findings and suggestions. There is no free-form action.
An automated test pins the line, and fails if any of these ever appears in the catalog. It walks every catalog entry and asserts that neither the action key nor any parameter name contains any of: admin, role, permission, secret, password, passwd, credential, api_key, apikey, token, payment, curlec, licen, delete, remove, destroy, purge, radius, mfa, sso, ssh, backup, connector; that every action's permission is an edit-* permission a tenant admin can hold, or view-kaia for the finding verbs; and that the catalog holds at least ten actions, so it cannot be emptied to pass. The one allowed exception is account_reset_password, which the test requires to take only a username: the new password is generated at the moment of Apply, never chosen or seen by the model, shown once to the admin who clicked, and not written to the audit trail; the catalog refuses admin accounts and accounts a directory, OTP or social login signs in.
Every change needs a person
No assistant applies anything on its own. A KAIA proposal is held in the cache for 30 minutes, bound to the tenant and the admin who was asking; another admin, another tenant, or a stale id cannot apply it. Apply runs only from the confirm endpoint, re-checks that the admin holds the action's permission, goes through the same code path as a hand-made edit so the ordinary audit trail records the change, and writes one kaia.action row of its own. Over MCP, where no admin is present, a proposal is validated and described and nothing is stored; the scheduled heartbeat has no tools at all and narrates only from the evidence the detectors hand it. There is no autopilot mode. Marketing proposals follow the same rule with a state machine instead of a cache: approve, reject and execute are administrator clicks, the once-a-minute cron only runs proposals a person approved and only low-risk ones, and the sole approval-less path — tagging, when the tenant has opted in — writes rows to Kiwire's own segment table and nothing else.
The marketing consent gate fails closed
A marketing message reaches a guest only through a consent record: the consent gate reads the tenant's kiwire_consent_records and calls a guest reachable on a channel only with a granted, un-withdrawn, un-expired record for marketing_<channel>, matched by username or email. Where the consent module is not installed on a tenant, every guest is blocked on every channel with a reason that says so, and no proposal to send is filed. The gate runs when the proposal is filed and again for every guest inside the send job, so a withdrawal between approval and the send window is honoured. An SMS or WhatsApp send to more than 1,000 guests is high-risk by rule and the model is told not to propose it. Details on KAIA marketing.
Token caps bound the blast radius
Every request on Kiwire's shared key is metered per tenant per month and refused once the tenant's cap is reached — the tenant's own limit, otherwise the platform default (200,000 tokens on this install), never absent. Beyond cost, this bounds how much a runaway conversation, a misbehaving MCP client or an injection attempt that keeps a model calling tools can consume before it is stopped. KAIA's detectors watch the same counter and raise a finding as a tenant approaches its cap. A tenant on its own key is outside the cap and is billed by its provider directly; the key is stored encrypted in the tenant's own database and shown back only as its last four characters. See Keys, models and limits.
MCP is behind three switches and pinned to one tenant
An external client reaches any of the three MCP servers only with a tenant API key that carries the mcp scope, on a tenant Synchroweb has allowed MCP for, with the tenant's own switch for that server on; the KAIA endpoints additionally require KAIA to be on for the tenant. The key is resolved in the tenant's own database and the request is pinned to that tenant before any tool runs. Every server is read-only in effect: no tool writes a record, a KAIA action over MCP is a description, and a marketing proposal over MCP waits for an administrator inside Kiwire. Calls are counted per server per month so an unexpected client shows up. See MCP and external agents.