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:
- Internal CODITECT team -- Platform administrators and framework developers who need broad access to all tenants, tracks, and system configuration.
- External customers -- Organizations that purchase CODITECT licenses and operate within their own isolated tenant, managing their own projects, teams, and users.
- 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:
| Risk | Impact |
|---|---|
| No project-level permissions | Any authenticated user within a tenant can modify any project |
| No track ownership model | Track files have no formal access gating; anyone can edit any track |
| Agent privilege escalation | Agents execute with the full permissions of their invoking context, with no scoped restrictions |
| No contributor scoping | Open-source contributors have no formal permission boundary |
| Audit gap | No 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_idfield 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
- RBAC must work both offline (local CLI) and online (cloud API) with eventual consistency
- The local enforcement layer must degrade gracefully when the cloud is unreachable
- Agent permissions must be scoped without requiring agents to authenticate independently
- The system must support the existing multi-tenant hierarchy: Platform > Tenant > Team > Project > User
- 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:
| Role | Scope | Description |
|---|---|---|
| Platform Admin | Platform | Full access to all tenants, projects, system configuration, and platform operations. Reserved for AZ1.AI internal operators. |
| Org Admin | Tenant | Full administrative control within a single tenant: manage projects, teams, users, billing, and tenant-level configuration. |
| Project Owner | Project | Full control of a specific project: plans, tracks, tasks, members, agent configuration, and project-level settings. |
| Project Contributor | Project | Read/write access to tasks, plans, and code within assigned tracks of a specific project. Cannot modify project settings or membership. |
| Project Viewer | Project | Read-only access to project plans, task status, reports, and dashboards. Cannot modify any project resource. |
| Track Lead | Track(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) | Variable | Automated 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
| Operation | Platform Admin | Org Admin | Project Owner | Contributor | Viewer | Track Lead | Agent |
|---|---|---|---|---|---|---|---|
| Create project | Y | Y | N | N | N | N | N |
| Read project metadata | Y | Y | Y | Y | Y | Y | Y* |
| Update project settings | Y | Y | Y | N | N | N | N |
| Archive project | Y | Y | Y | N | N | N | N |
| Delete project | Y | N | N | N | N | N | N |
| List projects in tenant | Y | Y | Y | Y** | 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
| Operation | Platform Admin | Org Admin | Project Owner | Contributor | Viewer | Track Lead | Agent |
|---|---|---|---|---|---|---|---|
| Create track in project | Y | Y | Y | N | N | N | N |
| Read track file | Y | Y | Y | Y | Y | Y | Y* |
| Modify tasks in track | Y | Y | Y | Y*** | N | Y**** | Y* |
| Mark tasks complete | Y | Y | Y | Y*** | N | Y**** | Y* |
| Reassign track ownership | Y | Y | Y | N | N | N | N |
| Delete/archive track | Y | Y | Y | N | N | N | N |
* 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
| Operation | Platform Admin | Org Admin | Project Owner | Contributor | Viewer | Track Lead | Agent |
|---|---|---|---|---|---|---|---|
| Assign task to user | Y | Y | Y | N | N | Y* | N |
| Assign task to agent | Y | Y | Y | N | N | Y* | N |
| Self-assign task | Y | Y | Y | Y | N | Y | N |
| View task assignments | Y | Y | Y | Y | Y | Y | Y** |
* Track Leads can assign tasks only within their assigned tracks. ** Agent visibility is scoped to the current project context.
2.4 Plan Modification
| Operation | Platform Admin | Org Admin | Project Owner | Contributor | Viewer | Track Lead | Agent |
|---|---|---|---|---|---|---|---|
| Edit PROJECT-PLAN.md | Y | Y | Y | N | N | N | N |
| Edit track files (structure) | Y | Y | Y | N | N | N | N |
| Edit task content in track | Y | Y | Y | Y* | N | Y** | Y*** |
| Create checkpoints | Y | Y | Y | Y | N | Y | N |
| View plan/checkpoints | Y | Y | Y | Y | Y | Y | Y*** |
* 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
| Operation | Platform Admin | Org Admin | Project Owner | Contributor | Viewer | Track Lead | Agent |
|---|---|---|---|---|---|---|---|
| View project registry | Y | Y | Y | N | N | N | N |
| Modify registry entries | Y | Y | N | N | N | N | N |
| Cross-project search | Y | Y | N | N | N | N | N |
| View other tenant's projects | Y | N | N | N | N | N | N |
2.6 Cloud Sync & Agent Invocation
| Operation | Platform Admin | Org Admin | Project Owner | Contributor | Viewer | Track Lead | Agent |
|---|---|---|---|---|---|---|---|
| Push to cloud API | Y | Y | Y | Y | N | Y | Y* |
| Pull from cloud API | Y | Y | Y | Y | Y | Y | Y* |
| Invoke any agent | Y | Y | Y | N | N | N | N |
| Invoke track-scoped agents | Y | Y | Y | Y | N | Y | N |
| Invoke read-only agents | Y | Y | Y | Y | Y | Y | N |
* 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:
- Reads the active project context from
~/.coditect-data/active-project.json - Reads the user's role assignment from
~/.coditect-data/rbac/role-cache.json - Checks the requested operation against the permission matrix
- If the cloud API is reachable, validates the role cache is fresh (max age: 5 minutes)
- If the cloud API is unreachable, uses the cached role (offline mode)
- Blocks the operation with an explanatory error if the role is insufficient
- 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:
- User invokes agent via
/agent <name> "task"orTask(subagent_type=..., prompt=...) - The agent dispatcher (
scripts/core/agent_dispatcher.py) reads the invoking user's active role - The dispatcher reads the agent's
rbacfrontmatter policy - The effective permission set is computed as the intersection
- The agent session is initialized with a scoped context that includes only the permitted operations
- If the agent attempts an operation outside its effective permissions,
rbac_enforcer.pyblocks 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 │
│ │
└──────────────────────────────────────────────────────────┘
- On session start,
machine-id.jsonand the active license key identify the user and machine - The system checks
~/.coditect-data/rbac/role-cache.jsonfor cached role assignments - If the cache is stale (older than
cache_ttl_seconds) and the cloud API is reachable, the cache is refreshed viaGET /api/v1/rbac/my-roles/ - If the cloud API is unreachable, the stale cache is used with a warning logged
- The active project context (from
~/.coditect-data/active-project.jsonor project registry) determines the project scope - 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 │
│ │
└──────────────────────────────────────────────────────────┘
- Users authenticate via OAuth2 at
auth.coditect.ai, receiving a JWT pair (access token + refresh token) - The access token (15-minute expiry) contains embedded role claims to avoid per-request database lookups
- The refresh token (7-day expiry) is stored securely in
~/.coditect-data/rbac/auth-tokens.jsonwith file permissions0600 - The cloud API validates the JWT signature (RS256), checks expiration, binds the tenant context, and extracts roles for permission checking
- 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:
| Layer | Mechanism | Failure Mode |
|---|---|---|
| Database | django-multitenant auto-filters all queries by tenant_id | If bypassed, data leaks across tenants |
| API | MultitenantMiddleware sets tenant context from JWT | If JWT is forged, middleware catches mismatched tenant |
| RBAC | Role assignments are tenant-scoped; RoleAssignment.tenant_id is always checked | Cross-tenant role grants are structurally impossible |
| Local | role-cache.json includes tenant_id; project context includes tenant_id from ADR-144 | Offline cache is tenant-bound |
| Audit | All audit entries include tenant_id for tenant-scoped audit queries | Audit trail is per-tenant |
Cross-Tenant Boundary Rules
- Customer data NEVER crosses tenant boundaries. The
django-multitenantlibrary enforces this at the ORM layer. There is no API endpoint that accepts atenant_idparameter -- the tenant is always derived from the authenticated JWT. - Local
org.dbstores internal CODITECT project data only. Customer project data resides exclusively in the cloud PostgreSQL database, scoped bytenant_id. The localorg.db(ADR-118, Tier 2) contains only the internal CODITECT organization's decisions, learnings, and error solutions. - Project registry entries are filtered by
tenant_id. As defined in ADR-144, every project registry entry includes atenant_idfield. The RBAC system enforces that users can only query and modify registry entries within their own tenant. - 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
| Attribute | Value |
|---|---|
| Tenant | az1-internal (reserved tenant ID) |
| Default Roles | Platform Admin (infrastructure team), Org Admin (framework developers) |
| Track Access | All Tier 1 tracks (A-N), plus Tier 2 business tracks (O-AA) |
| Special Permissions | Cross-tenant visibility for support, system configuration, platform metrics |
| Authentication | OAuth2 via auth.coditect.ai with Google Workspace SSO |
External Customers
| Attribute | Value |
|---|---|
| Tenant | Customer-specific tenant ID (provisioned at onboarding) |
| Default Roles | Org Admin (account owner), Project Owner (per project), Contributor/Viewer (team members) |
| Track Access | Tracks defined in their projects; Tier 2 and Tier 3 tracks from config/tracks.json |
| Special Permissions | None beyond their tenant boundary |
| Authentication | OAuth2 via auth.coditect.ai with optional SSO (SAML/OIDC) |
Open-Source Contributors
| Attribute | Value |
|---|---|
| Tenant | oss-community (shared tenant for open-source contributions) |
| Default Roles | Project Viewer (all public projects), Project Contributor (approved public tracks only) |
| Track Access | Public framework tracks only (subset of Tier 1, as designated by Platform Admin) |
| Special Permissions | Cannot create projects, cannot access customer data, cannot invoke privileged agents |
| Authentication | OAuth2 via auth.coditect.ai with GitHub SSO |
| Promotion Path | Consistent 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:
| Endpoint | Method | Permission | Purpose |
|---|---|---|---|
/api/v1/rbac/roles/ | GET | Org Admin+ | List roles in tenant |
/api/v1/rbac/roles/ | POST | Platform Admin | Create custom role |
/api/v1/rbac/assignments/ | GET | Project Owner+ | List role assignments |
/api/v1/rbac/assignments/ | POST | Org Admin+ | Assign role to user |
/api/v1/rbac/assignments/{id}/ | DELETE | Org Admin+ | Revoke role assignment |
/api/v1/rbac/my-roles/ | GET | Any authenticated | Get own role assignments |
/api/v1/rbac/check/ | POST | Any authenticated | Check permission for operation |
/api/v1/rbac/audit/ | GET | Org Admin+ | Query audit log |
Local (Cache)
Role data is cached locally for offline operation:
| File | Purpose | TTL |
|---|---|---|
~/.coditect-data/rbac/role-cache.json | Cached role assignments for current user | 5 minutes (online), unlimited (offline) |
~/.coditect-data/rbac/auth-tokens.json | JWT access + refresh tokens (mode 0600) | 15 min access, 7 day refresh |
~/.coditect-data/rbac/audit-log.jsonl | Local audit trail (synced to cloud) | 30 days local retention |
~/.coditect-data/active-project.json | Active project context (existing) | Session-scoped |
Consequences
Positive
- 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.
- 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.
- 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.
- Offline Capability. The local role cache enables RBAC enforcement even when the cloud API is unreachable, maintaining security guarantees in disconnected environments.
- 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.
- 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.
- Track-Level Granularity. Track Leads can manage their domain independently, enabling parallel development across multiple tracks without permission conflicts.
Negative
- Complexity. Seven roles, three enforcement layers, and online/offline synchronization add significant complexity to the system. Role resolution logic must be thoroughly tested.
- Latency. The
rbac_enforcer.pyhook 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. - 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).
- Migration Effort. Existing internal users must be assigned roles, and the governance hooks must be updated to include the new
rbac_enforcer.pyin the PreToolUse chain.
Neutral
- No Change to Tenant Isolation.
django-multitenantcontinues to be the primary tenant isolation mechanism. RBAC adds authorization within a tenant but does not replace the existing isolation model. - 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)
- Define the seven system roles as seed data in the cloud database
- Create Django models:
Role,RoleAssignment,AuditEntry - Implement
RBACPermissionMiddlewarefor the cloud API - Create REST API endpoints for role management
- Write database migrations with rollback support
Phase 2: Local Enforcement (Week 3-4)
- Implement
rbac_enforcer.pygovernance hook - Create local role cache (
role-cache.json) with sync logic - Integrate with existing
task_id_validator.pyhook chain - Implement offline degradation mode with warning logging
- Create local audit log (
audit-log.jsonl) with cloud sync
Phase 3: Agent Integration (Week 5-6)
- Define
rbacfrontmatter schema for agent definitions - Update
agent_dispatcher.pywith delegated authority logic - Implement scoped agent token generation
- Update existing agent definitions with appropriate
rbacpolicies - Add agent permission checks to
rbac_enforcer.py
Phase 4: User Migration & Testing (Week 7-8)
- Assign roles to all internal CODITECT team members
- Create the
oss-communitytenant and seed contributor roles - Conduct security review of all enforcement points
- Run penetration testing focused on tenant boundary violations
- Perform load testing on
rbac_enforcer.pyto verify sub-10ms performance - Write integration tests for all role-permission combinations
Security Considerations
Threat Model
| Threat | Mitigation |
|---|---|
| Privilege escalation via agent | Delegated authority model caps agent permissions at invoking user's role |
| Cross-tenant data access | Four-layer defense: database isolation, API middleware, RBAC scoping, local cache tenant binding |
| Stolen JWT token | Short-lived access tokens (15 min), refresh token rotation, IP binding in audit log |
| Role cache tampering | Cache is validated against cloud API on refresh; tampered cache only persists until next sync |
| Offline role bypass | Local audit log captures all offline decisions; anomalies are flagged on next cloud sync |
| API endpoint enumeration | All 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
- JWT signing uses RS256 with key rotation every 90 days
- Refresh tokens are stored encrypted at rest using the machine's keychain (macOS Keychain / Linux Secret Service)
- Local audit logs are signed with HMAC-SHA256 using a per-machine key derived from
machine-id.jsonto detect tampering - All API communication uses TLS 1.3 exclusively
Rate Limiting
| Endpoint | Rate Limit | Scope |
|---|---|---|
/api/v1/rbac/check/ | 1000 req/min | Per user |
/api/v1/rbac/assignments/ POST | 50 req/min | Per tenant |
/api/v1/rbac/audit/ | 100 req/min | Per user |
| Authentication endpoints | 10 req/min | Per IP |
Compliance Notes
SOC2 (Trust Services Criteria)
| Criteria | How ADR-145 Addresses It |
|---|---|
| CC6.1 Logical access security | Role-based access with principle of least privilege; seven defined roles with explicit permission boundaries |
| CC6.2 Access provisioning | Role assignments are tracked with granted_by, granted_at, and expires_at fields; Org Admins manage access within their tenant |
| CC6.3 Access removal | Role assignments can be revoked via API; expired assignments are automatically inactive; 5-minute propagation to local cache |
| CC6.6 System boundaries | Tenant isolation enforced at database, API, RBAC, and local cache layers; cross-tenant access is structurally prevented |
| CC7.1 Monitoring | Immutable audit log captures all access decisions (ALLOW/DENY) with user, action, resource, role, timestamp, and IP address |
| CC7.2 Anomaly detection | Offline access decisions are flagged for review on cloud sync; unusual access patterns are detectable via audit log queries |
GDPR (General Data Protection Regulation)
| Requirement | How ADR-145 Addresses It |
|---|---|
| Article 5(1)(f) Integrity and confidentiality | Tenant isolation ensures personal data is processed only within the authorized tenant boundary; RBAC prevents unauthorized access within a tenant |
| Article 25 Data protection by design | Role-based access is enforced at every layer (local, API, agent); default role for new users is Project Viewer (read-only) |
| Article 30 Records of processing | Audit entries record every data access operation with full context, satisfying the requirement for processing activity records |
| Article 32 Security of processing | Multi-layer enforcement, encrypted token storage, signed audit logs, and short-lived credentials provide appropriate security measures |
| Article 17 Right to erasure | Tenant deletion cascades through all role assignments, audit entries, and cached data; the tenant_id scope ensures complete data removal |
| Article 33 Breach notification | Audit log enables rapid identification of affected data subjects in the event of a breach by filtering on tenant_id and resource_type |
Related ADRs
| ADR | Relationship |
|---|---|
| ADR-053 | Cloud Context Sync Architecture -- RBAC enforces permissions on sync operations (push/pull) |
| ADR-074 | Governance Hook Architecture -- rbac_enforcer.py is added to the PreToolUse hook chain |
| ADR-117 | Hierarchical Plan Location Strategy -- Plan scopes (platform, org, customer, project) map to RBAC role scopes |
| ADR-118 | Four-Tier Database Architecture -- Local databases remain internal-only; customer data stays in cloud |
| ADR-144 | Project Registry -- tenant_id field in project registry entries is used for RBAC tenant scoping |
| ADR-054 | Track Nomenclature Extensibility -- Track letters in RBAC track-scoped roles follow this standard |
| ADR-116 | Track-Based Plan Architecture -- Track files are the resources gated by track-level permissions |
| ADR-146 | Unified 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