Agent DocsDeveloper documentation
Admin Guide

Access Control (4D Model)

4-dimension access control with Cedar policy examples

Access Control: Agent, Tool, Model & User Visibility

This document defines who can see what, who can call what, and who can use which models. It closes the gaps between tenant-level isolation (already solid) and the fine-grained controls needed for real enterprise deployment.

All four dimensions are DEFAULT-DENY. Access must be explicitly granted.

Identity Prerequisites

The 4-dimension access control model depends on knowing who the user is and which groups/roles they belong to. This requires a working identity layer:

Authentication Flow

  1. User visits platform → redirected to tenant's configured IdP (Keycloak, Azure AD, or generic OIDC)
  2. User authenticates at IdP → redirected back with OIDC authorization code
  3. Platform exchanges code for ID token + access token
  4. Platform extracts: sub (external user ID), groups, roles, email, name from JWT claims
  5. Platform looks up PlatformUser by sub + tenantId:
    • Found → update lastLogin, refresh groups (JIT mode), return session
    • Not found → create PlatformUser, assign default role, return session
  6. Session contains: userId, role, tenantId, groups, permissions

User Provisioning Modes

  • SCIM 2.0 (recommended): IdP pushes user/group changes to platform in real time. Platform maintains a synced, browsable cache of users and groups. Best for Dimension 1 because builders can browse real IdP groups when configuring entitlements.
  • JIT (Just-In-Time): User record created on first login from JWT claims. Groups refreshed on each login. Simpler but groups only update when users log in.
  • Bundled Keycloak: Platform ships its own Keycloak instance. For standalone/PoC deployments where no external IdP exists.

Platform Roles (locally managed)

  • consumer → Portal only (chat with agents, approve/reject)
  • builder → Portal + Studio (create agents, configure access, deploy)
  • admin → All three apps (manage tenants, policies, users, platform health)

Role assignment: mapped from IdP groups (e.g., agentic-builders → builder), or manually assigned by admin in the platform's User Directory.

Why This Matters for Access Control

  • Dimension 1 requires groups and roles from the identity layer to evaluate allowedGroups and allowedRoles on agent entitlements
  • The Agent Designer's Access tab needs a group picker populated from real IdP groups (not free-text) — this requires SCIM sync or at minimum JIT-cached groups
  • Audit logging needs userId to record who did what
  • Cost attribution needs userId to track per-user spending

4D Access Control Flow

The following diagram shows how each request is evaluated through all four dimensions sequentially. A denial at any dimension short-circuits the pipeline:

Scroll to zoom · Drag to pan · 100%

Not every dimension applies to every request. For example, a simple chat request evaluates D1 (can the user see this agent?) and D4 (can the agent use this model?), but skips D2 and D3 unless tool calls or agent delegations occur during execution.


Four Access Control Dimensions

TENANT BOUNDARY (hard wall — tenant_id in every request)

  Dimension 1: USER -> AGENT visibility
  "Which agents can this user see and interact with?"
  Controlled by: Agent Entitlements (teams/roles/users)
  Enforced at: API Gateway (L1)

  Dimension 2: AGENT -> TOOL permissions
  "Which tools can this agent invoke?"
  Controlled by: Tool Grants (per-agent allowlist)
  Enforced at: MCP Gateway (L5)

  Dimension 3: AGENT -> AGENT permissions
  "Which agents can this agent call via A2A?"
  Controlled by: Agent Interaction Policies
  Enforced at: A2A Gateway / Agent Orchestrator (L2)

  Dimension 4: AGENT -> MODEL access
  "Which LLM models can this agent use?"
  Controlled by: Model Access Policies (per-agent + per-tenant)
  Enforced at: Model Access PEP (L7, in front of AIFS)

Dimension 1: User -> Agent Visibility

Problem

Within a tenant, not every user should see every agent. The HR agent handles sensitive employee data. The finance agent can initiate payments. The legal agent accesses privileged communications. Exposing all agents to all users violates least-privilege.

Solution: Agent Entitlements

Each agent has an entitlements configuration that controls visibility:

interface AgentEntitlements {
  // Visibility mode
  visibility: 'public' | 'restricted' | 'private';
  
