Skip to main content

ADR-145: Project-Level RBAC & Multi-Tenant Access Control

Status

PROPOSED (2026-02-02)

Context

Problem Statement

CODITECT is a multi-tenant SaaS platform for AI-assisted software development. The platform serves three distinct user populations with fundamentally different access requirements:

  1. Internal CODITECT team -- Platform administrators and framework developers who need broad access to all tenants, tracks, and system configuration.
  2. External customers -- Organizations that purchase CODITECT licenses and operate within their own isolated tenant, managing their own projects, teams, and users.
  3. Open-source contributors -- Community members who contribute to public tracks and framework components with limited, read-heavy access patterns.

Today, access control is implicit: machine-id.json identifies a machine, the license system (Track C.12) gates feature availability, and django-multitenant provides database-level row isolation in the cloud backend. However, there is no formal role-based access control (RBAC) system that defines who can do what at the project level. This creates several risks:

RiskImpact
No project-level permissionsAny authenticated user within a tenant can modify any project
No track ownership modelTrack files have no formal access gating; anyone can edit any track
Agent privilege escalationAgents execute with the full permissions of their invoking context, with no scoped restrictions
No contributor scopingOpen-source contributors have no formal permission boundary
Audit gapNo structured audit trail for who performed which action on which resource

Existing Infrastructure

The following systems are already in place and must be integrated with, not replaced:

  • django-multitenant on the cloud backend (api.coditect.ai) provides tenant-level row isolation at the database query layer
  • ADR-053 defines the cloud sync architecture for task tracking, including local SQLite caching and cloud PostgreSQL as source of truth
  • ADR-074 defines governance hooks that run at PreToolUse time, validating task IDs and track nomenclature
  • ADR-117 defines hierarchical plan locations with four scopes: platform, org, customer, project
  • ADR-118 defines the four-tier local database architecture: platform.db (read-only), org.db (critical), sessions.db (regenerable)
  • ADR-144 defines the project registry with a tenant_id field for multi-tenant project isolation
  • License system (Track C.12) provides time-controlled feature gating per machine/user
  • machine-id.json provides hardware-based machine identification

Design Constraints

  1. RBAC must work both offline (local CLI) and online (cloud API) with eventual consistency
  2. The local enforcement layer must degrade gracefully when the cloud is unreachable
  3. Agent permissions must be scoped without requiring agents to authenticate independently
  4. The system must support the existing multi-tenant hierarchy: Platform > Tenant > Team > Project > User
  5. Track-level ownership must integrate with the existing track nomenclature (ADR-054) and track-based plan architecture (ADR-116)

Decision

We will implement a project-level RBAC system with seven defined roles, enforced at three layers (local CLI hooks, cloud API middleware, agent runtime), with tenant isolation as an inviolable constraint.

1. Role Definitions

Seven roles are defined across three scope levels:

RoleScopeDescription
Platform AdminPlatformFull access to all tenants, projects, system configuration, and platform operations. Reserved for AZ1.AI internal operators.
Org AdminTenantFull administrative control within a single tenant: manage projects, teams, users, billing, and tenant-level configuration.
Project OwnerProjectFull control of a specific project: plans, tracks, tasks, members, agent configuration, and project-level settings.
Project ContributorProjectRead/write access to tasks, plans, and code within assigned tracks of a specific project. Cannot modify project settings or membership.
Project ViewerProjectRead-only access to project plans, task status, reports, and dashboards. Cannot modify any project resource.
Track LeadTrack(s)Manage tasks, assign work, and approve completions within one or more assigned tracks. Inherits Contributor permissions for the assigned tracks, plus task management authority.
Agent (System)VariableAutomated access scoped to the invoking user's project context and role ceiling. Agents cannot exceed the permissions of the user who invoked them.

Role Hierarchy

Roles form an inheritance hierarchy where higher roles include the permissions of lower roles within the same scope:

Platform Admin
└── Org Admin (within tenant)
└── Project Owner (within project)
└── Track Lead (within assigned tracks)
└── Project Contributor (within project)
└── Project Viewer (within project)

