Portal Community

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 TypeIsolation MechanismEnforcement Point
SQL (Episodes, Procedures, Agents)TenantId global query filter on all entitiesEF Core (DbContext)
Vector DB (Qdrant)Per-agent collection; agent resolved via tenant-scoped AgentStoreAgentStore + collection naming
Vector DB (PGVector)Per-agent table + tenant_id column filterSQL WHERE clause in all queries
Working MemoryIn-process; scoped to the HTTP request and authenticated userASP.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);
}
Never Bypass Filters in Tenant-Scoped Code

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.