  // Who can see & interact with this agent (when visibility = 'restricted')
  allowedGroups?: string[];    // e.g. ['hr-team', 'people-ops', 'executives']
  allowedUsers?: string[];     // e.g. ['user-123'] -- for personal agents
  allowedRoles?: string[];     // e.g. ['manager', 'director', 'vp']
  
  // Who can configure/manage this agent (always restricted)
  owners: string[];            // builder user IDs who can edit
  ownerGroups?: string[];      // builder groups who can edit
}

Visibility modes:

  • public -- All users in the tenant can see and use this agent
  • restricted -- Only users matching allowedGroups, allowedUsers, or allowedRoles
  • private -- Only the agent owners can see it (for agents in development)

Where groups come from: The platform uses a hybrid identity model with pluggable IdP per tenant (Keycloak, Azure AD/Entra ID, or any OIDC provider). User group memberships and role attributes arrive via JWT claims and are kept current through one of three provisioning modes: SCIM 2.0 sync (recommended — IdP pushes changes in real time), JIT provisioning (updated on each login), or bundled Keycloak (standalone deployments). Groups map directly to allowedGroups and allowedRoles. Platform roles (consumer/builder/admin) are assigned locally and determine app access.

Cedar Policy Example

// Allow user to interact with a public agent in their tenant
permit(
  principal,
  action == Action::"chat",
  resource
) when {
  resource.type == "agent" &&
  resource.visibility == "public" &&
  principal.tenant_id == resource.tenant_id
};

// Allow user to interact with a restricted agent if they're in an allowed group
permit(
  principal,
  action == Action::"chat",
  resource
) when {
  resource.type == "agent" &&
  resource.visibility == "restricted" &&
  principal.tenant_id == resource.tenant_id &&
  principal.groups.containsAny(resource.allowed_groups)
};

// Block interns from any autonomous agent
forbid(
  principal,
  action in [Action::"agent:view", Action::"agent:chat"],
  resource
) when { resource.riskClassification == "autonomous" &&
         principal.role == "intern" };

API Enforcement

The agent listing endpoint MUST filter by entitlements:

GET /api/agents -> returns ONLY agents the requesting user is entitled to see

This is NOT a frontend filter. The API gateway queries Cedar PDP for each agent's visibility before including it in the response. For performance, this should use a batch evaluation or a pre-computed materialized view.

UI Impact

Portal Agent Catalog (P2): Only shows agents the user is entitled to see. No indication that hidden agents exist.

Studio Agent Designer (S2): Entitlements tab added to agent configuration. Builder sets visibility mode and allowed groups/users/roles.

Admin Agent Registry (A2): Admin sees ALL agents regardless of entitlements (admin privilege), but the entitlements config is visible and editable.


Dimension 2: Agent -> Tool Permissions

Problem

Not every agent should have access to every MCP tool. A customer-facing FAQ bot should not be able to call delete-database-records or send-wire-transfer. A data analyst agent shouldn't be able to call send-email or post-to-slack. Tool access must be explicitly granted per agent.

Solution: Tool Grants

Each agent has an explicit tool allowlist. If a tool is not in the list, the agent cannot invoke it AND cannot even discover it.

interface AgentToolGrants {
  // Explicitly granted tools
  tools: ToolGrant[];
  
  // Whether the agent can discover tools dynamically via MCP
  dynamicDiscovery: boolean;  // default: false -- safer
  
  // Maximum tool calls per execution (circuit breaker)
  maxToolCallsPerExecution: number;  // default: 20
}

interface ToolGrant {
  toolId: string;                       // MCP tool identifier
  permissions: ('read' | 'write' | 'execute' | 'admin')[];
  conditions?: {
    maxCallsPerHour?: number;           // rate limit per tool
    requireApproval?: boolean;          // HITL for this specific tool
    allowedInputPatterns?: string[];    // restrict input values (regex)
    timeWindow?: { start: string; end: string }; // time-based access
  };
}

Key principle: default-deny. An agent with no tool grants has no tool access. Builders must explicitly grant each tool during agent configuration.

Tool Tags for Organization (D2)

Tools support a tags field (stored as jsonb on the mcp_tools table in the DB schema) for organizational filtering and bulk grant operations. Tags are free-form strings (e.g., ["analytics", "read-only", "finance"]). Cedar policies can reference tags for group-level access rules (see the trading-tools Cedar example below). Tags also appear in the Studio Tool Registry and Admin Security Center for filtering and bulk operations.

