.NET / SQL / Enterprise Engineering
Executive Summary
Report summary
We design a secure multi-tenant graph-memory repository by enforcing strict row-level security (RLS) in SQL Server and isolating privileges. Each agent is assigned a TenantId and FleetId in a trusted authentication token. The ASP.NET Core app injects these into the database session via sp set sessio
Key topics
- .NET / SQL / Enterprise Engineering
- .NET
- SQL
- Enterprise Engineering
- C#
- Python
- Runtime
- Semantic Systems
- Research Archive
Research provenance
For citation, use the report title and canonical URL. Archival presence does not establish authorship or promote report statements into portfolio evidence.
This page renders the archived Markdown as safe, formatted HTML. It is background research and does not become a portfolio claim without evidence review.
Full report
On this page
We design a secure multi-tenant graph-memory repository by enforcing strict row-level security (RLS) in SQL Server and isolating privileges. Each agent is assigned a TenantId and FleetId in a trusted authentication token. The ASP.NET Core app injects these into the database session via sp_set_session_context (with @read_only=1) on each connection, so that SQL filters all queries by the agent’s tenant and fleet. Memory nodes, edges, assertions, etc. all carry TenantId/FleetId ownership fields and are filtered by inline RLS predicates. Ordinary agents can only query their own fleet’s subgraph. A separate alignment service account has read-only cross-fleet access to isolated snapshots; it proposes new cross-fleet edges but cannot modify active memory. Approved edges are stored in a dedicated table and exposed via a view that only reveals the explicitly linked node (not the entire remote subgraph). Contradictory assertions are resolved by selectable C# strategies (e.g. most-recent, highest-confidence, or confidence-weighted) while preserving provenance. We include a detailed SQL schema, RLS policies, C# code, threat analysis, diagrams, and testing plan to validate isolation and handle breaches.
Threat Model
We identify threats across STRIDE categories and map mitigations:
- Spoofing / Session-context spoofing: Agents might try to set arbitrary Tenant/Fleet IDs. Control: Use
sp_set_session_context @read_only=1so once set it cannot be overridden. The ASP.NET layer sets context from authenticated claims (never from client input) and we deny direct calls to set context. OWASP warns: “Never trust client-supplied tenant IDs”. We bind tenant/fleet to the authenticated user and set context in a trusted interceptor/middleware early in each request. Residual risk: If a malicious user steals the app’s DB login or finds a way to open a new session, context might not be set (logon triggers are possible on SQL Server to enforce this, but not on Azure SQL). We mitigate by limiting direct access and logging any missing-context accesses as potential attacks.
- Tampering / SQL injection & context injection: Malicious input could inject SQL or manipulate memory. Control: Use parameterized queries and ORM mapping; never construct SQL with string concatenation. At the DB layer, RLS acts as defense-in-depth but does not replace parameterization. We also deny database roles like
db_datawriterto agents, restricting them to specific tables via stored procedures. AnyINSERT/UPDATEis checked by block-predicates so an agent cannot write memory outside its fleet (see RLS section).
- Information disclosure / Unauthorized cross-fleet traversal: Without care, an agent might infer or brute-force other fleets’ data (e.g. via timing or row-count). Control: We apply RLS predicates on every table (filter by TenantId/FleetId) so all normal SELECTs are constrained. We do not expose any full cross-fleet link except approved edges. Even approved links are shown via a narrow view that only exposes the related node ID (the agent sees only its side of the edge) and hides all other remote data. We also prevent recursive traversal: queries on the cross-fleet view cannot join into the remote graph because RLS will filter out that remote fleet’s rows. Microsoft docs note that RLS simply adds a WHERE clause, so with proper indexing performance is acceptable, but it must be tested.
- Insecure privilege escalation / Overprivileged alignment service: The alignment service must not get blanket access. Control: Give it a separate DB principal with only SELECT on memory tables (no write rights on live nodes/edges). It reads snapshots (e.g. read-only replica or periodic dump) and writes proposals into a restricted table. Approved edges are inserted only via a controlled path (e.g. a stored procedure or by an admin role). All actions by this service are logged in an audit table. We also use network segmentation (e.g. limit the service’s IP, use VM isolation or containers) and managed identities so its credentials can be rotated without human exposure.
- Injection / Malicious prompt or memory injection: Agents could try to insert malicious assertions or edges. Control: RLS block predicates prevent an agent from inserting data with a TenantId/FleetId they don’t own. We also escape or parameterize any dynamic graph-query constructs. Crucially, the alignment output is treated only as proposals; it cannot automatically alter memory except through the review workflow.
- Confused-deputy: One component (e.g. app) might be tricked to act on behalf of another. Control: The app trusts only its own validated context. If a less-privileged service calls a stored proc, we use
EXECUTE ASor signed modules so it cannot arbitrarily escalate context.
- Race conditions / Concurrent writes: Two agents might update the same memory at once. Control: We include
rowversionconcurrency tokens on key tables and handle conflicts in code. Strategies for assertion resolution (latest timestamp, weighted) account for near-simultaneous updates.
- Side-channel leakage: Even with RLS, attackers could infer information from query timing, count differences, or errors. Control: Avoid returning explicit error messages (scrub errors) and use constant-time patterns where possible. For Azure, use Confidential VMs or isolated services to reduce microarchitectural leaks. We also avoid exposing row counts or statistics in the API layer, and throttle repeated probes.
- Unauthorized promotion of cross-fleet edges: An agent might try to mark an edge approved without review. Control: Only a privileged role (e.g. a human reviewer or an automated validator with a special role) can change the status to “approved”. We enforce this via a security policy or by granting the
INSERT/UPDATEon the ApprovedEdges table only to that role. All proposals and approvals are audited in a ReviewDecisions table with who/when.
- Bulk extraction: Agents could attempt to exfiltrate data by many small queries. Control: RLS already limits each query, but we additionally monitor and log large scans. We can implement query throttling at the application or use an API gateway rate-limiter. Exporting entire tables is only allowed via background jobs with service accounts (e.g. for reporting), not by agents.
- Poisoned confidence scores or attributes: A malicious agent might attach very high confidence to false data to game conflict resolution. Control: We calibrate confidences (e.g. cap them, use normalized scales) and keep all provenance. During conflict resolution we retain every assertion unless a policy explicitly merges them. We also log suspicious confidence outliers and may temporarily ignore assertions with extremely unusual confidence.
Each threat is mitigated by one or more controls above. Residual risk remains: RLS is not foolproof (experts note it can be circumvented by side-channel or by exploiting logical bugs). Thus we adopt defense-in-depth: database security, code checks, network controls, and monitoring. We emphasize denying by default (no query succeeds unless explicitly allowed). Regular security audits and penetration testing should validate that no unintended cross-tenant or cross-fleet leaks remain.
Authorization Model
We adopt a claims-based multi-tenant model. Each agent authenticates to an identity provider (e.g. OAuth/OIDC) which issues a token containing the agent’s TenantId and FleetId (non-guessable GUIDs) and roles (e.g. “AlignmentService”, “Admin”). In the ASP.NET Core app, a DbConnectionInterceptor sets two session-context keys (TenantId, FleetId) on every new SQL connection before issuing queries. Both are marked @read_only=1 so they cannot be altered later. We do not allow the agent’s code to choose these values arbitrarily; they come from the validated token (server-side) and the app runs with a single DB user credential.
For privileged operations (alignment comparisons, cross-fleet review), we use separate database principals or execution contexts. For example, an alignment service principal has SELECT on snapshots but no write permission on production tables. A reviewer principal (e.g. a DBA role) can INSERT into the ReviewDecisions and finalize edges, but ordinary agents cannot. We avoid a simple “IsPrivileged” flag in session context alone (since a malicious client could set that); instead, the review and alignment actions run under a distinct login/account and/or use signed stored procedures marked with EXECUTE AS OWNER, ensuring only a genuine privileged user can invoke them.
Row-Level Security (RLS) predicates refer to SESSION_CONTEXT(N'TenantId') and SESSION_CONTEXT(N'FleetId'). Because these values are set by the trusted app or service at connection-open, and cannot be spoofed (read-only), the DB enforces that an agent only accesses rows with matching tenant and fleet. This satisfies OWASP’s advice to “propagate tenant context securely through all layers” and never trust client input. Any attempt by a caller to run queries without setting these (or to inject another context) will simply return zero rows (no access). The ASP.NET app must therefore set the session context on every connection; a common implementation is an EF Core DbConnectionInterceptor that executes:
EXEC sp_set_session_context @key=N'TenantId', @value=@TenantId, @read_only=1;
EXEC sp_set_session_context @key=N'FleetId', @value=@FleetId, @read_only=1;
and similarly for an IsAlignmentService key when using the alignment account. We disable end-user access to sp_set_session_context so only our code path can call it. (In on-prem SQL Server we could also use a LOGON trigger to enforce this, but Azure SQL does not support it.)
Overall, our authorization model is: Tenant → Fleet → Agent. RLS inline functions check (TenantId, FleetId) from SESSION_CONTEXT against each row. Agents see only rows matching their context. The alignment service is exempted in the predicate (it checks a different flag) so it can read across fleets. Approved edges, review tables, and audit logs include explicit audit fields, and their visibility is also controlled by RLS or separate policies.
SQL Server Schema
We use a single database with shared tables. Below is a T-SQL schema sketch. All IDs are uniqueidentifier (GUID) to avoid integer overflow and to be non-guessable. We use DATETIMEOFFSET with SYSUTCDATETIME() defaults for UTC timestamps, and rowversion for concurrency tokens. Soft-delete flags are included as needed.
-- Tenants
CREATE TABLE Tenants (
TenantId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
Name NVARCHAR(200) NOT NULL,
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
IsDeleted BIT NOT NULL DEFAULT 0,
RowVer ROWVERSION NOT NULL
);
-- Each tenant name is unique
CREATE UNIQUE INDEX UX_Tenants_Name ON Tenants(Name);
-- Fleets (a fleet belongs to one tenant)
CREATE TABLE Fleets (
FleetId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
TenantId UNIQUEIDENTIFIER NOT NULL REFERENCES Tenants(TenantId),
Name NVARCHAR(200) NOT NULL,
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
IsDeleted BIT NOT NULL DEFAULT 0,
RowVer ROWVERSION NOT NULL,
CONSTRAINT UQ_Fleet UNIQUE (TenantId, Name)
);
-- Agents (users/bots in fleets; an agent is associated with one tenant)
CREATE TABLE Agents (
AgentId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
TenantId UNIQUEIDENTIFIER NOT NULL REFERENCES Tenants(TenantId),
Name NVARCHAR(200) NOT NULL,
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
IsActive BIT NOT NULL DEFAULT 1,
RowVer ROWVERSION NOT NULL
);
-- If agents can belong to multiple fleets, use a membership table:
CREATE TABLE AgentFleetMemberships (
AgentId UNIQUEIDENTIFIER NOT NULL REFERENCES Agents(AgentId),
FleetId UNIQUEIDENTIFIER NOT NULL REFERENCES Fleets(FleetId),
IsMember BIT NOT NULL DEFAULT 1,
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
PRIMARY KEY (AgentId, FleetId)
);
-- Tasks (e.g. cognitive or external tasks generating memory)
CREATE TABLE Tasks (
TaskId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
AgentId UNIQUEIDENTIFIER NOT NULL REFERENCES Agents(AgentId),
FleetId UNIQUEIDENTIFIER NOT NULL REFERENCES Fleets(FleetId),
Description NVARCHAR(MAX) NULL,
StartedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
CompletedAt DATETIMEOFFSET NULL,
RowVer ROWVERSION NOT NULL
);
-- MemoryNodes (vertices in the knowledge graph)
CREATE TABLE MemoryNodes (
NodeId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
TenantId UNIQUEIDENTIFIER NOT NULL REFERENCES Tenants(TenantId),
FleetId UNIQUEIDENTIFIER NOT NULL REFERENCES Fleets(FleetId),
AgentId UNIQUEIDENTIFIER NOT NULL REFERENCES Agents(AgentId),
TaskId UNIQUEIDENTIFIER NULL REFERENCES Tasks(TaskId),
Content NVARCHAR(MAX) NOT NULL, -- e.g. text or JSON
Valid BIT NOT NULL DEFAULT 1, -- validity state
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
UpdatedAt DATETIMEOFFSET NULL,
UpdatedBy UNIQUEIDENTIFIER NULL REFERENCES Agents(AgentId),
IsDeleted BIT NOT NULL DEFAULT 0,
RowVer ROWVERSION NOT NULL
);
-- Index to speed tenant/fleet filtering
CREATE INDEX IX_MemoryNodes_Tenant_Fleet ON MemoryNodes(TenantId, FleetId);
-- MemoryEdges (directed links between nodes)
CREATE TABLE MemoryEdges (
EdgeId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
TenantId UNIQUEIDENTIFIER NOT NULL REFERENCES Tenants(TenantId),
FleetId UNIQUEIDENTIFIER NOT NULL REFERENCES Fleets(FleetId),
SourceNodeId UNIQUEIDENTIFIER NOT NULL REFERENCES MemoryNodes(NodeId),
TargetNodeId UNIQUEIDENTIFIER NOT NULL REFERENCES MemoryNodes(NodeId),
Relationship NVARCHAR(100) NULL, -- e.g. type of edge
Valid BIT NOT NULL DEFAULT 1,
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
CreatedBy UNIQUEIDENTIFIER NULL REFERENCES Agents(AgentId),
IsDeleted BIT NOT NULL DEFAULT 0,
RowVer ROWVERSION NOT NULL,
CHECK (SourceNodeId <> TargetNodeId)
);
CREATE INDEX IX_MemoryEdges_Source ON MemoryEdges(SourceNodeId);
CREATE INDEX IX_MemoryEdges_Target ON MemoryEdges(TargetNodeId);
-- MemoryAssertions (beliefs or observations attached to nodes or edges)
CREATE TABLE MemoryAssertions (
AssertionId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
TenantId UNIQUEIDENTIFIER NOT NULL REFERENCES Tenants(TenantId),
FleetId UNIQUEIDENTIFIER NOT NULL REFERENCES Fleets(FleetId),
NodeId UNIQUEIDENTIFIER NULL REFERENCES MemoryNodes(NodeId),
EdgeId UNIQUEIDENTIFIER NULL REFERENCES MemoryEdges(EdgeId),
Text NVARCHAR(MAX) NOT NULL,
Confidence DECIMAL(5,4) NOT NULL, -- e.g. 0.0001 - 1.0000
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
CreatedBy UNIQUEIDENTIFIER NULL REFERENCES Agents(AgentId),
IsValid BIT NOT NULL DEFAULT 1,
RowVer ROWVERSION NOT NULL
);
-- Either NodeId or EdgeId should be non-null; could enforce with CHECK (not shown)
-- ConfidenceScores (separate numeric scores, if needed)
CREATE TABLE ConfidenceScores (
ScoreId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
AssertionId UNIQUEIDENTIFIER NOT NULL REFERENCES MemoryAssertions(AssertionId),
ScoreValue DECIMAL(5,4) NOT NULL,
EvaluatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
EvaluatedBy UNIQUEIDENTIFIER NULL REFERENCES Agents(AgentId),
RowVer ROWVERSION NOT NULL
);
-- MemoryProvenance (tracking source/context of nodes/edges)
CREATE TABLE MemoryProvenance (
ProvenanceId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
RelatedTable NVARCHAR(100) NOT NULL, -- e.g. 'MemoryNodes' or 'MemoryEdges'
RelatedId UNIQUEIDENTIFIER NOT NULL, -- the PK of that row
Description NVARCHAR(MAX) NULL, -- e.g. source system or log
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
CreatedBy UNIQUEIDENTIFIER NULL REFERENCES Agents(AgentId),
RowVer ROWVERSION NOT NULL
);
-- CrossFleetEdgeProposals (alignment service outputs pending review)
CREATE TABLE CrossFleetEdgeProposals (
ProposalId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
SourceNodeId UNIQUEIDENTIFIER NOT NULL REFERENCES MemoryNodes(NodeId),
TargetNodeId UNIQUEIDENTIFIER NOT NULL REFERENCES MemoryNodes(NodeId),
ProposedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
ProposedBy UNIQUEIDENTIFIER NULL REFERENCES Agents(AgentId),
Status NVARCHAR(50) NOT NULL DEFAULT 'Pending', -- or Approved/Rejected
ReviewNotes NVARCHAR(MAX) NULL,
RowVer ROWVERSION NOT NULL
);
-- ApprovedCrossFleetEdges (finalized cross-fleet links)
CREATE TABLE ApprovedCrossFleetEdges (
EdgeId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
SourceNodeId UNIQUEIDENTIFIER NOT NULL REFERENCES MemoryNodes(NodeId),
TargetNodeId UNIQUEIDENTIFIER NOT NULL REFERENCES MemoryNodes(NodeId),
ApprovedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
ApprovedBy UNIQUEIDENTIFIER NULL REFERENCES Agents(AgentId),
CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
IsActive BIT NOT NULL DEFAULT 1,
RowVer ROWVERSION NOT NULL
);
-- ReviewDecisions (audit of proposal reviews)
CREATE TABLE ReviewDecisions (
DecisionId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
ProposalId UNIQUEIDENTIFIER NOT NULL REFERENCES CrossFleetEdgeProposals(ProposalId),
ReviewerId UNIQUEIDENTIFIER NOT NULL REFERENCES Agents(AgentId),
Decision NVARCHAR(20) NOT NULL, -- 'Approved' or 'Rejected'
DecisionAt DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
Notes NVARCHAR(MAX) NULL
);
-- SecurityAuditEvents (generic audit log)
CREATE TABLE SecurityAuditEvents (
EventId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
EventTime DATETIMEOFFSET NOT NULL DEFAULT SYSUTCDATETIME(),
AgentId UNIQUEIDENTIFIER NULL REFERENCES Agents(AgentId),
EventType NVARCHAR(100) NOT NULL,
Details NVARCHAR(MAX) NULL
);
All foreign keys enforce Tenant/Fleet ownership chains. We include CHECK constraints (e.g. no self-edge) and unique constraints (e.g. fleet names are unique per tenant, tasks per agent, etc.). Timestamps use UTC. Concurrency is handled by ROWVERSION columns (not shown in code) to catch conflicting writes. We index the foreign keys and especially (TenantId, FleetId) on large tables to support RLS filtering. (In practice, large tables might also be partitioned by TenantId if workloads demand it.)
Row-Level Security Implementation
For each table containing tenant/fleet-owned data (MemoryNodes, MemoryEdges, MemoryAssertions, CrossFleetEdgeProposals, etc.), we create inline table-valued functions and security policies. For example, for MemoryNodes:
-- 1. Predicate function: only allow rows in this agent's tenant & fleet
CREATE FUNCTION dbo.fn_RLS_MemoryNodes()
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS AccessResult
WHERE TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS UNIQUEIDENTIFIER)
AND FleetId = CAST(SESSION_CONTEXT(N'FleetId') AS UNIQUEIDENTIFIER);
-- 2. Block function: prevent writes outside own tenant/fleet
CREATE FUNCTION dbo.fn_Block_MemoryNodes()
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS BlockResult
WHERE TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS UNIQUEIDENTIFIER)
AND FleetId = CAST(SESSION_CONTEXT(N'FleetId') AS UNIQUEIDENTIFIER);
-- 3. Apply RLS policy on MemoryNodes
CREATE SECURITY POLICY RLS_MemoryNodes
ADD FILTER PREDICATE dbo.fn_RLS_MemoryNodes() ON dbo.MemoryNodes,
ADD BLOCK PREDICATE dbo.fn_Block_MemoryNodes() AFTER INSERT, AFTER UPDATE ON dbo.MemoryNodes
WITH (STATE = ON);
Analogous functions/policies are applied to MemoryEdges, MemoryAssertions, CrossFleetEdgeProposals, etc. The filter predicate ensures SELECT/UPDATE sees only rows matching the session’s TenantId/FleetId. The block predicate ensures that any INSERT or UPDATE also has the same tenant/fleet (rejects out-of-bounds writes). This enforces the rule that agents cannot create or modify memory belonging to another fleet.
Session Context Usage: The app sets SESSION_CONTEXT('TenantId') and 'FleetId' upon connection. Note Microsoft’s best practice: “use SESSION_CONTEXT for users who connect through a middle-tier where application users share the same SQL login”. By using @read_only=1, we lock each key (as shown above). This means after the app calls:
EXEC sp_set_session_context @key=N'TenantId', @value=@TenantId, @read_only=1;
EXEC sp_set_session_context @key=N'FleetId', @value=@FleetId, @read_only=1;
no subsequent attempt to change those values will succeed. It is crucial that this is done automatically on each new connection (for example via an EF Core DbConnectionInterceptor). We also use the same mechanism to set an IsAlignmentService=1 flag for the alignment principal, which the RLS predicates could check if needed (e.g. allow a special case to read all fleets).
Protection: By moving tenancy logic into the database, we achieve defense-in-depth. Even if application code had a bug, the DB will not return unauthorized rows. (As one expert warns, RLS “is not really a security solution” on its own, so we still validate input and limit logins.) We enforce that sp_set_session_context is only called by our trusted code path; clients have no direct access to set arbitrary context. We also give each agent DB user account minimal permissions (no db_datareader/writer roles) and require all operations go through these RLS-filtered tables.
Preventing RLS Bypass: RLS can be circumvented by certain side-channels or misconfiguration. For example, if an agent manages to open a raw connection without setting context, they would see no rows (deny-by-default). This is acceptable (they have no access). We log any access where SESSION_CONTEXT is not set, and we make sure to test queries for zero rows. Azure SQL doesn’t support logon triggers, so the responsibility is on the application layer to set context every time. We discourage any use of global roles or invisible filters; instead, everything is enforced at the table-valued function level, as in the above example.
Approved Cross-Fleet Access
After the alignment service proposes cross-fleet links and they are approved, we must expose only the relationship itself, without granting general access. We implement this via a view (or table-valued function) that filters the ApprovedCrossFleetEdges table by session context. For example:
CREATE VIEW dbo.CrossFleetEdges AS
SELECT
EdgeId,
CASE
WHEN SourceNodeId IN (SELECT NodeId FROM MemoryNodes WHERE FleetId = CAST(SESSION_CONTEXT(N'FleetId') AS UNIQUEIDENTIFIER))
THEN SourceNodeId
ELSE TargetNodeId
END AS LocalNodeId,
CASE
WHEN SourceNodeId IN (SELECT NodeId FROM MemoryNodes WHERE FleetId = CAST(SESSION_CONTEXT(N'FleetId') AS UNIQUEIDENTIFIER))
THEN TargetNodeId
ELSE SourceNodeId
END AS RemoteNodeId,
CreatedAt, ApprovedAt
FROM dbo.ApprovedCrossFleetEdges AS E
WHERE ( -- show edges where one end is in this fleet
E.SourceNodeId IN (SELECT NodeId FROM dbo.MemoryNodes WHERE FleetId = CAST(SESSION_CONTEXT(N'FleetId') AS UNIQUEIDENTIFIER))
OR E.TargetNodeId IN (SELECT NodeId FROM dbo.MemoryNodes WHERE FleetId = CAST(SESSION_CONTEXT(N'FleetId') AS UNIQUEIDENTIFIER))
);
This view returns at most those edges that touch the agent’s fleet. It outputs the local node ID and the remote node ID, but it does not join to the remote node’s data. Because of RLS on MemoryNodes, any attempt to join or select the remote node’s fields will return nothing unless the agent’s fleet actually matches (which it doesn’t). Thus, agents can see “there is an approved link from my node X to some external node Y”, but they cannot see any content or neighbors of Y beyond what their own side exposes. This view could also include flags like IsOutgoing if we wanted to indicate direction from the agent’s perspective.
Controls on Cross-Edges: RLS alone is not enough to prevent misuse of cross-edges. We do not treat an approved edge as granting membership. The view ensures you cannot traverse beyond one hop. We rely on the application to call this view or a stored procedure; an agent cannot directly insert into ApprovedCrossFleetEdges because that table’s INSERT privilege is limited to the review role. If needed, we also implement temporal validity by adding an expiration timestamp and filtering it out in the view when expired. We audit all uses of cross-edges (via the view) so that any bulk traversal attempts are logged.
In summary, approved cross-fleet access is achieved by: (1) storing only explicit approved links in ApprovedCrossFleetEdges; (2) providing a restricted view that filters by the agent’s fleet; (3) relying on RLS on MemoryNodes to block any indirect discovery. This approach prevents recursive multi-hop traversal and limits data leakage to only the attributes of the approved link itself.
C# Conflict-Resolution Strategies
We model conflicting assertions in C# with domain classes and a strategy pattern. For example:
/// <summary>
/// Represents a knowledge assertion or observation with confidence and provenance.
/// </summary>
public class Assertion
{
[Display(Name = "Assertion")]
public Guid AssertionId { get; set; }
[Display(Name = "Value")]
public string Value { get; set; }
[Display(Name = "Timestamp")]
public DateTimeOffset Timestamp { get; set; }
[Display(Name = "Confidence")]
public double Confidence { get; set; }
// Additional fields like AgentId, Source, etc. could go here.
}
/// <summary>
/// Defines a strategy for resolving contradictory assertions.
/// </summary>
public interface IConflictResolutionStrategy
{
/// <summary>
/// Resolves a set of conflicting assertions according to the strategy.
/// </summary>
/// <param name="assertions">The assertions to resolve.</param>
/// <returns>
/// The chosen assertion(s). Depending on the strategy, this may be a single assertion or multiple preserved assertions.
/// </returns>
IEnumerable<Assertion> Resolve(IEnumerable<Assertion> assertions);
}
/// <summary>
/// Chooses the most recently updated assertion (highest Timestamp).
/// </summary>
public class LatestUpdateStrategy : IConflictResolutionStrategy
{
/// <summary>
/// Returns the assertion with the latest (max) timestamp.
/// </summary>
public IEnumerable<Assertion> Resolve(IEnumerable<Assertion> assertions)
{
if (!assertions.Any())
return Enumerable.Empty<Assertion>();
var latest = assertions.OrderByDescending(a => a.Timestamp).First();
return new[] { latest };
}
}
/// <summary>
/// Chooses the assertion with the highest confidence value.
/// </summary>
public class HighestConfidenceStrategy : IConflictResolutionStrategy
{
/// <summary>
/// Returns the assertion with the maximum Confidence.
/// If there is a tie, returns one of them arbitrarily (or could be extended to handle ties).
/// </summary>
public IEnumerable<Assertion> Resolve(IEnumerable<Assertion> assertions)
{
if (!assertions.Any())
return Enumerable.Empty<Assertion>();
var top = assertions.OrderByDescending(a => a.Confidence).First();
return new[] { top };
}
}
/// <summary>
/// Preserves all assertions but orders them by descending confidence.
/// This strategy does not collapse values; it retains the full set,
/// reflecting a confidence-weighted perspective.
/// </summary>
public class ConfidenceWeightedStrategy : IConflictResolutionStrategy
{
/// <summary>
/// Returns all assertions sorted by Confidence (highest first).
/// </summary>
public IEnumerable<Assertion> Resolve(IEnumerable<Assertion> assertions)
{
if (!assertions.Any())
return Enumerable.Empty<Assertion>();
// In a real system, one might compute a weighted average or other fusion.
// Here we simply preserve all assertions, sorted by confidence.
return assertions.OrderByDescending(a => a.Confidence).ToList();
}
}
Each strategy’s Resolve method returns an IEnumerable<Assertion>. In LatestUpdate and HighestConfidence, we return a single best assertion (conflicting others remain in memory but not chosen). In ConfidenceWeighted, we return all of them (no loss) but could be extended to compute an aggregate value if appropriate. We purposely do not merge distinct assertions into one result unless the policy requires it. This preserves contradictory evidence.
Strategies should account for calibration (e.g. ensure confidence scores are on a consistent scale), source reliability (could weight some agents more), and ties (here we simply pick the first in descending order, but real logic could use secondary criteria). We maintain provenance on the assertions, so the output strategy could, for example, also return multiple assertions with their Confidence fields unchanged (so a human can later review if needed). This ensures numeric stability and auditability. (If in future we use the weighted-average approach, it would produce a new “ensemble” assertion with a computed value and a composite confidence.)
Privileged Alignment Service
The alignment service runs offline (e.g. as a scheduled job or container). It uses a separate database principal with only read access. The workflow is:
- Snapshot Extraction: It queries the memory tables via a restricted interface. This could be a read-only replica or database backup to avoid affecting the live system. Crucially, it only reads the schema needed for structure (node IDs, edges) plus optional attributes if needed for semantic matching; it omits sensitive fields.
- Structural Alignment: Using an algorithm like Fused Gromov-Wasserstein (FGW), the service computes similarity between the two subgraphs. FGW combines node embeddings (semantics) and adjacency structure to suggest correspondences without any supervision. We only consider aligned node pairs with score above a threshold.
- Edge Proposals: For each high-scoring alignment, the service inserts a row into
CrossFleetEdgeProposals. It setsStatus = 'Pending'. The service does not write toMemoryNodesorMemoryEdgesof either fleet – the new relationships are just stored as “proposals”.
- Review Workflow: A human or automated review process examines each proposal. A review role (distinct DB user) executes a stored procedure or simple
UPDATEto set the proposal’s status to “Approved” or “Rejected”. Approved proposals are then inserted intoApprovedCrossFleetEdgesby a controlled process (could be the same review role or a job). The service principal has no permission to change status; only the reviewer role does.
- Credential Isolation: The alignment code runs under its own credentials (e.g. a managed identity or service account) that is rotated regularly. We network-isolate it (e.g. in a VPN or secure subnet) so that even if compromised, it cannot access other systems. Its database user has only
SELECTon the memory tables (andINSERTon the proposal table), no direct DML on core memory tables.
- Audit and Monitoring: Every access by the alignment service (and the admin review role) is logged in
SecurityAuditEvents. Proposed edges include aProposedByfield. If an edge is later revoked, the revocation is logged too. We keep a complete history inReviewDecisions.
Because the alignment service can compute on historical snapshots, it must not write back into active memory; writing directly could bypass RLS or approval rules. By treating its output only as proposals, we ensure the standard RLS and human checks remain the gatekeeper. This matches the principle of “Privileged access as a complete system”: only approved elevation paths (the review workflow) can introduce cross-fleet knowledge.
Finally, we segment this service: e.g. in Azure we might use an Azure Function with a managed identity that connects via TLS to the SQL DB. Keys or secrets are stored in a vault and rotated. We apply least-privilege (no more than needed). All relevant connections and data flows would be shown in the trust boundary diagram below.
graph LR
subgraph TrustedBoundary
App[ASP.NET Core API]
Auth[Identity Provider]
DB[(SQL Server with RLS)]
Align[Alignment Service]
Admin[Review Interface]
end
subgraph Agents
A1[Agent (Fleet1)]
A2[Agent (Fleet2)]
end
A1 -->|Authenticate| Auth
A1 -->|API Calls| App
A2 -->|Authenticate| Auth
A2 -->|API Calls| App
App -->|Sets SESSION_CONTEXT (TenantId, FleetId)| DB
App -->|Query/Update Memory| DB
Align -->|Read snapshots via secured context| DB
Admin -->|Review Proposals (approve)| DB
Testing and Validation
We plan extensive integration tests on SQL Server (using tSQLt or real queries) to confirm RLS and policies work. Example test cases:
- Cross-Tenant Denial: Connect as an agent from Tenant A (set
SESSION_CONTEXTto A). AttemptSELECT * FROM MemoryNodes WHERE TenantId = Bor on Fleet B. Expect 0 rows or permission error. - Cross-Fleet Denial: Similarly, an agent in Fleet1 of TenantA should not see any rows from Fleet2 of TenantA:
EXEC sp_set_session_context 'TenantId','<TenantA-GUID>',1;
EXEC sp_set_session_context 'FleetId','<Fleet1-GUID>',1;
SELECT COUNT(*) FROM MemoryNodes WHERE FleetId = '<Fleet2-GUID>'; -- Expect 0
- Authorized Same-Fleet Access: With context A/Fleet1,
SELECT * FROM MemoryNodes WHERE FleetId = <Fleet1-GUID>should return all Fleet1 rows. - Approved-Edge Retrieval: Approve an edge between NodeX (Fleet1) and NodeY (Fleet2). As Fleet1 agent, query
SELECT * FROM CrossFleetEdges. Should see an edge listing LocalNodeId=NodeX and RemoteNodeId=NodeY. As Fleet2 agent, similarly (or none if directional). Any attempt to join toMemoryNodesfor the remote node should yield no data due to RLS. - Traversal Prevention: Try to do a multi-hop, e.g. cross edges plus a second join into the other fleet’s edges; should return no hidden paths.
- Session Spoof Test: Attempt to run
EXEC sp_set_session_context @key='TenantId', @value='OtherTenant', @read_only=1after context was already set. Expect an error (cannot override read-only key). - Insert/Update Block: As a Fleet1 agent, attempt to insert a
MemoryNodewithFleetId=Fleet2. This should be blocked by the block predicate (error). E.g.:
EXEC sp_set_session_context 'TenantId','T1',1;
EXEC sp_set_session_context 'FleetId','F1',1;
INSERT INTO MemoryNodes(NodeId,TenantId,FleetId,Content)
VALUES(NEWID(),'T1','F2','Invalid'); -- Should fail
- Revoked Access: Remove an AgentFleetMembership for an agent and re-run a query; RLS should now treat that agent as not a member (no rows returned).
- Conflicting Writes: Simulate two updates (possibly in two transactions) to the same node with different values and confidences, then apply each resolution strategy and check outputs.
- Concurrency/Review Race: Simultaneously attempt to approve the same proposal twice; database constraints or transaction isolation should prevent duplicate or conflicting states.
These tests ensure our security rules are enforced in the live database. We will run them against an actual SQL Server instance or container (not just EF Core’s in-memory DB). Each test either expects a fixed row count or an error, and we log discrepancies. We also include scenario-based tests for the ASP.NET layer (mocking user tokens) to verify sp_set_session_context calls occur as expected.
Performance Considerations
Row-Level Security adds a filter predicate much like an extra WHERE clause, so performance is largely a function of index design. We must index (TenantId, FleetId) on each table that is filtered, as in the schema above. For example, IX_MemoryNodes_Tenant_Fleet allows SQL to quickly eliminate other tenants. The filter functions themselves are inline TVFs (not scalar functions) to allow good optimization. We avoid ORs in the predicate (favoring indexed AND checks). Queries that join across RLS-protected tables pay the cost of the extra filter, but this is unavoidable. In practice, with proper indexes, the overhead is low: Redgate benchmarks show only a few percent slow-down if the predicate is simple.
Graph traversal (finding neighbors via edges) uses the MemoryEdges table. We index the SourceNodeId and TargetNodeId (as shown) to speed graph lookups. Very large graphs may still require many joins or recursive CTEs; we should monitor such queries. We might consider SQL Server’s built-in Graph features (node/edge tables) in the future, but for now our design is relational. If needed, horizontal partitioning by tenant or splitting cold data to archived tables can help.
Finally, we note that alignment computations (FGW) are done offline, so their cost (likely in memory/R, Python or a specialized service) does not affect query performance. Approved edges are infrequent, so the cross-edge view is small and lightweight.
Operational Runbook & Incident Response
We define clear procedures for revocation and breaches:
- Key/Context Rotation: Regularly rotate database credentials and tokens for the alignment service, reviewer accounts, and the ASP.NET app (if using secrets). Use Managed Identities or gMSAs to automate rotation.
- Revoking an Agent: If an agent is compromised, immediately mark its membership inactive (
AgentFleetMemberships.IsMember=0andAgents.IsActive=0) and change its password or token. This revokes all its session context rights; RLS will then prevent further access. Log this event toSecurityAuditEvents.
- Revoking a Cross-Fleet Edge: If a proposed or approved edge is found malicious, update
Status='Revoked'or setApprovedCrossFleetEdges.IsActive=0and record the action (possibly adding a “revoked” review decision). The edge then disappears from the view. Since the view only returns active edges, agents lose that knowledge link (we could also alert affected agents).
- Audit Logging: We enable SQL Server Audit (or Extended Events) on the RLS tables. Key events are: any
SELECT/INSERT/UPDATEon memory tables by agents, any attempt to disable a policy, any use of the privileged review commands. As Microsoft advises, “audit tables and columns with security measures”. We periodically review these logs for anomalies (e.g. queries returning large result sets, repeated failed accesses, or changes to audit schema).
- Incident Steps: In the event of a suspected breach (e.g. evidence of cross-fleet leak or admin account misuse), we would: (1) Disable the suspect agent/db login and rotate keys; (2) Run queries to detect any unexpected data changes; (3) Roll back or quarantine nodes if needed (e.g. using
IsValid=0flags); (4) Notify compliance/leadership; (5) Re-run conflict-resolution to detect if any memory was tampered with (since provenance is kept); and (6) consider increasing RLS strictness (e.g. disabling query shapes used by attacker).
- Review and Training: We continuously train developers on not bypassing RLS in application code. All schema changes or code that might affect isolation must pass security review.
Remaining Risks
Despite these measures, some risks remain:
- Side-Channel Inference: As noted, RLS does not hide timing or count differences. A clever attacker might still infer the existence of other fleets’ data by probing row-count. Mitigation is hard; we minimize error signals and lock performance patterns, but it’s a known residual risk.
- Administrative Compromise: If a sysadmin or SA account is compromised, all bets are off (they can disable RLS). We mitigate by minimizing full-privilege logins, separating duties, and using features like
CONTROL SERVERinstead ofsysadmin. Still, a malicious DBA could inspect all data. This risk is documented as inherent.
- Bugs or Misconfiguration: An incorrect RLS predicate (e.g. forgetting a table) could accidentally expose data. We mitigate by thorough testing and using
ALTER TABLE … FORCE ROW LEVEL SECURITYif needed to ensure owners are filtered. Regular security testing should catch such errors.
- Alignment Service Bias: The FGW algorithm may produce erroneous edges (false positives) that get approved by mistake. These bad edges could indirectly expose relationships between fleets. We lower this risk by requiring human review for every proposal (no auto-approval) and logging all such edges for audit.
- Bulk Export or Backup Leakage: Data in backups or replicas must be protected as well. Even if an attacker can’t query them via the app, they might get a DB snapshot. We secure backups with encryption (TDE) and limit access.
In summary, our architecture significantly reduces direct cross-fleet leakage, but no system is infallible. We rely on defense-in-depth and monitoring to catch any residual issues. Regular review and adaptation (e.g. tightening policies, adding masking on sensitive attributes) will be part of ongoing risk management.
References: Our design follows Microsoft’s RLS guidance, OWASP multi-tenant best practices, and state-of-art graph alignment research. All security claims are grounded in official docs and industry papers.