Skip to main content
Pass a principal when creating a scheduled automation or a suggestion, and every list / accept / dismiss / remove call is automatically scoped to that owner — one gateway user’s jobs are invisible to another’s.

Quick Start

1

Create a job for a specific user

Set principal on a ScheduleJob. store.list(principal="alice") returns only Alice’s job, while store.list() returns everything.
2

Scope a suggestion to a user

Set principal on a Suggestion. Each owner sees only their own pending queue.
3

Backward compatible (single-user)

Omit principal everywhere. Behaviour is unchanged — the store is one shared pool.

How It Works

The gateway resolves the caller to a stable identity, then threads that string into every store call as principal. The store filters reads and refuses cross-owner mutations.

Configuration Options

Every method below gained an optional principal filter. principal=None is the default and preserves global / single-tenant behaviour.

Suggestion field

SuggestionStore methods

ScheduleJob field

FileScheduleStore / ConfigYamlScheduleStore methods


Semantics Reference


Choosing When to Scope


Common Patterns

Thread the gateway-resolved identity down; the core carries the string and enforces the filter but never resolves identity itself.
get(job_id) and remove(job_id) are intentionally NOT scoped. Direct-id access is capability-shaped — the id is an opaque 12-character hex (uuid4().hex[:12]). The guessable surface is the human-readable name, so scoping the name-based path (get_by_name / remove_by_name) is what closes the leak.
Any store implementing ScheduleStoreProtocol MUST accept principal: Optional[str] = None on list, get_by_name, and remove_by_name for isinstance(store, ScheduleStoreProtocol) (runtime-checkable) to keep passing. It MUST also honour the “cross-owner is invisible” contract: hide other owners’ items on filtered reads, and refuse cross-owner mutations by returning None / False.

Backward Compatibility

Existing on-disk jobs.json, config.yaml, and suggestions.json files load unchanged. principal is omitted from to_dict() output when None, and every store method defaults principal to None — the pre-scoping global behaviour.

Best Practices

Thread the identity the gateway already resolved (IdentityResolverProtocol output) down as principal. Do not invent a new identity in the store layer — the core carries the string and enforces the filter, it does not resolve identity.
The store is designed for one file with per-record ownership, not one file per user. Pass principal on each record instead of creating a separate store per user.
If you list per-principal in the UI, always mutate per-principal in the callback. Never accept a naked sug_id from an untrusted UI without re-scoping to the caller’s principal.
get(id) and remove(id) are intentionally unscoped. Surface a job’s opaque id only to its owner — the id itself is the access token for direct-id operations.

Automation Suggestions

Per-user consent-first automation proposals.

Async Agent Scheduler

The scheduler that owns these jobs.

Scheduled Run Policy

Safety gate for unattended runs — a different layer.

Bot Command Access Control

Who may call /automations at all.