Tool Risk Classification

Every tool in the MCP Registry has a risk level:

interface ToolRegistration {
  id: string;
  name: string;
  description: string;
  riskLevel: 'low' | 'medium' | 'high' | 'critical';
  category: 'read-only' | 'write' | 'external-comms' | 'financial' | 'destructive';
}

Risk classification drives defaults:

  • low (read-only tools): Granted easily, no approval needed
  • medium (write tools): Requires builder confirmation, logged
  • high (external comms, financial): Requires HITL approval per invocation
  • critical (destructive, admin): Requires admin approval to grant, always HITL

MCP Gateway Enforcement (Two-Layer Model)

Tool access uses a two-layer enforcement model:

Layer 1 — Grant Check (code-level default-deny):

  1. Agent calls mcp.tool.invoke(toolId, input)
  2. MCP Gateway looks up agent's tool grants (agent config → DB tool_grants table → tool sets)
  3. If tool not in grants -> DENY (return error to agent, tool invisible)
  4. If tool has conditions (rate limit, approval, time window) -> evaluate
  5. If conditions pass -> proceed to Layer 2
  6. If conditions fail -> DENY or escalate to HITL

Layer 2 — Cedar PDP Overlay (policy-based restrictions): 7. Evaluate Cedar D2 policies for the agent + tool pair 8. Cedar forbid policies override permit (deny-wins semantics) 9. If Cedar denies -> DENY (policy name included in error) 10. If Cedar allows (or no matching policies) -> forward to tool, log in audit

Important: The baseline Cedar D2 policy is permit — not forbid. Default-deny is already enforced by Layer 1 (grant check). Cedar's role is to add additional restrictions on top of grants (e.g., blocking high-risk tools during specific conditions). A blanket forbid in Cedar would block ALL tool access including granted tools.

HITL Approval Flow (Security)

When a tool grant has requireApproval: true, the execution pauses and the platform:

  1. Creates an approval record in the DB with risk score, impact, and audit metadata
  2. Emits an approval_required SSE event to the frontend
  3. The workflow waits for the approval signal (up to 24 hours)

After the user decides via POST /api/approvals/:id/decide (which enforces authorization, self-approval prevention, and audit logging), the platform: 4. Signals the Temporal workflow with the decision 5. Re-executes the tool with a verified approvalId (defense-in-depth: the approval is re-verified in the DB) 6. Emits tool_result and workflow_step events to update the frontend

Security: The /api/workflows/:id/resume endpoint verifies that the approval exists in the DB, belongs to the target workflow, and has actually been decided before forwarding the signal. This prevents forged approval signals from bypassing HITL gates.

Cedar Policy Example

// Allow agent to invoke a tool it has been granted
permit(
  principal,
  action == Action::"invoke",
  resource
) when {
  principal.type == "agent" &&
  resource.type == "tool" &&
  principal.tenant_id == resource.tenant_id &&
  principal.granted_tools.contains(resource.tool_id)
};

// Block any high-risk tool invocation without HITL approval
forbid(
  principal,
  action == Action::"invoke",
  resource
) when {
  resource.type == "tool" &&
  resource.risk_level == "high" &&
  context.hitl_approval != true
};

// Trading tools only during market hours
permit(
  principal == Agent::"trading-executor",
  action == Action::"tool:invoke",
  resource in ToolTag::"trading"
) when {
  context.currentHour >= 9 && context.currentHour <= 16 &&
  context.dayOfWeek in ["Mon", "Tue", "Wed", "Thu", "Fri"]
};

UI Impact

Studio Agent Designer (S2) -- Tools tab: Shows the MCP tool catalog. Builder checks tools to grant. Each tool shows risk level badge. High/critical tools show a warning and require additional confirmation. Per-tool conditions are configurable (rate limit, require approval, time windows).

Studio Tool Registry (S6): Shows tool risk level. Admin can change risk classification. Shows which agents have been granted this tool.

Admin Security Center (A3): Dashboard showing tool grant distribution. Alerts for agents with excessive tool grants. "Most privileged agents" view.


Dimension 3: Agent -> Agent Permissions

Problem

With A2A protocol, agents can invoke other agents. The orchestrator agent delegates to specialist agents. But a low-trust customer FAQ bot shouldn't be able to invoke the autonomous code deployment agent or the financial trading agent. Agent-to-agent communication needs explicit authorization.

