UserComposite
The UserComposite represents a human (or system) interacting with Octopus agents. It controls which agents a user can access, carries user preferences, and links to their conversation history — all scoped to their tenant.
User Identity
Octopus users are identified via their tenant authentication system. The ExternalUserId is the identity claim from the auth provider (BizFirstGO SSO, Azure AD, etc.). Octopus does not manage passwords — it trusts the external identity provider:
public class UserComposite
{
public Guid Id { get; init; } // Octopus internal ID
public string ExternalUserId { get; set; } // from auth token (sub claim)
public string TenantId { get; init; }
public string DisplayName { get; set; }
public string Email { get; set; }
public UserPreferences Preferences { get; set; }
public IReadOnlyList<Guid> AccessibleAgentIds { get; set; }
public IReadOnlyList<Guid> ConversationIds { get; set; }
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset LastActiveAt { get; set; }
}
Agent Access Control
A user can only interact with agents that appear in their AccessibleAgentIds list. This list is managed by tenant administrators via the agents-app. Agent access can be granted at three levels:
| Level | Scope | How to Set |
|---|---|---|
| Individual | One user, one agent | User record in agents-app |
| Role-based | All users with a given role | Agent access policy in agents-app |
| Tenant-wide | All users in the tenant | Agent marked as public within tenant |
User Preferences
public class UserPreferences
{
public string PreferredLanguage { get; set; } // "en", "es", "fr"
public string Timezone { get; set; } // "America/New_York"
public bool StreamingEnabled { get; set; } // SSE streaming vs. full response
public bool ShowToolCallDetails { get; set; } // show tool call steps in UI
public Guid? DefaultAgentId { get; set; } // agent to open by default
}
Multi-Tenant Scoping
The TenantId on the UserComposite is the hard boundary. A user from Tenant A can never access agents, conversations, or memory belonging to Tenant B. The repository layer enforces this:
// All user queries are scoped to the tenant
public async Task<UserComposite?> GetByExternalIdAsync(
string externalUserId, string tenantId)
{
return await _db.OctopusUsers
.Where(u => u.ExternalUserId == externalUserId
&& u.TenantId == tenantId)
.Select(u => MapToComposite(u))
.FirstOrDefaultAsync();
}
Auto-Provisioning
When a user first accesses the Octopus chat for a tenant, a UserComposite is auto-provisioned from the auth token claims if it does not exist. The default accessible agents are set from the tenant's default agent access policy:
// In the chat API controller, before routing to agent:
var user = await _userRepository.GetOrProvisionAsync(
externalUserId: HttpContext.User.FindFirst("sub")!.Value,
tenantId: tenantId,
displayName: HttpContext.User.FindFirst("name")?.Value ?? "User",
email: HttpContext.User.FindFirst("email")?.Value);
if (!user.AccessibleAgentIds.Contains(agentId))
return Forbid("User does not have access to this agent");
Octopus supports system users — non-human identities used by other services to interact with agents programmatically. For example, a Flow Studio workflow can act as a system user to call an agent's reasoning capability. System users have their own UserComposite with service-specific preferences.