Tenant Isolation
Octopus is a multi-tenant system. All SQL-backed memory tables include a TenantId column, and EF Core global query filters ensure every database query is automatically scoped. Vector storage is isolated by agent-scoped collections, which are tied to tenants by agent ownership.
SQL Isolation: Global Query Filters
EF Core global query filters are applied on every entity type that carries a TenantId. The current tenant is resolved from the HTTP request JWT and stored in a scoped ITenantContext service:
// OctopusDbContext — global query filters applied once, used everywhere
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Every entity with TenantId is automatically filtered
modelBuilder.Entity<Episode>()
.HasQueryFilter(e => e.TenantId == _tenantContext.TenantId
&& !e.IsDeleted);
modelBuilder.Entity<Procedure>()
.HasQueryFilter(p => p.TenantId == _tenantContext.TenantId
&& p.IsActive && p.IsApproved);
modelBuilder.Entity<OctopusAgent>()
.HasQueryFilter(a => a.TenantId == _tenantContext.TenantId
&& a.IsActive);
}
// ITenantContext — resolved from JWT claims on every request
public interface ITenantContext
{
Guid TenantId { get; }
}
public class JwtTenantContext : ITenantContext
{
public Guid TenantId { get; }
public JwtTenantContext(IHttpContextAccessor http)
{
var claim = http.HttpContext?.User?.FindFirst("tenant_id")?.Value
?? throw new UnauthorizedAccessException("tenant_id claim missing");
TenantId = Guid.Parse(claim);
}
}
Vector Storage Isolation
Semantic memory is isolated at the collection level — each agent has its own vector collection. Because agents are owned by tenants, cross-tenant collection access is impossible without knowing the agentId of a foreign tenant's agent:
// Collection name includes agentId — agents are owned by tenants
// Tenant A's agent → "agent_A1A1A1..."
// Tenant B's agent → "agent_B2B2B2..."
// The AgentStore validates that the agent belongs to the requesting tenant
// before any vector store operation:
public async Task<OctopusAgent> GetAgentAsync(Guid agentId, CancellationToken ct)
{
// EF Core global filter already scopes to TenantId
return await _db.Agents.FirstOrDefaultAsync(a => a.AgentId == agentId, ct)
?? throw new NotFoundException($"Agent {agentId} not found in tenant");
}
// If Tenant B tries to use Tenant A's agentId, the global filter returns null → 404
Isolation Layers Summary
| Storage Type | Isolation Mechanism | Enforcement Point |
|---|---|---|
| SQL (Episodes, Procedures, Agents) | TenantId global query filter on all entities | EF Core (DbContext) |
| Vector DB (Qdrant) | Per-agent collection; agent resolved via tenant-scoped AgentStore | AgentStore + collection naming |
| Vector DB (PGVector) | Per-agent table + tenant_id column filter | SQL WHERE clause in all queries |
| Working Memory | In-process; scoped to the HTTP request and authenticated user | ASP.NET Core DI scope |
Admin Bypass
Super-admin operations (e.g. tenant deletion, cross-tenant health checks) can bypass global filters using IgnoreQueryFilters(). This is restricted to the platform administration API — not accessible to tenant users:
// Super-admin only — purge all data for a tenant
public async Task PurgeTenantDataAsync(Guid tenantId, CancellationToken ct)
{
// IgnoreQueryFilters() required to cross tenant boundary
var episodes = await _db.Episodes
.IgnoreQueryFilters()
.Where(e => e.TenantId == tenantId)
.ToListAsync(ct);
_db.Episodes.RemoveRange(episodes);
await _db.SaveChangesAsync(ct);
}
Only platform-level super-admin code should call IgnoreQueryFilters(). Any tenant-scoped service, API controller, or background job that bypasses global query filters risks exposing cross-tenant data. Code review must flag any non-admin use of IgnoreQueryFilters() as a critical security violation.