Solution: Agent Interaction Policy

Defines which agents can call which other agents:

interface AgentInteractionPolicy {
  // Agents this agent can invoke (callee allowlist)
  canInvoke: AgentInvokeGrant[];
  
  // Agents that can invoke this agent (caller allowlist)
  invocableBy: AgentCallerGrant[];
  
  // Whether this agent can be discovered by other agents
  discoverable: boolean;  // default: true for active agents
}

interface AgentInvokeGrant {
  agentId: string;          // specific agent, or '*' for any (dangerous)
  agentTags?: string[];     // alternative: grant by capability tag
  maxDelegationsPerExecution: number;  // prevent infinite loops
  requireApproval?: boolean;
}

interface AgentCallerGrant {
  agentId: string;          // who can call me
  agentTags?: string[];     // or any agent with these tags
  principalType?: 'agent' | 'workflow';  // default: 'agent'
}

Default policy: An agent cannot invoke other agents unless explicitly granted. The Agent Orchestrator (L2) enforces this.

Workflow → Agent invocation (workflow-aware path)

Temporal workflow orchestrators (Process Hub / workflow_sdk.call_agent) also invoke agents, but a workflow is not a registered agent — it has no agent row and no canInvoke policy of its own. Requiring workflow IDs to be registered as agent principals (so the D3 source lookup resolves) pollutes the agent registry and the admin/KPI views with synthetic principals.

Instead the source is marked sourcePrincipalType: 'workflow' end-to-end (call_agent/internal/agents/invokecheckInteractionPolicy). On that path the check skips the source-agent lookup and canInvoke requirement and authorizes purely on the target agent's opt-in: an invocableBy entry with principalType: 'workflow' whose agentId is '*' (any platform workflow) or the specific workflow source ID. Default-deny still holds — a target with no workflow opt-in rejects workflow callers. A Workflow-principal Cedar overlay governs the path (fail-closed). This keeps the agent registry clean while decoupling workflow execution from agent-principal registration.

// Target agent opts into being invoked by any platform workflow:
"invocableBy": [{ "agentId": "*", "principalType": "workflow" }]

Interaction Patterns

Hub-and-Spoke: One orchestrator agent can call specialized agents, but they cannot call each other.

Peer Network: Agents in the same functional domain can call each other.

Hierarchical: Agents can only call agents at the same or lower risk classification. This prevents privilege escalation (assistive agent cannot invoke autonomous agent).

Loop Prevention

Agent interaction chains can create infinite loops (A calls B calls C calls A). Prevention:

  • maxDelegationsPerExecution: Hard cap per execution trace
  • execution_depth counter: Incremented on each delegation, max depth = 5
  • Cycle detection: If an agent appears twice in the same trace's delegation chain -> block

Cedar Policy Example

// Allow agent A to invoke agent B if A has a grant for B
permit(
  principal,
  action == Action::"delegate",
  resource
) when {
  principal.type == "agent" &&
  resource.type == "agent" &&
  principal.tenant_id == resource.tenant_id &&
  principal.invoke_grants.contains(resource.id) &&
  resource.invocable_by.contains(principal.id)
};

// Block privilege escalation: assistive cannot invoke autonomous
forbid(
  principal,
  action == Action::"delegate",
  resource
) when {
  principal.type == "agent" &&
  resource.type == "agent" &&
  principal.risk_classification == "assistive" &&
  resource.risk_classification == "autonomous"
};

// Max delegation depth of 5
forbid(
  principal,
  action == Action::"delegate",
  resource
) when { context.callDepth > 5 };

UI Impact

Studio Agent Designer (S2): "Interactions" sub-section within the Access tab:

  • "This agent can invoke:" -- multi-select of other agents (or tags)
  • "This agent can be invoked by:" -- multi-select of other agents (or tags)
  • Visual graph preview showing the interaction topology

Admin Agent Registry (A2): Interaction topology visualization -- directed graph showing which agents can call which. Highlights high-risk paths.


Dimension 4: Agent -> Model Access

Problem

