Server-side integrations (SCSG)
ScaiClip is end-to-end encrypted. The server holds ciphertext and opaque key wraps. That's what keeps it a "blind coordinator" — your keys aren't in your provider's database, they're in your users' devices.
That model works cleanly when the clients are user-owned devices. It's harder when a server-side service — a voice assistant, a hosted terminal, a hosted editor — wants to read what the user just copied. The service isn't a device the user owns. It doesn't hold a wrap. If we give it plaintext access, we break the whole crypto story.
SCSG (Service Consumer Session Grant) is how server-side services participate without breaking anything.
The idea#
When a server-side service starts a session with a user — a voice call, a shell attach, a doc open — it opens an SCSG grant. The user's own device gets a consent prompt, and if the user allows it, wraps the channel key for an ephemeral pubkey the service generates for exactly this session. The service now has decryption access for the duration of that session, and only for that session. When the session ends, the wrap is shredded and the ephemeral keypair is destroyed.
The user's device is still the sole holder of the private key that unlocks the channel. The service holds a copy of the content key (wrapped for its own ephemeral pubkey), which the user's device explicitly generated for it. Consent is per-session, granular, and revocable.
The consumer services#
Four ScaiLabs services currently use SCSG:
| Service | Session trigger | Duration cap |
|---|---|---|
| ScaiWave | A voice call opens | Until hangup |
| ScaiTerm | A remote shell attaches | Until disconnect |
| ScaiDrive-server | Server-side viewer opens a doc | Until viewer closes |
| ScaiEdit-server | Hosted edit session opens | Until save/close |
Your own third-party service can also consume SCSG — you register a service_id with ScaiLabs and issue OAuth2 client_credentials on that identity.
The five things every SCSG session gets right#
- Auth as a service principal. Not a user JWT — a
client_credentialstoken issued to the service identity. - An ephemeral crypto identity. A fresh X25519 keypair per session, in-memory only, destroyed at close.
- Explicit user consent. The user's device prompts, then wraps the channel key for the ephemeral pubkey.
- A hard duration cap. The grid closes the session automatically at expiry. There is no renewal — a long-running service opens a new session, the user consents again.
- Uniform audit. Every session lifecycle event lands in
audit_logwith a stablescaiclip.scsg.*action.
The verbs#
SCSG lives on its own WebSocket endpoint:
1 | |
Sub-protocol string: scsg/1. Auth: client_credentials bearer.
session.open (0x30)#
Service → grid.
1 2 3 4 | |
Errors: E_FORBIDDEN (tenant hasn't allowed this service), E_QUOTA_EXCEEDED (concurrent-session cap hit), E_UNKNOWN_SERVICE.
session.grant_request (0x31)#
Grid → user's devices.
Every online device the user owns receives this event when a service opens a session. The device renders a consent UI and either wraps the channel key (implicit approval) or issues session.deny.
session.ready (0x32)#
Grid → service.
Pushed when the wraps land. The service is now cleared to operate within granted_channel_ids and capabilities.
session.close (0x33)#
Service → grid.
Ends the session cleanly. Idempotent — a re-close returns the same closed response.
session.denied / session.expired / session.revoked (0x35 / 0x36 / 0x37)#
Grid → service push events. The service MUST tear down its state on receipt.
Capabilities#
scope.capabilities is a u32 bitfield. Ask for the minimum you need — the consent UI shows the user each capability in plain English, so overreach reads as untrustworthy.
| Bit | Name | Meaning |
|---|---|---|
| 0x01 | READ | Subscribe + fetch |
| 0x02 | WRITE | Publish |
| 0x04 | CLAIM | Consume one-shot clips |
| 0x08 | SCRATCH_READ | Read scratch objects |
| 0x10 | SCRATCH_WRITE | Upload scratch objects |
| 0x20 | PUBLIC_SHARE | Mint public links |
The CLAIM capability is additionally gated by the tenant's allow_agent_claim policy — a user's consent alone isn't enough; the tenant admin has to have opted the service in for one-shot consumption. Read/write don't require this.
Consent UI conventions#
When the user's device receives a session.grant_request, the consent UI SHOULD show:
- Who's asking. The service's
displayname, notservice_id. - Why. The
reasonfield, verbatim. Services SHOULD write reasons the user can act on ("call with alice@example.com", "editing quarterly-report.docx"), not internal handles. - What. The capabilities as plain-English lines.
- How long. The duration in wall-clock terms ("until 14:35 today" beats "for 4 hours").
- Fingerprint. A short hash of
ephemeral_sig_pubfor advanced users to verify against a service-side log. - Three options. Allow once, Allow always for this session (context-dependent — e.g. this voice call), Deny.
Rate limits and quotas#
- Per-
(tenant, service_id)token bucket onsession.open: 5/s with a 20-token burst. - Per-
(tenant, service_id)concurrent-session cap: 100 by default. A voice service handling more calls than that needs an uplifted quota. - Publish/fetch/claim within a session are rate-limited on the ephemeral device_id, same budget as a real user device.
Audit#
Every SCSG event lands in audit_log:
1 2 3 4 5 6 7 8 | |
Filter with the standard /v1/audit query API. service_id, grant_id, user_id, consenting_device_id, and the requested capabilities are all in the details field.
Integrating a new service#
If you're building a server-side service that wants clipboard access:
- Register a
service_idwith ScaiLabs. Provide a display name, a default capability set, and a static description of what the service does with the user's clipboard. - Get OAuth2
client_credentialson the service identity (via ScaiKey). - On session start (call, shell, doc-open), generate an X25519 KEM keypair + Ed25519 signing keypair in memory.
- Open an SCSG session with the ephemeral pubkeys.
- Wait for
session.ready. Operate withingranted_channel_idsandcapabilities. - On session end, close cleanly with
session.close. - Destroy the in-memory keypairs.
The consent UI, the wrap lifecycle, the audit trail, and the crypto shredding on close are the grid's responsibility. You only see SCWP verbs scoped to what the user allowed.
Why not just give the service the user's channel key?#
Because then a compromised service host leaks every clipboard for every user forever, and the user has no way to see it, revoke it, or bound it. The whole point of ScaiClip is that the crypto perimeter is tight and the audit surface is honest. SCSG keeps both intact.
What's next#
- Encryption & consent — the crypto that makes bounded grants safe.
- Scratch storage & public links — what services that need durable content or public shares should reach for.
- SCWP reference — the full wire verb registry.