// Companion capability scopes — the SDK's self-contained copy (no $lib // imports; same doctrine as protocol.ts). The server's source of truth is // src/lib/types/companion-scopes.ts; protocol.drift.test.ts fails if the two // diverge — extend both together. // // Why the SDK exports these: a host app configuring a key, gating a feature // on `hello.ack.grantedScopes`, or explaining a `403 missing scope` needs the // vocabulary as types and constants, not prose. Typos in scope strings become // compile errors instead of runtime 403s. /** Every capability scope a Personal Access Token / OAuth grant can carry. */ export const COMPANION_SCOPES = [ 'chat', // text turns 'voice', // TTS / STT 'call', // realtime WebRTC voice session 'call.record', // record a whole call, both voices, provider-side (startCall/connectCall `record`) and fetch/delete it (getCallRecording / deleteCallRecording). Not granted by default: the agent's `recording` switch in the dashboard grants it. 'call.video', // a talking-face video avatar on a realtime call (startCall `avatar: { faceId }` → a Simli session token). Not granted by default: a project gets it once it has a ready face (built with its admin key); withhold it per reader with `withhold_scopes`. 'files', // multimodal file in/out (vision / image) 'events.subscribe', // receive the outbound companion event stream 'worldstate.write', // push inbound live world-state / context events 'ui.render', // let the companion render Instant UI surfaces on the host (display + forms; not sensitive) 'data.activity', // receive metadata-only Data-plane activity frames (capability + outcome + timing; never rows or intent values; not sensitive) 'chat.instructions', // carry developer-authored per-turn instructions on a text turn (sendText `instructions`) — joined to the prompt, never scored as user speech. Not granted by default, and NOT obtainable by naming it at mint: only the agent-template flag `instructions: true` (Dashboard → Agents, or PATCH the agent) puts it on newly minted session tokens. 'memory.read:app', // read this app's own memory namespace 'memory.write:app', // write into this app's own memory namespace 'memory.read:core', // read the shared global brain (SENSITIVE) 'memory.write:core', // write into the shared global brain (SENSITIVE) 'skills.execute', // run the user's installed skills (SENSITIVE) 'wallet.read', // READ-ONLY wallet: balance + own deposit address (receive-only; not sensitive). GET /api/companion/wallet (SDK: getWallet(), 0.20.0+) or ask the companion's wallet tools. 'wallet.spend', // spend from the Care Wallet (SENSITIVE) 'social.message', // message friends via A2A (SENSITIVE) // ── Representative ("on-behalf-of") plane ───────────────────────────── 'represent', // open visitor-facing representative sessions (SENSITIVE) 'expose:knowledge', // let the representative draw on the owner's knowledge base (SENSITIVE) 'expose:facts', // widen exposed facts past the identity/preference floor (SENSITIVE) 'represent:pair', // let a representative pair a visitor (also a Pouchy user) as a friend (SENSITIVE) 'represent:remember' // let a representative keep durable per-visitor notes across visits (SENSITIVE) ] as const; export type CompanionScope = (typeof COMPANION_SCOPES)[number]; /** I/O modalities a key can carry alongside its scopes. */ export const COMPANION_MODALITIES = ['text', 'voice', 'call', 'files'] as const; export type CompanionModality = (typeof COMPANION_MODALITIES)[number]; /** Sensitive scopes — never granted by default; the user opts in at key * creation, and execution still passes a confirm on a Pouchy-controlled * surface (see `companion.confirm_request`). Not every non-default scope is * opted into this way: `chat.instructions` is granted ONLY by the agent * template's `instructions: true` flag, and a mint that names it is silently * narrowed (CR-927). */ export const SENSITIVE_SCOPES: ReadonlySet = new Set([ 'memory.read:core', 'memory.write:core', 'skills.execute', 'wallet.spend', 'social.message', 'represent', 'expose:knowledge', 'expose:facts', 'represent:pair', 'represent:remember' ]); /** The representative ("on-behalf-of") plane — a distinct axis from the * execution scopes: one lets the companion ACT for the owner, the other lets * it REPRESENT the owner to an outside visitor. `represent` is the umbrella; * the `expose:*` / `represent:*` scopes only mean anything alongside it. */ export const REPRESENT_SCOPES: ReadonlySet = new Set([ 'represent', 'expose:knowledge', 'expose:facts', 'represent:pair', 'represent:remember' ]); /** What a freshly-generated key carries unless the user opts into more: a * chat/play companion confined to its own app memory namespace. */ export const DEFAULT_SCOPES: readonly CompanionScope[] = [ 'chat', 'voice', 'call', 'files', 'events.subscribe', 'worldstate.write', 'memory.read:app', 'memory.write:app' ]; export const DEFAULT_MODALITIES: readonly CompanionModality[] = ['text', 'voice']; export function isSensitiveScope(s: CompanionScope): boolean { return SENSITIVE_SCOPES.has(s); } export function isRepresentScope(s: CompanionScope): boolean { return REPRESENT_SCOPES.has(s); } /** Does a granted scope set (e.g. `hello.ack.grantedScopes`) satisfy a * required scope? Exact-match for v1 (no hierarchical wildcards). Use this for * an EXACT-grant check ("was this scope explicitly granted?"). To gate a * feature on whether an API CALL will succeed, prefer `grantsScope`, which * also honours scope subsumption. */ export function hasScope(granted: readonly string[], required: CompanionScope): boolean { return granted.includes(required); } /** Scope subsumption: for a required scope, the OTHER granted scopes that also * satisfy it on the server. v1 has exactly one — `wallet.spend` can already see * the balance, so it also satisfies a `wallet.read` (mirrors the server gate at * `GET /api/companion/wallet`). Keep in lockstep with the server if more are * added. */ const SCOPE_SUBSUMED_BY: Partial> = { 'wallet.read': ['wallet.spend'] }; /** Like {@link hasScope}, but honours the server's scope subsumptions — so a key * granted only `wallet.spend` still `grantsScope(g, 'wallet.read')` (the API * would serve the balance). Gate a read-only affordance (e.g. showing the * wallet balance via `getWallet()`) on THIS, not `hasScope`, or the UI hides a * feature the server would allow. For non-subsumed scopes it is identical to * `hasScope`. */ export function grantsScope(granted: readonly string[], required: CompanionScope): boolean { if (granted.includes(required)) return true; const implied = SCOPE_SUBSUMED_BY[required]; return implied ? implied.some((s) => granted.includes(s)) : false; }