Skip to documentation
Documentation

Conversation authentication

One agent and one number, with account-scoped conversation principals.

Keiki already isolates conversation history, state and persistence. Sharing an agent and number does not mean sharing an application's account credentials. Use one hosted agent for the app, with one conversation per verified sender:

text
one number → one agent → Alice's conversation → Alice's account principal
                     → Bob's conversation   → Bob's account principal

There is no sender-to-agent router, per-account deployment, shared-gateway registration endpoint or additional Keiki routing schema. Separate agents are only necessary when accounts need independently deployed configurations.

Reuse the identity webhook

Configure identity: { webhook_url, secret } on the hosted deployment. The application verifies the raw callback with verifyWebhook from @onkeiki/agents/cloud, then resolves the verified sender to its own account:

JSON
{ "userId": "app-user-id", "role": "owner", "scopes": ["mail:read"], "ttlSeconds": 0 }

Use ttlSeconds: 0 when revocation must take effect on the very next turn. Never derive authorization from a model-provided account ID, a matching email string, or an unchecked phone number in tool arguments.

Photon carries its provider-verified sender, destination line and DM status on the platform dispatch context. Its identity callback identifies the sender as { channel: "photon", teamId: "<line>", userId: "<sender>" }, with addressing.isDm. The optional messages array contains provider-derived IDs and text for a linking challenge; the model cannot choose those fields.

API turns use { channel: "api", conversationId: "<client-conversation-id>" }. The app must authorize or bind that opaque conversation ID server-side before calling completions. Possession of an ID or a user ID embedded in a prompt is not account authentication.

OAuth linking and deterministic replies

OAuth authenticates the app account, not control of an iMessage identity. Use a short-lived, single-use link and complete a challenge from the original verified sender before binding it. Keep provider OAuth tokens in the application's own credential store, scoped to the authenticated user.

For an unknown, unlinked, revoked or disabled sender, return an identity answer with a deterministic reply:

JSON
{ "userId": null, "scopes": [], "ttlSeconds": 0, "reply": "Connect your account at https://app.example/connect/..." }

This ends the inbound message before old history, suspended tool runs or the model are read. Photon delivers the reply through normal transport opt-out, inbound-first and volume controls. API turns return it as a completion with zero model tokens. Identity service errors fail closed rather than exposing a previously authenticated conversation's history. Replies are bounded to 2,000 characters and are never cached.

Identity-configured Photon conversations require explicit DM evidence; groups and unknown space types cannot expose private account data. Existing line routing and delivery are unchanged when no identity webhook is configured.

Authorize tools independently

Declare policy.requiredScopes on account tools. Custom webhook calls are HMAC signed and contain platform-derived agent, conversation, channel and principal context. The tool server must also re-check the current account binding, scopes and resource ownership, including for queued turns after disconnect. Never let the model select a credential or impersonate another principal.

The same tool-context boundary resolves principals for hosted/external harnesses and approval handlers. Secrets stay out of prompts and code-tool runners. Signed webhooks omit the legacy X-Orchid-Api-Key header; unsigned integrations retain their prior behavior. Existing Slack/Teams identity hooks and per-sender MCP credentials continue to work. This change does not add Photon MCP credential storage or automatically link channel histories.

Account deletion must revoke that account's bindings and credentials, not delete the shared agent. Never rebind a recycled identity to another account while its old transcript remains accessible; retain a non-authorizing tombstone or perform an explicit history-isolating cleanup. Keiki retention/deletion policies still apply independently of application authorization.

Conversation principals do not implicitly control agent-wide state. With an identity webhook configured, shared note writes require agent:configure and peer-agent calls require agent:delegate. Do not grant those scopes to ordinary account users, or store private account facts in shared agent/group notes.