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:
one number → one agent → Alice's conversation → Alice's account principal
→ Bob's conversation → Bob's account principalThere 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:
{ "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:
{ "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.