The Agent role is orthogonal -- it inherits from the invoking user's effective role but is further constrained by agent-specific policy.

2. Permission Matrix

2.1 Project Operations

OperationPlatform AdminOrg AdminProject OwnerContributorViewerTrack LeadAgent
Create projectYYNNNNN
Read project metadataYYYYYYY*
Update project settingsYYYNNNN
Archive projectYYYNNNN
Delete projectYNNNNNN
List projects in tenantYYYY**Y**Y**N

* Agent reads are scoped to the current project context only. ** Contributors, Viewers, and Track Leads see only projects they are members of.

2.2 Track File Operations

OperationPlatform AdminOrg AdminProject OwnerContributorViewerTrack LeadAgent
Create track in projectYYYNNNN
Read track fileYYYYYYY*
Modify tasks in trackYYYY***NY****Y*
Mark tasks completeYYYY***NY****Y*
Reassign track ownershipYYYNNNN
Delete/archive trackYYYNNNN

* Agent operations are scoped to the invoking user's role ceiling. *** Contributors can only modify tasks within tracks they are assigned to. **** Track Leads can modify/complete any task within their assigned tracks.

2.3 Task Assignment

OperationPlatform AdminOrg AdminProject OwnerContributorViewerTrack LeadAgent
Assign task to userYYYNNY*N
Assign task to agentYYYNNY*N
Self-assign taskYYYYNYN
View task assignmentsYYYYYYY**

* Track Leads can assign tasks only within their assigned tracks. ** Agent visibility is scoped to the current project context.

2.4 Plan Modification

OperationPlatform AdminOrg AdminProject OwnerContributorViewerTrack LeadAgent
Edit PROJECT-PLAN.mdYYYNNNN
Edit track files (structure)YYYNNNN
Edit task content in trackYYYY*NY**Y***
Create checkpointsYYYYNYN
View plan/checkpointsYYYYYYY***

* Contributors can edit task content only in their assigned tracks. ** Track Leads can edit task content in their assigned tracks. *** Agent access follows the invoking user's role ceiling.

2.5 Registry & Cross-Project Access

OperationPlatform AdminOrg AdminProject OwnerContributorViewerTrack LeadAgent
View project registryYYYNNNN
Modify registry entriesYYNNNNN
Cross-project searchYYNNNNN
View other tenant's projectsYNNNNNN

2.6 Cloud Sync & Agent Invocation

OperationPlatform AdminOrg AdminProject OwnerContributorViewerTrack LeadAgent
Push to cloud APIYYYYNYY*
Pull from cloud APIYYYYYYY*
Invoke any agentYYYNNNN
Invoke track-scoped agentsYYYYNYN
Invoke read-only agentsYYYYYYN

* Agent sync operations use the invoking user's credentials and are scoped to the current project.

3. Enforcement Points

3.1 Local CLI Enforcement (Governance Hooks)

The existing governance hook architecture (ADR-074) is extended with a role-checking middleware layer:

Tool Call (Bash/Write/Edit/Task)


┌─────────────────────────┐
│ task_id_validator.py │ ← Existing: validates task ID format
│ (PreToolUse) │
└────────┬────────────────┘


┌─────────────────────────┐
│ rbac_enforcer.py │ ← NEW: validates user role + project scope
│ (PreToolUse) │
└────────┬────────────────┘


┌─────────────────────────┐
│ Tool Execution │
└─────────────────────────┘

rbac_enforcer.py behavior:

  1. Reads the active project context from ~/.coditect-data/active-project.json
  2. Reads the user's role assignment from ~/.coditect-data/rbac/role-cache.json
  3. Checks the requested operation against the permission matrix
  4. If the cloud API is reachable, validates the role cache is fresh (max age: 5 minutes)
  5. If the cloud API is unreachable, uses the cached role (offline mode)
  6. Blocks the operation with an explanatory error if the role is insufficient
  7. Logs the access decision to ~/.coditect-data/rbac/audit-log.jsonl

