Policy Cascades for Governed Multi-Tenant Agent Platforms
The Problem with Single Configs
Most agent platforms give you one configuration file and hope. When you are running agents for more than one team - or more than one customer - a single config breaks down fast. Each workspace needs its own model, its own service access, its own secrets, but someone has to guarantee that no workspace can spend more than its budget or reach a service it was never authorized to use.
The answer is a policy cascade. Every setting - model, temperature, allowed services, API keys, skill availability, TTL defaults - resolves through three ordered tiers: company, repo, and workspace. A lower tier can narrow an upstream ceiling but never widen it. That single rule is what makes it safe to hand a workspace to a team without handing them the keys.
How the Cascade Resolves
Three tiers, bottom-up. The company tier sets the floor. The repo tier overrides it. The workspace tier overrides both. The resolution order is fixed for every kind of setting:
| Kind | Company tier | Repo tier | Workspace tier |
|---|---|---|---|
| Policy fields | defaults | override | override |
| Variables | floor | override | override |
| Skills | floor | override | override |
| Secrets | last-resort fallback | override | first-resolved |
Notice that secrets run in reverse. A workspace-tier credential wins over the repo and company defaults, and an empty value at the workspace tier falls through to the repo. This means you set a per-customer token where it is used and fall back up the chain only when it is absent. You are never forced to duplicate credentials across every workspace.
Narrow-Only: The Safety Property
The cascade is not a free-for-all override. For service access, budgets, quotas, and TTL defaults, a lower tier can only narrow what the tier above it allows. If the company grants a repo [github, slack, search], a workspace under that repo can select a subset - [github, search] - but it cannot add linkedin. The resolve-time intersection is enforced, not advisory.
This extends to per-service API surface control. Granting access to GitHub does not mean granting access to every GitHub endpoint. A service entry can carry endpoint patterns - /user/repos or /repos/* - and any outbound call to an undeclared path is rejected before it leaves the platform. A lower tier can only further restrict those patterns.
Mutability Classes and Hard Locks
Every policy field belongs to one of four mutability classes:
- Compliance fields (audit logging, token limits, prompt injection guards) are company-only - no lower tier can touch them.
- Identity fields (model, temperature, service access) cascade normally.
- Operational fields (budgets, quotas, memory settings) are open to any tier.
- Collection fields (per-workspace variables and secrets) are workspace-only and never pre-populated from above.
On top of that, the company tier can place hard locks on individual fields. A locked field rejects every override from below - not silently ignored, but flatly rejected with a diagnostic. The lock is absolute until a company-tier operator clears it.
We use this for compliance mandates: audit logging is company-wide, so it is hard-locked at the company tier and no workspace under that company can disable it.
Skills Form a Floor, Never a Ceiling
Company-tier skills are visible to every workspace under that company and cannot be removed. A workspace can add its own skills on top of the floor but it can never subtract from it. If a seed skill is deleted, the deletion is recorded as a durable tombstone rather than a physical removal - the floor is stable across restarts and redeploys.
Workspace-authored skills that collide with a company-floor skill by ID are rejected at load time. Skills also obey the three-tier service ceiling: a skill that requires, say, GitHub access will not load for a workspace whose allowed_services intersection does not include GitHub, regardless of whether the skill sits on the company floor.
Multiple Companies, Complete Isolation
A single policy file can define multiple companies, each with its own defaults, locks, skills, and secrets. The cascade never cross-contaminates between companies - a workspace under company A never inherits company B's runtime overrides, skills, or workflow catalog.
Each company gets its own workspace singleton for live policy writes, and a company can opt into hide_base_pack to suppress the platform's entire base skill and workflow catalog for its workspaces. This keeps a customer-facing instance clean - it sees only the skills and workflows authored for that customer, not the platform internals.
Two-Layer Storage with Live Effect
Every tier stores policy in two layers. The git baseline is the cold-start seed - what you get on a fresh deploy. A runtime override layer, written to a persistent volume via CLI (policy set, vars set, secret set), takes effect immediately. The resolver detects filesystem changes and re-resolves on next access - no restart, no redeploy.
An operator can adjust a budget cap or add a service to a workspace and the change is live across the platform in the next request. The two layers can diverge intentionally - a workspace that starts from a company default of temperature: 0.7 and overrides it to 0.5 keeps that override across restarts. The baseline remains the floor; promotion of a long-lived override back to the git baseline is available through a typed promotion verb when the operator decides it should become the new default.
What You Get
The cascade model gives you a single answer to the question "what is this workspace's effective policy right now?" without hunting through files or guessing which override landed last. It gives you a safety perimeter - a workspace can never exceed the bounds its parent tiers set. And it gives you live mutability without a deploy cycle, because policy is data, not code.
This is the governance model that runs the ToolShell agent platform.
Comments
No comments yet. Start the discussion.