Not every agent should be able to use every LLM model available through T-Systems AIFS. Without model access control:

  • A data-sovereign tenant's agents could call externally-hosted models (Claude, GPT, Gemini), violating the requirement that data never leaves the EU
  • A simple FAQ bot could burn tokens on expensive reasoning models (o3, gemini-2.5-pro) when gpt-4.1-nano would suffice
  • An "assistive" low-risk agent could access the most powerful models, creating unnecessary cost and capability exposure
  • Regulated industries cannot demonstrate model provenance control to auditors

Solution: Model Access PEP (Policy Enforcement Point)

A Model Access PEP sits between the platform API gateway and T-Systems AIFS. Every LLM call is checked against model access policies before forwarding to AIFS.

interface ModelAccessPolicy {
  id: string;
  
  // Who this policy applies to
  principalType: 'agent' | 'tenant' | 'agent-tag';
  principalId: string;
  
  // What models are allowed
  allowedModels?: string[];          // explicit model IDs (e.g. ['claude-sonnet-4', 'Llama-3.3-70B-Instruct'])
  allowedModelTags?: string[];       // e.g. ['eu-hosted', 'open-source', 'budget-tier']
  deniedModels?: string[];           // explicit deny (overrides allow)
  
  // Cost guardrails
  maxCostPerRequest?: number;        // dollar cap per single LLM call
  maxTokensPerRequest?: number;      // token cap per call
  maxCostPerDay?: number;            // daily spend cap for this agent
  
  // Metadata
  grantedBy: string;
  grantedAt: string;
  reason?: string;                   // why this policy exists (audit trail)
}

Model Classification

Every model in AIFS gets tagged with properties that policies can reference:

interface ModelClassification {
  modelId: string;                   // e.g. 'claude-sonnet-4'
  provider: string;                  // 'anthropic', 'openai', 'google', 'meta', 'deepseek', 'alibaba'
  hostingType: 'eu-self-hosted' | 'eu-partner' | 'external';
  sourceType: 'open-source' | 'proprietary';
  costTier: 'budget' | 'standard' | 'premium' | 'reasoning';
  capabilities: string[];           // ['chat', 'vision', 'code', 'reasoning', 'embedding', 'speech']
  deploymentRegion: string;          // 'otc-germany', 'otc-switzerland', 'external-us', etc.
  isExternallyHosted: boolean;       // true = data leaves T-Systems infrastructure
}

Model tags based on current AIFS inventory:

ModelHostingSourceCost TierData Leaves EU?
Llama-3.3-70B-Instructeu-self-hostedopen-sourcestandardNo
Qwen3-32B-FP8eu-self-hostedopen-sourcestandardNo
DeepSeek-R1-Distill-Llama-70Beu-self-hostedopen-sourcestandardNo
Qwen2.5-VL-72B-Instructeu-self-hostedopen-sourcestandardNo
claude-sonnet-4externalproprietarypremiumYes (via partner)
claude-3-7-sonnetexternalproprietarypremiumYes (via partner)
gpt-4.1externalproprietarypremiumYes (via partner)
gpt-4.1-miniexternalproprietarystandardYes (via partner)
gpt-4.1-nanoexternalproprietarybudgetYes (via partner)
gemini-2.5-proexternalproprietaryreasoningYes (via partner)
gemini-2.5-flashexternalproprietarystandardYes (via partner)
o3externalproprietaryreasoningYes (via partner)
o3-miniexternalproprietarypremiumYes (via partner)
o4-miniexternalproprietarypremiumYes (via partner)

How the Model Access PEP Works

