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 asprincipal. The store filters reads and refuses cross-owner mutations.
Configuration Options
Every method below gained an optionalprincipal 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.Backward Compatibility
Existing on-diskjobs.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
Reuse the gateway-resolved identity
Reuse the gateway-resolved identity
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.Match the mutation to the read
Match the mutation to the read
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.Treat the direct id as a capability
Treat the direct id as a capability
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.Related
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.
