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.
Auto-scoping from the session context
On a bot gateway (Telegram, Discord, Slack, or WhatsApp adapter) every turn installs aSessionContext carrying a unified_user_id. The agent tools and the gateway bridge read that value automatically, so isolation is on by default — you never thread identity by hand.
These surfaces now default principal from SessionContext.unified_user_id:
schedule_add,schedule_list,schedule_remove— the agent-callable toolsSuggestionEngine.propose/pending/accept/dismiss— the wrapper engine_automations.pyinline-keyboard callbacks andcreate_from_blueprint
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
ScheduleJob also carries provider / model / pin_model for pinning a job to a specific model — snapshot the model at creation so an unattended run fails closed on drift. See Scheduler Model Pin.FileScheduleStore / ConfigYamlScheduleStore methods
Agent-tool signatures
The agent-callable tools and the wrapper engine gained an optionalprincipal. On a gateway it defaults from the session; passing it by hand only matters on the CLI / bridge path.
Semantics Reference
Choosing When to Scope
Common Patterns
On a bot gateway the tools read the session for you — noprincipal= needed. Once the gateway installs a SessionContext, an agent turn gets per-user isolation for free.
Cross-owner reads are refused too. On the gateway bridge,
accept_suggestion in _automations.py hides a suggestion whose principal doesn’t match the caller — the accept path returns “That suggestion was not found or has already been handled.” Details never leak by guessing an id.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.
