---
title: SCSG verb reference
order: '21'
summary: "Service Consumer Session Grant \u2014 endpoint, sub-protocol, verbs, capability\
  \ bitfield, service registry."
path: reference/scsg
status: published
---

SCSG is the sub-protocol server-side services (ScaiWave, ScaiTerm, ScaiDrive-server, ScaiEdit-server) use to obtain bounded, user-consented clipboard access. It runs on its own WebSocket endpoint separate from the user-facing `scaiclip/1`.

For the conceptual model, see [Server-side integrations](../concepts/service-consumers.md).

## Endpoint

```
wss://scaigrid.scailabs.ai/v1/modules/scaiclip/scsg/ws
```

Sub-protocol string: `scsg/1`.

**Auth**: OAuth2 `client_credentials` JWT via `?token=<jwt>` query param or `Authorization: Bearer <jwt>` header. The token's `sub` claim (== `client_id`) must correspond to a registered `service_id` in the SCSG registry.

**Naming convention** (M1): `client_id` starts with `cli_<service_id>_…` (e.g. `cli_scaiwave_prod_a1b2c3`). Future ScaiKey versions will carry `service_id` as an explicit claim.

## Verbs

### Client (service) → grid

| Type | Verb | Body | Response |
|---|---|---|---|
| `0x60` | `session.open` | `{1: session_id, 2: user_id, 3: ephemeral_kem_pub, 4: ephemeral_sig_pub, 5: {1: channel_ids | 'user_default', 2: capabilities}, 6: duration_max_s, 7: reason}` | `{1: grant_id, 2: 'pending_consent', 3: consent_deadline_ms}` |
| `0x61` | `session.close` | `{1: grant_id, 2: reason}` | `{1: 'closed', 2: closed_at_ms}` |
| `0x63` | `session.list` | `{}` | `{1: [{1: grant_id, 2: service_id, 3: user_id, 4: status, 5: capabilities, 6: expires_at_ms}]}` |

### Grid → service push

| Type | Event | Body |
|---|---|---|
| `0xA1` | `session.ready` | `{1: grant_id, 2: [channel_id_bytes], 3: expires_at_ms, 4: consenting_device_id}` |
| `0xA2` | `session.denied` | `{1: grant_id, 2: reason}` |
| `0xA3` | `session.expired` | `{1: grant_id}` |
| `0xA4` | `session.revoked` | `{1: grant_id, 2: reason}` |

### User-side (scaiclip/1 endpoint)

| Type | Verb | Body | Response |
|---|---|---|---|
| `0x62` | `session.deny` | `{1: grant_id, 2: reason}` | `{1: 'denied'}` |
| `0xA0` | `session.grant_request` (grid → user) | `{1: grant_id, 2: service_id, 3: reason, 4: {1: channel_ids, 2: capabilities}, 5: duration_max_s, 6: ephemeral_sig_pub, 7: consent_deadline_ms, 8: display_name}` | — |

User consents by installing wraps for the ephemeral device via the standard `key.wraps.put` verb targeting `dev_scsg_<service_id>_<session_id>`. The grid's implicit-consent hook detects this pattern and transitions the grant to `active`.

## Capabilities bitfield

`scope.capabilities` is a u32 with these flags:

| Bit | Name | Description |
|---|---|---|
| `0x01` | READ | Subscribe + fetch (`clip.fetch`, `channel.sync`) |
| `0x02` | WRITE | Publish (`clip.publish`) |
| `0x04` | CLAIM | Consume one-shot clips (subject to tenant policy) |
| `0x08` | SCRATCH_READ | Read scratch objects |
| `0x10` | SCRATCH_WRITE | Upload scratch objects |
| `0x20` | PUBLIC_SHARE | Mint public share links |

Services SHOULD request the minimum bit set for their function. Consent UIs enumerate capabilities in human terms.

## Service registry

Only pre-registered `service_id` values may open sessions.

| `service_id` | Display | Default capabilities |
|---|---|---|
| `scaiwave` | ScaiWave (voice) | READ · WRITE |
| `scaiterm` | ScaiTerm (terminal) | READ · WRITE · CLAIM |
| `scaidrive` | ScaiDrive (files) | READ · SCRATCH_READ · SCRATCH_WRITE |
| `scaiedit` | ScaiEdit (editor) | READ · WRITE · SCRATCH_READ |

Unlisted `service_id` → `E_UNKNOWN_SERVICE`.

Third-party service integration requires a manual registration step (email platform@scailabs.ai) plus a ScaiKey OAuth app.

## Tenant policy

Each service must be opted in by the tenant admin:

| Column in `mod_scaiclip_tenant_policy` | Default |
|---|---|
| `allow_scsg_scaiwave` | `false` |
| `allow_scsg_scaiterm` | `false` |
| `allow_scsg_scaidrive` | `false` |
| `allow_scsg_scaiedit` | `false` |
| `allow_agent_claim` | `false` — gates the CLAIM capability regardless of service |
| `max_session_duration_s` | `14400` (4 hours) — clamps `duration_max_s` |

Missing tenant policy row → all flags default false. Attempting `session.open` for a service the tenant hasn't opted into → `E_FORBIDDEN`.

## Rate limits

| Limit | Setting | Default |
|---|---|---|
| `session.open` per `(tenant, service_id)` | `scaiclip_scsg_session_open_per_s` / `_burst` | 5/s, burst 20 |
| Concurrent sessions per `(tenant, service_id)` | `scaiclip_scsg_max_concurrent_per_service` | 100 |
| Consent window | `scaiclip_scsg_consent_window_s` | 60 s |
| Absolute max session duration | `scaiclip_scsg_absolute_max_duration_s` | 4 h |
| Expiry sweep cadence | `scaiclip_scsg_expiry_sweep_s` | 30 s |

## Audit stream

Every SCSG lifecycle event lands in `audit_log`:

```
scaiclip.scsg.session_opened
scaiclip.scsg.session_granted
scaiclip.scsg.session_denied
scaiclip.scsg.session_closed
scaiclip.scsg.session_expired
scaiclip.scsg.session_revoked
scaiclip.scsg.out_of_scope_denied
scaiclip.scsg.unknown_service
```

Every row's `details` field carries `service_id`, `grant_id`, `user_id`, `capabilities`, `duration_max_s`, and where applicable `consenting_device_id`.

## Reference client

The Python reference client at `modules/scaiclip/services/scsg_client.py` (in the ScaiGrid repo) wraps the WS protocol so consumer services call `open_session` / `wait_ready` / `close_session` async methods without touching CBOR.

## Related

- [Server-side integrations](../concepts/service-consumers.md) — conceptual overview.
- [Encryption & consent](../concepts/encryption.md) — how SCSG preserves E2E.
- [Integrating a service consumer](../tutorials/integrate-a-service-consumer.md) — end-to-end walkthrough.
- [Error codes](./errors.md).