Skip to content

ump_sdk.scope

module ump-sdk

Who is calling, and which clients and actions they may touch (IAP/OIDC stand-in, read from core).

Apps call await caller_scope(p, core_url=..., transport=...) with the caller’s Principal (forwarded X-UMP-Principal). It asks core GET /v1/whoami as that principal, caches the answer for a few seconds, and returns a :class:CallerScope:

  • allows(client_id) and filter(rows) for app-local data (sessions, scenarios, plan commitments);
  • require_client(client_id) and require(permission) raise a 403 FORBIDDEN problem whose detail is one plain sentence (extra.reason is client_scope or role).

Permissions mirror core’s whoami.permissions: create_drafts, review, approve, anchor, grant_override. Core stays the authority: it enforces the same rules on every forwarded write.

When core cannot be asked (not running, or a test double without /v1/whoami), the scope is verified=False and allows everything; core still refuses out-of-scope writes. This keeps app unit tests with mocked cores working, and is labelled in as_dict()["verified"].

Service principals (documented, used only for background or system work, never for a user’s request): service:planner (stale sweeps), service:e2e-seed (pool-only benchmark contributors in the e2e seed), service:<app> for scheduled jobs. Core treats service:* as all-client with draft and submit scope and never lets one approve.

class

class CallerScope

Fields

NameTypeDefaultDescription
principalstr
namestr | NoneNone
rolestuple[str, ...]()
clientsfrozenset[str] | NoneNoneNone means every client.
is_agentboolFalse
permissionsdict[str, Any]field(default_factory=dict)
verifiedboolTrue
Methods and properties (10)

all_clients property

all_clients: bool

read_only property

read_only: bool

who property

who: str

can method

def can(self, permission: str) -> bool

allows method

def allows(self, client_id: str | None) -> bool

filter method

def filter(self, rows: Iterable[Any], key: str = 'client_id') -> list[Any]

require_client method

def require_client(self, client_id: str | None, what: str = 'this client') -> None

require method

def require(self, permission: str, what: str | None = None) -> None

as_dict method

def as_dict(self) -> dict[str, Any]

init constructor

CallerScope(
self,
principal: str,
name: str | None = None,
roles: tuple[str, ...] = (),
clients: frozenset[str] | None = None,
is_agent: bool = False,
permissions: dict[str, Any] = dict(),
verified: bool = True,
) -> None

Source: packages/ump-sdk/src/ump_sdk/scope.py, line 59

function

async def caller_scope(
p: Principal | str,
*,
core_url: str | None = None,
transport: Any = None,
ttl: float = 10.0,
fresh: bool = False,
) -> CallerScope

The caller’s scope from core /v1/whoami (cached ttl seconds per principal; fresh skips the cache, e.g. after the caller created a client and was bound to it).

Source: packages/ump-sdk/src/ump_sdk/scope.py, line 144

function

def caller_scope_sync(
p: Principal | str,
*,
core_url: str | None = None,
transport: Any = None,
ttl: float = 10.0,
fresh: bool = False,
) -> CallerScope

Sync twin of :func:caller_scope for sync route handlers (planner).

Source: packages/ump-sdk/src/ump_sdk/scope.py, line 168

function

def invalidate_scope_cache() -> None

Source: packages/ump-sdk/src/ump_sdk/scope.py, line 136

function

def scope_from_whoami(d: dict[str, Any]) -> CallerScope

Source: packages/ump-sdk/src/ump_sdk/scope.py, line 122

function

async def require_client_scope(
p: Principal | str,
client_id: str | None,
what: str = 'this client',
*,
core_url: str | None = None,
transport: Any = None,
) -> CallerScope

Raise 403 unless client_id is in the caller’s scope (asks core again before refusing, so a client the caller just created counts). Returns the scope.

Source: packages/ump-sdk/src/ump_sdk/scope.py, line 194

NameTypeValueDescription
SERVICE_PRINCIPALS{'service:planner': 'planner background sweeps (stale scenarios after a model...