Local role cache schema (role-cache.json):

{
"user_id": "uuid",
"machine_id": "uuid",
"tenant_id": "uuid",
"roles": [
{
"role": "project_contributor",
"scope": "project",
"scope_id": "project-uuid",
"assigned_tracks": ["A", "B", "E"],
"granted_at": "2026-02-01T00:00:00Z",
"expires_at": "2026-03-01T00:00:00Z"
}
],
"cache_refreshed_at": "2026-02-02T10:30:00Z",
"cache_ttl_seconds": 300
}

3.2 Cloud API Enforcement (Django Middleware)

The cloud backend (api.coditect.ai) enforces RBAC at two layers:

Layer 1: Tenant Isolation (existing)

django-multitenant continues to provide row-level tenant isolation. Every database query is automatically filtered by tenant_id. This is the inviolable boundary -- no RBAC misconfiguration can cause cross-tenant data leakage.

Layer 2: Role-Based Permission Checking (new)

A new Django middleware RBACPermissionMiddleware is inserted after authentication:

# coditect_license/middleware/rbac.py

MIDDLEWARE = [
# ... existing middleware ...
'django_multitenant.middleware.MultitenantMiddleware', # Tenant isolation
'coditect_license.middleware.auth.JWTAuthenticationMiddleware', # Authentication
'coditect_license.middleware.rbac.RBACPermissionMiddleware', # NEW: Role checking
]

Permission check flow:

HTTP Request


┌──────────────────────────────┐
│ MultitenantMiddleware │ ← Sets tenant context (existing)
└──────────────┬───────────────┘


┌──────────────────────────────┐
│ JWTAuthenticationMiddleware │ ← Validates JWT, sets user (existing)
└──────────────┬───────────────┘


┌──────────────────────────────┐
│ RBACPermissionMiddleware │ ← NEW: Checks role vs endpoint permission
└──────────────┬───────────────┘


┌──────────────────────────────┐
│ View / API Endpoint │
└──────────────────────────────┘

Django models for RBAC:

class Role(TenantModel):
"""Defines available roles within a tenant."""
tenant_id = models.CharField(max_length=64)
name = models.CharField(max_length=64) # e.g., "project_owner"
scope = models.CharField(max_length=32) # "platform", "org", "project", "track"
# Note: "tenant" refers to django-multitenant isolation concept; "org" is the scope enum value
permissions = models.JSONField() # Permission set as JSON
is_system_role = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = ('tenant_id', 'name')

class RoleAssignment(TenantModel):
"""Assigns a role to a user within a scope."""
tenant_id = models.CharField(max_length=64)
user = models.ForeignKey('User', on_delete=models.CASCADE)
role = models.ForeignKey('Role', on_delete=models.CASCADE)
scope_type = models.CharField(max_length=32) # "project", "track"
scope_id = models.CharField(max_length=128) # project UUID or track letter
assigned_tracks = ArrayField(
models.CharField(max_length=4), blank=True, default=list
) # For track-scoped roles
granted_by = models.ForeignKey('User', on_delete=models.SET_NULL, null=True, related_name='grants')
granted_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField(null=True, blank=True)
is_active = models.BooleanField(default=True)

class Meta:
indexes = [
models.Index(fields=['tenant_id', 'user', 'scope_type', 'scope_id']),
]

class AuditEntry(TenantModel):
"""Immutable audit log for access control events."""
tenant_id = models.CharField(max_length=64)
user = models.ForeignKey('User', on_delete=models.SET_NULL, null=True)
action = models.CharField(max_length=128)
resource_type = models.CharField(max_length=64)
resource_id = models.CharField(max_length=256)
role_used = models.CharField(max_length=64)
decision = models.CharField(max_length=16) # "ALLOW" or "DENY"
reason = models.TextField(blank=True)
ip_address = models.GenericIPAddressField(null=True)
user_agent = models.TextField(blank=True)
timestamp = models.DateTimeField(auto_now_add=True)

