ADR-159: Multi-Tenant Command Architecture
Status
PROPOSED (2026-02-06)
Context
CODITECT has 366 commands (as audited in H.12.1, 2026-02-08). The majority (76.2%) already operate at project scope, but 25 commands (6.8%) have gaps where they touch shared databases or perform git operations without proper project routing. As the platform evolves toward multi-tenant SaaS with customer projects (CUST-*), these gaps need systematic resolution.
Audit results (H.12.1):
| Scope Level | Count | Percentage |
|---|---|---|
| Global | 52 | 14.2% |
| Machine | 13 | 3.6% |
| Session | 20 | 5.5% |
| Project | 279 | 76.2% |
| Tenant | 2 | 0.5% |
25 commands with scope gaps, categorized:
- 8 DB-touching commands without
--projectfilter (data leakage risk) - 12 git operation commands without project routing (code mixing risk)
- 5 log operation commands without project scoping (noise risk)
Current state:
/session-logalready implements--project(ADR-155) - serves as the pattern template/sync-logspushes all logs to a central repo regardless of project ownership/cxextracts ALL sessions into a single unified store/cxqsearches across all projects with no isolation/backupbacks up everything globally/orientand/checkpointload global state without project context
Triggering event: Customer project CUST-avivatec-fpa has its own dedicated git repo, but /sync-logs routes to the central repo. H.12.1 audit confirmed this pattern exists across 25 commands.
Full audit data: internal/analysis/multi-tenant-command-audit/COMMAND-SCOPE-AUDIT.md
Related ADRs:
- ADR-155 established project-scoped session logs (the first command to get project awareness)
- ADR-156 established project-scoped context databases
- ADR-053 defined cloud context sync (tenant isolation at cloud level)
- ADR-118 defined four-tier database architecture (data separation)
Decision
We adopt a consistent multi-tenant command architecture with the following design:
1. Scope Hierarchy
All scope-aware commands operate within a 5-level hierarchy:
Tenant (organization: AZ1.AI, Avivatec, etc.)
└─ Team (department: engineering, product, etc.)
└─ Project (work scope: PILOT, CUST-avivatec-fpa, etc.)
└─ Machine (physical: d3d3a316-...)
└─ Session (ephemeral: uuid)
2. Standard Command Parameters
Every scope-aware command receives the same 3 optional flags:
| Flag | Type | Description | Example |
|---|---|---|---|
--project <id> | string | Filter/route by project | --project PILOT |
--team <id> | string | Filter/route by team | --team engineering |
--tenant <id> | string | Filter/route by tenant | --tenant avivatec |
Auto-detection: When no flag is provided, commands auto-detect the project from the working directory using discover_project() from scripts/core/paths.py.
Scope inheritance: --tenant implies all projects/teams within that tenant. --team implies all projects within that team.
3. Config Discovery Pattern
Commands resolve configuration via cascading lookup (most specific wins):
1. Explicit CLI flag --project PILOT
2. Environment variable $CODITECT_PROJECT
3. Working directory detect discover_project()
4. Project config config/projects/{id}/config.json
5. Tenant config config/tenants/{id}/config.json
6. Global config config/config.json
Config file structure:
// config/projects/CUST-avivatec-fpa/config.json
{
"project_id": "CUST-avivatec-fpa",
"tenant_id": "avivatec",
"session_log_repo": "https://github.com/coditect-ai/coditect-CUST-avivatec-fpa-session-logs.git",
"backup_bucket": "gs://coditect-customer-avivatec-backups",
"context_db_isolation": true,
"data_redaction": true
}
4. Data Routing
Commands that write or sync data use a routing table to determine the destination:
// config/session-log-repos.json (example for /sync-logs)
{
"default": "https://github.com/coditect-ai/coditect-core-sessions-logs.git",
"projects": {
"PILOT": "https://github.com/coditect-ai/coditect-core-sessions-logs.git",
"CUST-avivatec-fpa": "https://github.com/coditect-ai/coditect-CUST-avivatec-fpa-session-logs.git"
}
}
Routing resolution:
- Check
projects.{project_id}in routing config - Check
tenants.{tenant_id}in routing config - Fall back to
default
5. Data Isolation Requirements
| Scope Level | Isolation Rule |
|---|---|
| Tenant | CUST-* data MUST NOT leak to other tenants or central stores without explicit config |
| Project | Project data tagged with project_id in all stores (sessions.db, org.db, exports) |
| Team | Optional grouping; no hard isolation required in v1 |
| Machine | Already isolated via machine UUID (ADR-058) |
| Session | Already isolated via session UUID |
Customer data protection:
- CUST-* projects enforce
data_redaction: trueby default - PII check runs before any sync/export to external stores
- Directory permissions
0o700for CUST-* project data
6. Command Enhancement Matrix (Updated per H.12.1 Audit)
Commands are categorized by enhancement priority based on the 366-command audit:
Phase 0 - Already Done:
| Command | Scope Support | ADR |
|---|---|---|
/session-log | --project | ADR-155 |
Phase 1 - DB Query Isolation (Tier 1, 2-3 hours):
| Command | Gap | Fix |
|---|---|---|
/session-status | DB query unscoped | Add WHERE project_id = ? filter |
/session-conflicts | DB query unscoped | Add project filter to conflict detection |
/summaries | DB query unscoped | Scope summary queries by project |
/trajectory | DB query unscoped | Add project filter to trajectory data |
Phase 2 - Git/Data Routing (Tier 2, 6-8 hours):
| Command | Gap | Task |
|---|---|---|
/sync-logs | Single repo assumption | H.12.2 |
/db-backup | Backs up all project data | H.12.4 |
/commit | No project-specific hooks | - |
/git-sync | Single remote assumption | - |
/session-log-codex | Logs unscoped | - |
Phase 3 - Project Awareness (Tier 3, 6-8 hours):
| Command | Gap | Task |
|---|---|---|
/bi | DB query unscoped | - |
/component-activate | DB write unscoped | - |
/plugin | DB operations unscoped | - |
/search-scripts | Results unscoped | - |
/quality-gate | Git checks unscoped | - |
/weekly-digest | Data aggregation unscoped | - |
/update | Git operations unscoped | - |
/update-plan | Git operations unscoped | - |
/project-plan-update | Git operations unscoped | - |
Phase 4 - Cleanup (Tier 4, opportunistic):
| Command | Gap |
|---|---|
/markdown-cleanup | File operations unscoped |
/pilot | Log output unscoped |
/alias | Git operations |
/audit-trail | Git operations |
/component-lifecycle | Git operations |
/lowercase-migration | File operations |
/submodule-init | Git operations |
341 commands (93.2%) require no changes - already well-scoped or stateless.
7. Implementation Pattern
Each command enhancement follows this pattern:
# Standard scope resolution (add to scripts/core/scope.py)
def resolve_scope(
project: str | None = None,
team: str | None = None,
tenant: str | None = None,
) -> ScopeContext:
"""Resolve scope from flags, env vars, or auto-detection."""
# 1. Explicit flags take precedence
if project:
return ScopeContext(project=project, team=team, tenant=tenant)
# 2. Environment variable
env_project = os.environ.get("CODITECT_PROJECT")
if env_project:
return ScopeContext(project=env_project, team=team, tenant=tenant)
# 3. Auto-detect from working directory
from scripts.core.paths import discover_project
detected = discover_project()
if detected:
return ScopeContext(project=detected, team=team, tenant=tenant)
# 4. Global scope (no project context)
return ScopeContext(project=None, team=team, tenant=tenant)
def get_project_config(project_id: str) -> dict:
"""Load project-specific config with tenant fallback."""
project_config = f"config/projects/{project_id}/config.json"
if os.path.exists(project_config):
return json.load(open(project_config))
# Infer tenant from project prefix
if project_id.startswith("CUST-"):
tenant_id = project_id.split("-")[1]
tenant_config = f"config/tenants/{tenant_id}/config.json"
if os.path.exists(tenant_config):
return json.load(open(tenant_config))
return {} # Global defaults
Consequences
Positive
- Consistent UX: All commands use the same
--project/--team/--tenantflags - Customer data isolation: CUST-* project data stays within tenant boundaries
- Auto-detection: Most commands work without flags via
discover_project() - Backward compatible: No flags = current global behavior preserved
- Incremental adoption: Commands can be enhanced one at a time following the standard pattern
Negative
- Config proliferation: Each customer project may need its own config directory
- Audit overhead: H.12.1 audit (366 commands) is complete; 25 gaps identified across 4 priority tiers
- Testing complexity: Each command needs scope-aware test cases
Neutral
- ADR-155 already established the pattern - this ADR formalizes and extends it
- Cloud sync (ADR-053) already handles tenant isolation at the API level - this brings the same isolation to local CLI commands
Implementation Plan
| Phase | Tasks | Timeline | Dependency |
|---|---|---|---|
| 0 | H.12.1: Command scope audit (366 commands) | COMPLETE | None |
| 1 | Create scripts/core/scope.py shared module | Next | H.12.1 |
| 2 | Phase 1: DB query isolation (4 commands, 2-3 hrs) | After Phase 1 | scope.py |
| 3 | H.12.2: /sync-logs project routing (7 subtasks) | Parallel | None |
| 4 | Phase 2: Git/data routing (5 commands, 6-8 hrs) | After scope.py | scope.py |
| 5 | H.12.4, H.12.5, Phase 3-4: Remaining (16 commands) | After Phase 4 | scope.py |