Agent's LLM call (model: "o3", messages: [...])
         |
         v
  Model Access PEP
         |
  1. Look up agent's ModelAccessPolicy
  2. Look up tenant's ModelAccessPolicy (tenant-level overrides)
  3. Is "o3" in allowedModels or matches allowedModelTags?
  4. Is "o3" in deniedModels? (deny overrides allow)
  5. Check cost guardrails (daily spend cap, per-request cap)
  6. Cedar PDP evaluates full context
         |
    ALLOW -> forward to AIFS (https://llm-server.llmhub.t-systems.net/v2)
    DENY  -> return error to agent
             if fallback model configured and allowed -> retry with fallback
             log denied access in audit trail

Access Strategies

By Tenant (data sovereignty):

tenant: government-agency  -> only eu-self-hosted models
                              (Llama-3.3-70B, Qwen3-32B, DeepSeek-R1)
                              ALL external models denied

tenant: healthcare         -> eu-self-hosted + claude-sonnet-4 (via EU partnership)
                              deny: all OpenAI/Google models

tenant: startup            -> all models (no restriction)

By Agent Risk Classification:

assistive agents           -> budget + standard tier only
                              (gpt-4.1-nano, gemini-2.0-flash, Llama-3.3-70B)
semi-autonomous agents     -> budget + standard + premium tier
                              (adds claude-sonnet-4, gpt-4.1, gemini-2.5-flash)
autonomous agents          -> all tiers including reasoning
                              (adds o3, gemini-2.5-pro)

By Cost Budget:

agent: faq-bot             -> maxCostPerDay: $5, maxTokensPerRequest: 2000
agent: trading-analyzer    -> maxCostPerDay: $500, no token limit
agent: research-assistant  -> maxCostPerDay: $50, maxTokensPerRequest: 8000

Cedar Policy Examples

// Government tenant: EU-hosted models only, deny all external
forbid(
  principal,
  action == Action::"model:invoke",
  resource
) when {
  context.tenantId == "government-agency" &&
  resource.isExternallyHosted == true
};

// Assistive agents: budget/standard tier only
permit(
  principal,
  action == Action::"model:invoke",
  resource
) when {
  principal.type == "agent" &&
  principal.riskClassification == "assistive" &&
  resource.costTier in ["budget", "standard"]
};

forbid(
  principal,
  action == Action::"model:invoke",
  resource
) when {
  principal.type == "agent" &&
  principal.riskClassification == "assistive" &&
  resource.costTier in ["premium", "reasoning"]
};

// Daily cost cap enforcement
forbid(
  principal,
  action == Action::"model:invoke",
  resource
) when {
  principal.type == "agent" &&
  context.agentDailySpend >= principal.maxCostPerDay
};

Fallback Behavior

When a model is denied, the platform tries the agent's fallback model:

Agent configured: primary = claude-sonnet-4, fallback = Llama-3.3-70B-Instruct

Scenario: Tenant policy denies external models
  1. Agent calls claude-sonnet-4 -> Model Access PEP -> DENIED (external)
  2. Platform auto-retries with Llama-3.3-70B-Instruct -> ALLOWED (eu-self-hosted)
  3. Agent gets response from fallback model
  4. Audit log records: "Primary model denied (policy: data-sovereignty), fallback used"

This is transparent to the agent -- it just gets a response. The platform handles the fallback routing. The builder configures primary + fallback models knowing the tenant constraints.

UI Impact

Studio Agent Designer (S2) -- Model tab (enhanced):

  • Primary model selector with availability indicator (green = allowed, red = denied by policy)
  • Fallback model selector with same indicators
  • "Why is this model unavailable?" tooltip showing which policy denies it
  • Cost cap configuration (per-request, per-day)
  • Data sovereignty indicator per model (EU flag for self-hosted, warning for external)

Studio Model Selector (S12) -- enhanced:

  • Model comparison table now shows: hosting type, data sovereignty flag, cost tier
  • Filter: "Show only EU-hosted models", "Show only open-source models"
  • Per-model: "Available to X agents, denied to Y agents by policy"

Admin Console -- Model Access Policies (new section in A3 Security Center):

  • Tenant-level model policies (e.g. "this tenant can only use EU-hosted models")
  • Agent-level model policies (override or restrict further)
  • Policy simulator: "If agent X tries to call model Y, what happens?"
  • Cost dashboard: daily spend per agent vs. cap

Combined: Where Each Dimension Gets Enforced

LayerDimensionEnforcement
L1 ExperienceDim 1: User->AgentAPI Gateway filters agent list by user entitlements
L2 OrchestrationDim 3: Agent->AgentAgent Orchestrator checks interaction policies before delegation
L3 Agent ExecutionDim 2: Agent->ToolAgent runtime receives filtered tool list (only granted tools visible)
L5 Tool & ProtocolDim 2: Agent->ToolMCP Gateway enforces tool grants at call time
L7 AI/ML ServicesDim 4: Agent->ModelModel Access PEP checks model policies before AIFS call
L8 GovernanceAll 4 dimensionsCedar PDP is the single policy engine. Agent Registry stores all grants/policies
L9 ObservabilityAll 4 dimensionsALL access decisions (allow/deny) logged to Decision Audit Log

Combined Data Model

Agent Model (updated)

interface Agent {
  // ... existing fields ...
  
  // Access control (4 dimensions)
  entitlements: AgentEntitlements;           // Dim 1: User visibility
  toolGrants: AgentToolGrants;              // Dim 2: Tool permissions
  interactionPolicy: AgentInteractionPolicy; // Dim 3: Agent-to-agent
  modelAccess: AgentModelAccess;            // Dim 4: Model access
}

interface AgentModelAccess {
  allowedModels?: string[];          // explicit model IDs
  allowedModelTags?: string[];       // 'eu-hosted', 'open-source', etc.
  deniedModels?: string[];           // explicit deny
  maxCostPerRequest?: number;
  maxCostPerDay?: number;
  maxTokensPerRequest?: number;
}

Database Schema

-- Dimension 1: User -> Agent visibility
CREATE TABLE agent_entitlements (
  agent_id UUID REFERENCES agents(id),
  visibility TEXT NOT NULL DEFAULT 'public',  -- public, restricted, private
  PRIMARY KEY (agent_id)
);

CREATE TABLE agent_allowed_groups (
  agent_id UUID REFERENCES agents(id),
  group_name TEXT NOT NULL,
  PRIMARY KEY (agent_id, group_name)
);

CREATE TABLE agent_allowed_users (
  agent_id UUID REFERENCES agents(id),
  user_id TEXT NOT NULL,
  PRIMARY KEY (agent_id, user_id)
);

-- Dimension 2: Agent -> Tool permissions
CREATE TABLE agent_tool_grants (
  agent_id UUID REFERENCES agents(id),
  tool_id TEXT NOT NULL,
  permissions TEXT[] NOT NULL DEFAULT '{execute}',
  max_calls_per_hour INT,
  require_approval BOOLEAN DEFAULT false,
  time_window_start TIME,
  time_window_end TIME,
  PRIMARY KEY (agent_id, tool_id)
);

-- Dimension 3: Agent -> Agent permissions
CREATE TABLE agent_invoke_grants (
  caller_agent_id UUID REFERENCES agents(id),
  callee_agent_id UUID REFERENCES agents(id),
  max_delegations INT DEFAULT 5,
  require_approval BOOLEAN DEFAULT false,
  PRIMARY KEY (caller_agent_id, callee_agent_id)
);

-- Dimension 4: Agent -> Model access
CREATE TABLE agent_model_access (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  principal_type TEXT NOT NULL,              -- 'agent', 'tenant', 'agent-tag'
  principal_id TEXT NOT NULL,
  allowed_models TEXT[],
  allowed_model_tags TEXT[],
  denied_models TEXT[],
  max_cost_per_request DECIMAL,
  max_cost_per_day DECIMAL,
  max_tokens_per_request INT,
  granted_by UUID REFERENCES users(id),
  granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  reason TEXT
);

-- Tool risk classification
ALTER TABLE tools ADD COLUMN risk_level TEXT NOT NULL DEFAULT 'low';
ALTER TABLE tools ADD COLUMN category TEXT NOT NULL DEFAULT 'read-only';

-- Indexes
CREATE INDEX idx_model_access_tenant ON agent_model_access(tenant_id, principal_type, principal_id);
CREATE INDEX idx_tool_grants_agent ON agent_tool_grants(agent_id);
CREATE INDEX idx_invoke_grants_caller ON agent_invoke_grants(caller_agent_id);

API Endpoints

# Dimension 1: User -> Agent
GET  /api/agents                        -> filtered by user's entitlements
GET  /api/agents/:id/entitlements       -> get visibility config
PUT  /api/agents/:id/entitlements       -> set visibility config (builder/admin)

# Dimension 2: Agent -> Tool
GET  /api/agents/:id/tool-grants        -> get tool permissions
PUT  /api/agents/:id/tool-grants        -> set tool permissions (builder)
GET  /api/tools/:id/agents              -> which agents have access to this tool (admin)

# Dimension 3: Agent -> Agent
GET  /api/agents/:id/interactions       -> get A2A policy
PUT  /api/agents/:id/interactions       -> set A2A policy (builder)
GET  /api/admin/interaction-topology    -> full directed graph (admin)

# Dimension 4: Agent -> Model
GET  /api/agents/:id/model-access       -> get model access config
PUT  /api/agents/:id/model-access       -> set model access (builder/admin)
GET  /api/admin/model-policies          -> all model policies (admin)
POST /api/admin/model-policies          -> create tenant-level model policy (admin)

# Cross-cutting
POST /api/admin/access/simulate         -> "what if" policy simulator
  Body: { dimension, principalId, action, resourceId }
  Returns: { allowed: boolean, reason: string, matchedPolicies: Policy[] }
GET  /api/admin/privilege-report        -> agents with excessive permissions

UI: New "Access" Tab in Agent Designer (S2)

The Agent Designer gets a new 8th tab called Access with four sections:

Section 1: User Access (Dimension 1)

  • Visibility mode radio: Public / Restricted / Private
  • If restricted: multi-select for groups, users, roles from OIDC directory
  • Preview: "X users in Y groups can see this agent"

Section 2: Tool Access (Dimension 2)

  • MCP tool catalog browser (searchable, filterable by risk level)
  • Checkbox per tool to grant. Risk level badge shown (green/yellow/red/black)
  • Per-tool constraint expanders: rate limit, require approval, time window
  • Warning banner for critical tools: "Granting critical tools requires admin approval"
  • Summary: "This agent can use X tools (Y read-only, Z write, W high-risk)"

Section 3: Agent Access (Dimension 3)

  • "Can invoke:" multi-select of agents or agent tags
  • "Can be invoked by:" multi-select of agents or agent tags
  • Mini topology graph showing this agent's connections
  • Warning if creating a path to an autonomous agent from an assistive agent

Section 4: Model Access (Dimension 4)

  • Model list with checkboxes (populated from AIFS /models endpoint)
  • Each model shows: hosting type icon (EU flag or external warning), cost tier badge
  • Filter toggle: "EU-hosted only" / "Open source only"
  • Cost caps: max per request ($), max per day ($), max tokens per request
  • Tenant-level restrictions shown as locked (grayed out, "restricted by tenant policy")
  • Fallback chain: drag-and-drop ordered list of allowed fallback models
  • Preview: "Primary: claude-sonnet-4, Fallback: Llama-3.3-70B (EU-hosted)"

Ownership Checks (Builder Scope)

Builders can only modify access policies for agents they own. The API gateway enforces this with an ownership check on every mutating endpoint:

if (authUser.role === 'builder' && !matchesUserId([existing.owner], authUser.userId, authUser.platformUserId)) {
  return 403 "Only the agent owner or an admin can modify [resource]"
}

matchesUserId dual-matches the stored owner against the request's Keycloak sub (userId) or its resolved platformUserId — see User identity resolution. The same helper backs every user-grant check (visibility, allowedUsers, HITL approvers), so an owner/grantee stored as either id form resolves.

This check is applied consistently across all four dimensions:

EndpointOwnership Check
PUT /api/agents/:idBuilder must be owner of the agent
DELETE /api/agents/:idBuilder must be owner of the agent
PUT /api/agents/:id/entitlements (D1)Builder must be owner
PUT /api/agents/:id/tool-grants (D2)Builder must be owner
PUT /api/agents/:id/interactions (D3)Builder must be owner
PUT /api/agents/:id/model-access (D4)Builder must be owner

Admins bypass the ownership check and can modify any agent's access policies. This is enforced in services/api-gateway/src/routes/agents.ts.

User identity resolution (two id spaces)

A user grant (owners, allowedUsers, HITL approvers, workflow sharing) can be stored as either of a user's two ids, and both must resolve:

  • Keycloak sub — carried on every request (principal.userId). Runtime-created grants (e.g. an owner set from the executing principal) store this.
  • platform_users.id — the platform's own id (user-<uuid8>). The user picker and seed data store this.

At the auth boundary the gateway resolves the request's platformUserId from the sub (getUserByExternalId(sub), cached) and attaches it to the context. Every user-grant check then dual-matches against userId (sub) or platformUserId via the shared matchesUserId helper. This bridges the two id spaces without migrating stored grants.

Invariant: platform_users.externalId MUST equal the user's Keycloak sub — that is what the resolver keys on. JIT provisioning satisfies this automatically; seed data must set it explicitly.

To render a stored grant id as a readable name, POST /api/users/resolve maps ids (platform id or sub) to displayName/email.