class Meta:
indexes = [
models.Index(fields=['tenant_id', 'timestamp']),
models.Index(fields=['tenant_id', 'user', 'action']),
models.Index(fields=['tenant_id', 'resource_type', 'resource_id']),
]

3.3 Agent Runtime Enforcement

Agents operate under a delegated authority model: an agent's effective permissions are the intersection of its invoking user's role and any agent-specific policy restrictions.

Effective Agent Permissions = User Role ∩ Agent Policy

Agent policy schema (stored in agent frontmatter):

# In agent .md frontmatter
rbac:
max_role: project_contributor # Agent cannot exceed this role
allowed_operations:
- read_track
- modify_task
- mark_complete
denied_operations:
- create_project
- modify_registry
- assign_roles
track_scope: inherit # "inherit" = use invoking user's tracks, or explicit list

Enforcement flow for agent invocation:

  1. User invokes agent via /agent <name> "task" or Task(subagent_type=..., prompt=...)
  2. The agent dispatcher (scripts/core/agent_dispatcher.py) reads the invoking user's active role
  3. The dispatcher reads the agent's rbac frontmatter policy
  4. The effective permission set is computed as the intersection
  5. The agent session is initialized with a scoped context that includes only the permitted operations
  6. If the agent attempts an operation outside its effective permissions, rbac_enforcer.py blocks it at the hook level

4. Authentication Flow

4.1 Local Authentication (CLI)

┌──────────────────────────────────────────────────────────┐
│ LOCAL AUTHENTICATION │
│ │
│ machine-id.json ──┐ │
│ ├──► Identity Resolution │
│ license-key ──────┘ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Role Cache │ ◄── Cloud API │
│ │ (role-cache.json)│ (refresh) │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Active Project │ │
│ │ Context │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ Effective Role for Session │
│ │
└──────────────────────────────────────────────────────────┘
  1. On session start, machine-id.json and the active license key identify the user and machine
  2. The system checks ~/.coditect-data/rbac/role-cache.json for cached role assignments
  3. If the cache is stale (older than cache_ttl_seconds) and the cloud API is reachable, the cache is refreshed via GET /api/v1/rbac/my-roles/
  4. If the cloud API is unreachable, the stale cache is used with a warning logged
  5. The active project context (from ~/.coditect-data/active-project.json or project registry) determines the project scope
  6. The effective role for the session is resolved by matching the user's role assignments to the active project

4.2 Cloud Authentication (API)

┌──────────────────────────────────────────────────────────┐
│ CLOUD AUTHENTICATION │
│ │
│ Client ──► auth.coditect.ai ──► JWT (access + refresh) │
│ │
│ JWT contains: │
│ - user_id │
│ - tenant_id │
│ - machine_id │
│ - roles: [{role, scope_type, scope_id}] │
│ - exp (15 min access, 7 day refresh) │
│ │
│ api.coditect.ai validates JWT on every request: │
│ 1. Signature verification (RS256) │
│ 2. Expiration check │
│ 3. Tenant context binding │
│ 4. Role extraction for permission check │
│ │
└──────────────────────────────────────────────────────────┘
  1. Users authenticate via OAuth2 at auth.coditect.ai, receiving a JWT pair (access token + refresh token)
  2. The access token (15-minute expiry) contains embedded role claims to avoid per-request database lookups
  3. The refresh token (7-day expiry) is stored securely in ~/.coditect-data/rbac/auth-tokens.json with file permissions 0600
  4. The cloud API validates the JWT signature (RS256), checks expiration, binds the tenant context, and extracts roles for permission checking
  5. Role changes take effect on the next token refresh (max 15-minute propagation delay)

4.3 Agent Authentication (Service Accounts)

┌──────────────────────────────────────────────────────────┐
│ AGENT AUTHENTICATION │
│ │
│ Agent invocation: │
│ 1. User invokes agent │
│ 2. Dispatcher creates scoped agent token │
│ 3. Token = {user_id, project_id, max_role, ttl: 1h} │
│ 4. Agent uses token for cloud API calls │
│ 5. Token auto-expires after TTL │
│ │
│ Agent tokens are: │
│ - Short-lived (1 hour max) │
│ - Project-scoped (single project) │
│ - Role-capped (never exceeds invoking user) │
│ - Non-refreshable (new invocation = new token) │
│ │
└──────────────────────────────────────────────────────────┘

