Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Modellen Tools & Services
Oplossingen
Organisaties Ontwikkelaars Internet Service Providers Managed Service Providers AI-in-a-Box
Kenniscentrum
Ondersteuning Documentation Blog Downloads
Bedrijf
Over ons Onderzoek Vacatures Investeren Contact
Inloggen

Integrating a service consumer

Your service — a hosted document editor, a voice assistant, a remote terminal, whatever — needs clipboard access. This walkthrough gets you from "we want to build this" to "user consented, we're operating on their clipboard, we close cleanly."

What you need before you start#

  • A registered service_id. For ScaiLabs services, this is baked into the SCSG registry. Third parties email platform@scailabs.ai — the flow lands an entry in the static registry plus a ScaiKey OAuth app.
  • OAuth2 client_credentials from ScaiKey against your service identity. Follow the ScaiKey docs — the shape you get is a JWT with sub = cli_<service_id>_<instance> and token_type = "client_credentials".
  • The user's tenant must have opted your service in (allow_scsg_<service_id> set on mod_scaiclip_tenant_policy). Tenant admins do this from the admin UI.

Choose your capabilities#

Ask for the minimum you need. Users see every capability in the consent prompt — overreach reads as untrustworthy.

Bit Name When you need it
0x01 READ Any service that reads what the user copied
0x02 WRITE Any service that puts things back into the clipboard
0x04 CLAIM You consume ONE_SHOT clips (secrets, MFA codes)
0x08 SCRATCH_READ You read large files from central storage
0x10 SCRATCH_WRITE You upload large content back to central storage
0x20 PUBLIC_SHARE You mint public share URLs on behalf of the user

Combine with |. Example: a voice assistant that reads and writes: CAP_READ | CAP_WRITE = 0x03.

Use the reference client#

The Python reference client (modules.scaiclip.services.scsg_client.SCSGClient in the ScaiGrid repo, distributable as a package once we cut one) wraps the WS protocol so you don't touch CBOR framing.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from modules.scaiclip.services.scsg_client import SCSGClient, SCSGSessionError
from modules.scaiclip.services.scsg_registry import CAP_READ, CAP_WRITE

SCAIGRID_URL = "https://scaigrid.scailabs.ai"
SCSG_JWT     = "..."  # your client_credentials token

async def handle_voice_call(user_id: str, peer_email: str):
    async with SCSGClient(base_url=SCAIGRID_URL, token=SCSG_JWT) as client:
        grant = await client.open_session(
            user_id=user_id,
            reason=f"voice call with {peer_email}",
            duration_max_s=3600,
            capabilities=CAP_READ | CAP_WRITE,
        )
        print(f"grant {grant.grant_id} pending — user has "
              f"{(grant.consent_deadline_ms - int(time.time()*1000))//1000}s "
              f"to answer")

        try:
            ready = await client.wait_ready(grant.grant_id, timeout_s=60)
        except SCSGSessionError as exc:
            print(f"user denied or timed out: {exc.reason}")
            return

        print(f"got clipboard for {len(ready.granted_channel_ids)} "
              f"channel(s), expires at {ready.expires_at_ms}")

        # ... do voice-assistant things with the user's clipboard ...

        await client.close_session(
            grant.grant_id, reason="hangup",
        )

When you call session.open, the grid pushes session.grant_request to every online device the user owns. The T1 client (native app, browser extension, mobile app) renders a prompt.

Craft the reason field like a real user will read it — because they will. Compare:

  • Bad: session=abc123 caps=0x03 scope=user_default
  • Good: call with alice@example.com

Every user-facing field:

  • service_display_name — pulled from the registry ("ScaiWave (voice)"). You can't change this per-request.
  • reason — free text you set at session.open. Truncated to 512 chars.
  • capabilities — rendered as human-readable lines ("Read what you copied", "Send things back to your clipboard").
  • duration_max_s — rendered as wall-clock ("until 15:42 today" beats "for 3600 seconds").

Handling the terminal states#

Four ways a session ends. Your service must handle all of them.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
try:
    ready = await client.wait_ready(grant.grant_id)
    # session active
except SCSGSessionError as exc:
    if "denied" in exc.reason:
        # user tapped Deny
        show_ui("Access declined")
    elif "expired" in exc.reason:
        # consent window elapsed with no answer
        show_ui("Timed out waiting for consent")
    elif "revoked" in exc.reason:
        # user revoked from another device mid-session
        show_ui("Access revoked")

Grid also pushes session.expired mid-session at TTL. Your client's pump loop handles that — same shape as revoke.

Multi-session per process#

The client tracks multiple grants concurrently. Open one per user session:

python
1
2
3
4
5
call_a_grant = await client.open_session(user_id="usr_alice", )
call_b_grant = await client.open_session(user_id="usr_bob",   )

await client.close_session(call_a_grant.grant_id, reason="hangup")
# call_b_grant still active

One WS connection, many grants. Reconnect handling is deliberately not built into the reference client — production services should wrap it themselves with backoff.

Rate limits#

You get 5 session.open calls per second per (tenant, service_id) with a 20-token burst. Beyond that: E_RATE_LIMITED with retry_after_ms.

You get 100 concurrent sessions per (tenant, service_id). Beyond that: E_QUOTA_EXCEEDED { scope, used, limit }.

If you routinely hit these, ask for an uplift.

Audit trail#

Every session you open lands in the tenant's audit_log:

  • scaiclip.scsg.session_opened at request time.
  • scaiclip.scsg.session_granted when the user consents.
  • scaiclip.scsg.session_closed (or denied / expired / revoked) at the end.

Tenant admins can grep for service_id = "your_service" to see every session your service has opened.

Common gotchas#

  1. You forgot to opt in the tenant. session.open returns E_FORBIDDEN with service_id + tenant_id in detail. Ask the tenant admin to flip allow_scsg_<your_service>.
  2. The user has no online device. E_NO_ONLINE_DEVICE. You cannot consent-prompt a user who isn't running any ScaiClip client. Surface a UX like "please open ScaiClip on your phone / desktop".
  3. You requested CLAIM but the tenant hasn't opted in for one-shot consumption. session.open succeeds; the first clip.claim you attempt returns E_FORBIDDEN. Read requires opt-in of allow_agent_claim at the tenant.
  4. You touch a channel outside granted_channel_ids. E_OUT_OF_SCOPE. The grant scope is enforced on every downstream verb.
  5. You forgot to close. Grid times out at duration_max_s and emits session.expired, but leaving stale sessions eats your concurrent-session budget. Always close.
Updated 2026-07-12 00:26:08 View source (.md) rev 5