Agent tokens are not stored persistently. They exist only in memory for the duration of the agent's execution and are discarded when the agent completes.

5. Tenant Isolation Guarantees

Tenant isolation is the highest-priority security invariant. It is enforced at multiple layers to provide defense in depth:

LayerMechanismFailure Mode
Databasedjango-multitenant auto-filters all queries by tenant_idIf bypassed, data leaks across tenants
APIMultitenantMiddleware sets tenant context from JWTIf JWT is forged, middleware catches mismatched tenant
RBACRole assignments are tenant-scoped; RoleAssignment.tenant_id is always checkedCross-tenant role grants are structurally impossible
Localrole-cache.json includes tenant_id; project context includes tenant_id from ADR-144Offline cache is tenant-bound
AuditAll audit entries include tenant_id for tenant-scoped audit queriesAudit trail is per-tenant

Cross-Tenant Boundary Rules

  1. Customer data NEVER crosses tenant boundaries. The django-multitenant library enforces this at the ORM layer. There is no API endpoint that accepts a tenant_id parameter -- the tenant is always derived from the authenticated JWT.
  2. Local org.db stores internal CODITECT project data only. Customer project data resides exclusively in the cloud PostgreSQL database, scoped by tenant_id. The local org.db (ADR-118, Tier 2) contains only the internal CODITECT organization's decisions, learnings, and error solutions.
  3. Project registry entries are filtered by tenant_id. As defined in ADR-144, every project registry entry includes a tenant_id field. The RBAC system enforces that users can only query and modify registry entries within their own tenant.
  4. Track namespacing prevents cross-tenant task ID collision. Task IDs (e.g., A.9.1.3) are unique within a project, and projects are unique within a tenant. The fully qualified task identifier is {tenant_id}/{project_id}/{task_id}, ensuring global uniqueness.

6. Internal vs Customer vs Open-Source Access

Internal CODITECT Team

AttributeValue
Tenantaz1-internal (reserved tenant ID)
Default RolesPlatform Admin (infrastructure team), Org Admin (framework developers)
Track AccessAll Tier 1 tracks (A-N), plus Tier 2 business tracks (O-AA)
Special PermissionsCross-tenant visibility for support, system configuration, platform metrics
AuthenticationOAuth2 via auth.coditect.ai with Google Workspace SSO

External Customers

AttributeValue
TenantCustomer-specific tenant ID (provisioned at onboarding)
Default RolesOrg Admin (account owner), Project Owner (per project), Contributor/Viewer (team members)
Track AccessTracks defined in their projects; Tier 2 and Tier 3 tracks from config/tracks.json
Special PermissionsNone beyond their tenant boundary
AuthenticationOAuth2 via auth.coditect.ai with optional SSO (SAML/OIDC)

Open-Source Contributors

AttributeValue
Tenantoss-community (shared tenant for open-source contributions)
Default RolesProject Viewer (all public projects), Project Contributor (approved public tracks only)
Track AccessPublic framework tracks only (subset of Tier 1, as designated by Platform Admin)
Special PermissionsCannot create projects, cannot access customer data, cannot invoke privileged agents
AuthenticationOAuth2 via auth.coditect.ai with GitHub SSO
Promotion PathConsistent contribution history qualifies for Track Lead on public tracks

7. RBAC Data Storage

Cloud (Source of Truth)

Role definitions, role assignments, and audit entries are stored in PostgreSQL, managed by django-multitenant, and exposed via REST API:

EndpointMethodPermissionPurpose
/api/v1/rbac/roles/GETOrg Admin+List roles in tenant
/api/v1/rbac/roles/POSTPlatform AdminCreate custom role
/api/v1/rbac/assignments/GETProject Owner+List role assignments
/api/v1/rbac/assignments/POSTOrg Admin+Assign role to user
/api/v1/rbac/assignments/{id}/DELETEOrg Admin+Revoke role assignment
/api/v1/rbac/my-roles/GETAny authenticatedGet own role assignments
/api/v1/rbac/check/POSTAny authenticatedCheck permission for operation
/api/v1/rbac/audit/GETOrg Admin+Query audit log

Local (Cache)

Role data is cached locally for offline operation:

FilePurposeTTL
~/.coditect-data/rbac/role-cache.jsonCached role assignments for current user5 minutes (online), unlimited (offline)
~/.coditect-data/rbac/auth-tokens.jsonJWT access + refresh tokens (mode 0600)15 min access, 7 day refresh
~/.coditect-data/rbac/audit-log.jsonlLocal audit trail (synced to cloud)30 days local retention
~/.coditect-data/active-project.jsonActive project context (existing)Session-scoped

Consequences

Positive

  1. Principle of Least Privilege. Users and agents operate with the minimum permissions required for their role, reducing the blast radius of compromised credentials or misconfigured agents.
  2. Audit Trail. Every access decision (ALLOW and DENY) is logged with full context (user, role, action, resource, timestamp, IP), enabling SOC2 compliance and forensic analysis.
  3. Tenant Isolation with Defense in Depth. Four independent layers (database, API middleware, RBAC, local cache) ensure that a failure in any single layer does not breach tenant boundaries.
  4. Offline Capability. The local role cache enables RBAC enforcement even when the cloud API is unreachable, maintaining security guarantees in disconnected environments.
  5. Agent Safety. The delegated authority model prevents agents from escalating privileges beyond their invoking user's role, and agent-specific policies can further restrict operations.
  6. Open-Source Contributor Onboarding. A formal role for open-source contributors provides a clear, secure path for community participation without exposing customer data or internal systems.
  7. Track-Level Granularity. Track Leads can manage their domain independently, enabling parallel development across multiple tracks without permission conflicts.

Negative

  1. Complexity. Seven roles, three enforcement layers, and online/offline synchronization add significant complexity to the system. Role resolution logic must be thoroughly tested.
  2. Latency. The rbac_enforcer.py hook adds a permission check to every tool call. This must complete in under 10ms to avoid perceptible delay. The local cache mitigates this, but cache staleness introduces a consistency window.
  3. Role Cache Staleness. When operating offline or during the 5-minute cache TTL, role changes are not immediately reflected locally. A revoked role could remain active for up to 5 minutes (online) or indefinitely (offline, until next connection).
  4. Migration Effort. Existing internal users must be assigned roles, and the governance hooks must be updated to include the new rbac_enforcer.py in the PreToolUse chain.

Neutral

  1. No Change to Tenant Isolation. django-multitenant continues to be the primary tenant isolation mechanism. RBAC adds authorization within a tenant but does not replace the existing isolation model.
  2. License System Coexistence. The license system (Track C.12) continues to gate feature availability. RBAC gates who can perform which operations. A user might have the RBAC permission to create a project but lack the license tier to access premium features within it.

Implementation Plan

Phase 1: Foundation (Week 1-2)

  1. Define the seven system roles as seed data in the cloud database
  2. Create Django models: Role, RoleAssignment, AuditEntry
  3. Implement RBACPermissionMiddleware for the cloud API
  4. Create REST API endpoints for role management
  5. Write database migrations with rollback support

Phase 2: Local Enforcement (Week 3-4)

  1. Implement rbac_enforcer.py governance hook
  2. Create local role cache (role-cache.json) with sync logic
  3. Integrate with existing task_id_validator.py hook chain
  4. Implement offline degradation mode with warning logging
  5. Create local audit log (audit-log.jsonl) with cloud sync

Phase 3: Agent Integration (Week 5-6)

  1. Define rbac frontmatter schema for agent definitions
  2. Update agent_dispatcher.py with delegated authority logic
  3. Implement scoped agent token generation
  4. Update existing agent definitions with appropriate rbac policies
  5. Add agent permission checks to rbac_enforcer.py

Phase 4: User Migration & Testing (Week 7-8)

  1. Assign roles to all internal CODITECT team members
  2. Create the oss-community tenant and seed contributor roles
  3. Conduct security review of all enforcement points
  4. Run penetration testing focused on tenant boundary violations
  5. Perform load testing on rbac_enforcer.py to verify sub-10ms performance
  6. Write integration tests for all role-permission combinations

Security Considerations

Threat Model

ThreatMitigation
Privilege escalation via agentDelegated authority model caps agent permissions at invoking user's role
Cross-tenant data accessFour-layer defense: database isolation, API middleware, RBAC scoping, local cache tenant binding
Stolen JWT tokenShort-lived access tokens (15 min), refresh token rotation, IP binding in audit log
Role cache tamperingCache is validated against cloud API on refresh; tampered cache only persists until next sync
Offline role bypassLocal audit log captures all offline decisions; anomalies are flagged on next cloud sync
API endpoint enumerationAll endpoints require authentication; role-insufficient responses return 403 (not 404) to prevent information leakage
Insider threat (Platform Admin)All Platform Admin actions are logged to an immutable audit trail; require MFA for role assignment operations

Cryptographic Requirements

  1. JWT signing uses RS256 with key rotation every 90 days
  2. Refresh tokens are stored encrypted at rest using the machine's keychain (macOS Keychain / Linux Secret Service)
  3. Local audit logs are signed with HMAC-SHA256 using a per-machine key derived from machine-id.json to detect tampering
  4. All API communication uses TLS 1.3 exclusively

Rate Limiting

EndpointRate LimitScope
/api/v1/rbac/check/1000 req/minPer user
/api/v1/rbac/assignments/ POST50 req/minPer tenant
/api/v1/rbac/audit/100 req/minPer user
Authentication endpoints10 req/minPer IP

Compliance Notes

SOC2 (Trust Services Criteria)

CriteriaHow ADR-145 Addresses It
CC6.1 Logical access securityRole-based access with principle of least privilege; seven defined roles with explicit permission boundaries
CC6.2 Access provisioningRole assignments are tracked with granted_by, granted_at, and expires_at fields; Org Admins manage access within their tenant
CC6.3 Access removalRole assignments can be revoked via API; expired assignments are automatically inactive; 5-minute propagation to local cache
CC6.6 System boundariesTenant isolation enforced at database, API, RBAC, and local cache layers; cross-tenant access is structurally prevented
CC7.1 MonitoringImmutable audit log captures all access decisions (ALLOW/DENY) with user, action, resource, role, timestamp, and IP address
CC7.2 Anomaly detectionOffline access decisions are flagged for review on cloud sync; unusual access patterns are detectable via audit log queries

GDPR (General Data Protection Regulation)

RequirementHow ADR-145 Addresses It
Article 5(1)(f) Integrity and confidentialityTenant isolation ensures personal data is processed only within the authorized tenant boundary; RBAC prevents unauthorized access within a tenant
Article 25 Data protection by designRole-based access is enforced at every layer (local, API, agent); default role for new users is Project Viewer (read-only)
Article 30 Records of processingAudit entries record every data access operation with full context, satisfying the requirement for processing activity records
Article 32 Security of processingMulti-layer enforcement, encrypted token storage, signed audit logs, and short-lived credentials provide appropriate security measures
Article 17 Right to erasureTenant deletion cascades through all role assignments, audit entries, and cached data; the tenant_id scope ensures complete data removal
Article 33 Breach notificationAudit log enables rapid identification of affected data subjects in the event of a breach by filtering on tenant_id and resource_type
ADRRelationship
ADR-053Cloud Context Sync Architecture -- RBAC enforces permissions on sync operations (push/pull)
ADR-074Governance Hook Architecture -- rbac_enforcer.py is added to the PreToolUse hook chain
ADR-117Hierarchical Plan Location Strategy -- Plan scopes (platform, org, customer, project) map to RBAC role scopes
ADR-118Four-Tier Database Architecture -- Local databases remain internal-only; customer data stays in cloud
ADR-144Project Registry -- tenant_id field in project registry entries is used for RBAC tenant scoping
ADR-054Track Nomenclature Extensibility -- Track letters in RBAC track-scoped roles follow this standard
ADR-116Track-Based Plan Architecture -- Track files are the resources gated by track-level permissions
ADR-146Unified Task ID Strategy -- Task ID project prefix enables RBAC project-scoped validation

Appendix A: Default Role Seed Data

[
{
"name": "platform_admin",
"scope": "platform",
"is_system_role": true,
"permissions": {
"project": ["create", "read", "update", "archive", "delete"],
"track": ["create", "read", "update", "delete", "assign_lead"],
"task": ["create", "read", "update", "complete", "assign"],
"plan": ["read", "update"],
"registry": ["read", "update"],
"sync": ["push", "pull"],
"agent": ["invoke_any"],
"rbac": ["manage_roles", "assign_roles", "view_audit"],
"cross_tenant": ["view"]
}
},
{
"name": "org_admin",
"scope": "org",
"is_system_role": true,
"permissions": {
"project": ["create", "read", "update", "archive"],
"track": ["create", "read", "update", "delete", "assign_lead"],
"task": ["create", "read", "update", "complete", "assign"],
"plan": ["read", "update"],
"registry": ["read", "update"],
"sync": ["push", "pull"],
"agent": ["invoke_any"],
"rbac": ["assign_roles", "view_audit"]
}
},
{
"name": "project_owner",
"scope": "project",
"is_system_role": true,
"permissions": {
"project": ["read", "update", "archive"],
"track": ["create", "read", "update", "delete", "assign_lead"],
"task": ["create", "read", "update", "complete", "assign"],
"plan": ["read", "update"],
"registry": ["read"],
"sync": ["push", "pull"],
"agent": ["invoke_any"]
}
},
{
"name": "project_contributor",
"scope": "project",
"is_system_role": true,
"permissions": {
"project": ["read"],
"track": ["read", "update_assigned"],
"task": ["create", "read", "update_assigned", "complete_assigned", "self_assign"],
"plan": ["read", "update_task_content_assigned"],
"sync": ["push", "pull"],
"agent": ["invoke_track_scoped", "invoke_read_only"]
}
},
{
"name": "project_viewer",
"scope": "project",
"is_system_role": true,
"permissions": {
"project": ["read"],
"track": ["read"],
"task": ["read"],
"plan": ["read"],
"sync": ["pull"],
"agent": ["invoke_read_only"]
}
},
{
"name": "track_lead",
"scope": "track",
"is_system_role": true,
"permissions": {
"project": ["read"],
"track": ["read", "update_assigned"],
"task": ["create", "read", "update_assigned", "complete_assigned", "assign_in_track", "self_assign"],
"plan": ["read", "update_task_content_assigned", "create_checkpoint"],
"sync": ["push", "pull"],
"agent": ["invoke_track_scoped", "invoke_read_only"]
}
},
{
"name": "agent_system",
"scope": "variable",
"is_system_role": true,
"permissions": {
"_note": "Effective permissions are computed at runtime as intersection of invoking user role and agent policy",
"max_inheritable": "project_contributor"
}
}
]

Appendix B: Governance Hook Integration

The rbac_enforcer.py hook integrates with the existing hook chain defined in ADR-074:

{
"hooks": [
{
"type": "PreToolUse",
"command": "python3 ~/.coditect/hooks/task_id_validator.py",
"description": "Validates task ID format on tool calls"
},
{
"type": "PreToolUse",
"command": "python3 ~/.coditect/hooks/rbac_enforcer.py",
"description": "Validates user role and project scope before tool execution"
}
]
}

The rbac_enforcer.py hook receives the same input as other PreToolUse hooks (tool name, parameters, description) and returns either {"decision": "allow"} or {"decision": "deny", "reason": "..."}.


ADR-145 | Author: Hal Casteel | Date: 2026-02-02 Track: D (Security) | Status: PROPOSED