Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f84b3a8
feat(enterprise): private submodule + EntitlementService seam + SSO P…
dolho May 21, 2026
6b4402b
feat(enterprise): dual-mount submodule + frontend SSO view (#847 Phas…
dolho May 22, 2026
4855233
refactor(enterprise): frontend in OSS, EntitlementService registry (#…
dolho May 22, 2026
07b6564
feat(enterprise): SSO admin UI mock + Login page provider buttons (#847)
dolho May 22, 2026
5761811
fix(ci): lint + first-time-setup wiring for build-without-submodule
dolho May 22, 2026
c9020ab
fix(ci): complete setup via docker exec, not API grep
dolho May 22, 2026
3316479
fix: print enterprise registration status instead of logger.info
dolho May 22, 2026
2a6cbfb
fix(tests): widen except-ImportError search window in main.py static …
dolho May 22, 2026
08ba5ba
docs: enterprise modules feature flow with code links
dolho May 22, 2026
9845286
feat(audit): enterprise audit log dashboard (#941) + remove SSO PoC
dolho May 26, 2026
74c00db
docs(security): CSO --diff report for #941 audit dashboard
dolho May 26, 2026
ec348f5
docs(audit): sync feature-flows index + add Phase 5 tests section (#941)
dolho May 26, 2026
745a5a9
feat(audit-dashboard): stats tiles, time presets, drill-down, verify,…
dolho May 26, 2026
ccb65df
feat(audit-dashboard): dow×hour + GitHub-style calendar heatmaps + fo…
dolho May 27, 2026
d5f17f3
fix(ci): sys-modules lint + OSS-only skip for audit dashboard e2e (#941)
dolho May 27, 2026
1ec5d88
feat(deploy): enterprise overlay so dev VM ships audit dashboard (#847)
dolho May 27, 2026
c0396a7
test(api-keys-copy): swap UI cleanup for API DELETE — fits 30s budget…
dolho May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions .github/workflows/build-without-submodule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: build-without-submodule

# #847 Phase 0 — guard against the OSS-only break.
#
# The public Trinity backend conditionally imports the private
# enterprise submodule via `try: from enterprise import register_enterprise`
# in `src/backend/main.py`. This job proves the conditional import works
# correctly when the submodule is **absent** — i.e. when a customer
# without enterprise repo access clones the public repo.
#
# Without this gate, a future change to the enterprise loader could
# accidentally hard-require the submodule and break OSS-only deployments
# on the next release. The first signal would be customers reporting
# `ModuleNotFoundError: No module named 'enterprise'` after `git pull`.
#
# The job runs:
# 1. Checkout without submodules
# 2. Build the backend Docker image
# 3. Start it on a temp network
# 4. Assert /health responds and /api/settings/feature-flags returns
# `enterprise_features: []`
#
# Triggers: every PR + every push to dev/main. Issue #847.

on:
pull_request:
branches: [dev, main]
push:
branches: [dev, main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: build-without-submodule-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-boot:
name: backend boots without enterprise submodule
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout WITHOUT submodules
uses: actions/checkout@v4
with:
submodules: false # explicit — the whole point of this job
fetch-depth: 1
persist-credentials: false

- name: Confirm enterprise submodule is absent
run: |
# Single mount point (#847). Enterprise frontend ships in
# the OSS bundle and is gated server-side via feature-flags;
# the only submodule is the private backend at
# src/backend/enterprise/.
if [ -f src/backend/enterprise/backend/__init__.py ]; then
echo "::error::src/backend/enterprise/backend/__init__.py present — checkout pulled the submodule. The job must run without it."
exit 1
fi
echo "OK: enterprise submodule absent as expected"

- name: Generate ephemeral secrets for boot
run: |
# ADMIN_PASSWORD must meet OWASP ASVS 2.1 complexity
# (length 12+, upper, lower, digit, special) since
# /api/setup/admin-password validates it. Use a fixed
# known-strong test password — this container is ephemeral
# and CI-only, no security exposure.
{
echo "SECRET_KEY=$(openssl rand -hex 32)"
echo "ADMIN_PASSWORD=Trinity-CI-Test-2026!Aa1"
echo "REDIS_PASSWORD=$(openssl rand -hex 24)"
echo "REDIS_BACKEND_PASSWORD=$(openssl rand -hex 24)"
echo "CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -hex 32)"
echo "INTERNAL_API_SECRET=$(openssl rand -hex 32)"
} > .env

- name: Build backend image
run: docker compose build backend

- name: Boot the stack
run: |
docker compose up -d backend redis vector
# Wait up to 60s for backend /health
for i in $(seq 1 30); do
if curl -sf http://localhost:8000/health > /dev/null; then
echo "Backend healthy after ${i}x2s"
break
fi
sleep 2
done
curl -sf http://localhost:8000/health || (
echo "::error::Backend never became healthy"
docker logs trinity-backend --tail 100
exit 1
)

- name: Complete first-time setup
run: |
# A fresh DB requires first-time setup before /api/token works
# (`is_setup_completed()` is checked in routers/auth.py). The
# setup-token is `print()`ed to stdout but Python's block
# buffering on a pipe + the docker-logs capture means we
# can't reliably grep it on a short timing window. Skip the
# API and complete setup directly: docker exec into the
# backend container, hash the password via the same
# `dependencies.hash_password` helper the production
# endpoint uses, write to the SQLite `users` row, set
# `system_settings.setup_completed=true`. Idempotent.
ADMIN_PASSWORD=$(grep '^ADMIN_PASSWORD=' .env | cut -d= -f2)
docker exec -e ADMIN_PW="$ADMIN_PASSWORD" trinity-backend python3 -c "
import os
from database import db
from dependencies import hash_password
db.set_setting('setup_completed', 'true')
db.update_user_password('admin', hash_password(os.environ['ADMIN_PW']))
print('setup completed via direct DB write')
"

- name: Assert OSS-only feature flags
run: |
ADMIN_PASSWORD=$(grep '^ADMIN_PASSWORD=' .env | cut -d= -f2)
TOKEN=$(curl -sf -X POST http://localhost:8000/api/token \
-d "username=admin&password=${ADMIN_PASSWORD}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
FLAGS=$(curl -sf -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/settings/feature-flags)
echo "feature-flags response: $FLAGS"
ENT=$(echo "$FLAGS" | python3 -c "import sys,json; print(json.load(sys.stdin)['enterprise_features'])")
if [ "$ENT" != "[]" ]; then
echo "::error::Expected enterprise_features=[] in OSS-only build, got: $ENT"
exit 1
fi
echo "OK: enterprise_features=[] as expected"

- name: Assert SSO router not mounted
run: |
# No auth needed — the /api/enterprise/sso/* router is
# entitlement-gated (not user-gated). When the submodule
# is absent, the router never mounts and any path under
# /api/enterprise returns 404.
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
http://localhost:8000/api/enterprise/sso/providers)
if [ "$CODE" != "404" ]; then
echo "::error::Expected 404 from /api/enterprise/sso/providers in OSS-only build, got HTTP $CODE"
exit 1
fi
echo "OK: /api/enterprise/sso/providers returns 404 as expected"

- name: Confirm OSS-only log line was emitted
run: |
# The main.py except branch logs an informational message we
# can grep for. Catches a future regression where the
# ImportError is silently swallowed without a log.
if ! docker logs trinity-backend 2>&1 | grep -q "Trinity Enterprise submodule not present"; then
echo "::error::Expected 'Trinity Enterprise submodule not present' log line, did not find it"
docker logs trinity-backend --tail 60
exit 1
fi
echo "OK: OSS-only mode logged"

- name: Tear down
if: always()
run: docker compose down -v --remove-orphans
29 changes: 27 additions & 2 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,36 @@ jobs:
git pull origin dev
echo "Version: $(git log -1 --oneline)"

echo "=== Submodule (enterprise) ==="
# #847 — try to init the private trinity-enterprise submodule
# so the dev deploy lights up enterprise features (audit
# dashboard, etc.). If the VM lacks read access to the private
# repo, this is a non-fatal warning — the conditional import
# in main.py falls back to OSS-only mode and the deploy
# continues. Enterprise UI surfaces simply stay hidden.
if git submodule update --init --recursive src/backend/enterprise; then
ENT_SHA=$(git -C src/backend/enterprise rev-parse --short HEAD 2>/dev/null || echo "unknown")
echo "ENTERPRISE: initialized at ${ENT_SHA}"
else
echo "::warning::Enterprise submodule init failed — deploying in OSS-only mode"
echo "ENTERPRISE: confirm the dev VM has read access to Abilityai/trinity-enterprise"
echo "ENTERPRISE: (deploy key, PAT in a credential helper, or org membership all work)"
fi

# Compose file list: base prod + enterprise overlay. The
# overlay bind-mounts ./src/backend/enterprise into
# /app/enterprise inside the backend container. If the
# submodule init above failed, the host directory will be
# empty (or contain just .git), and the conditional import
# in main.py will fall through to OSS-only mode — same
# outcome as if the overlay weren't applied.
COMPOSE_FILES="-f docker-compose.prod.yml -f docker-compose.prod.enterprise.yml"

echo "=== Build ==="
sudo docker compose -f docker-compose.prod.yml build --no-cache backend frontend mcp-server scheduler
sudo docker compose ${COMPOSE_FILES} build --no-cache backend frontend mcp-server scheduler

echo "=== Restart ==="
sudo docker compose -f docker-compose.prod.yml up -d backend frontend mcp-server scheduler
sudo docker compose ${COMPOSE_FILES} up -d backend frontend mcp-server scheduler

echo "=== Health ==="
sleep 10
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
url = https://github.com/Abilityai/trinity-dev.git
update = checkout
fetchRecurseSubmodules = true
[submodule "src/backend/enterprise"]
path = src/backend/enterprise
url = git@github.com:Abilityai/trinity-enterprise.git
38 changes: 38 additions & 0 deletions docker-compose.prod.enterprise.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Enterprise overlay for production deployments (#847).
#
# Layered ON TOP of docker-compose.prod.yml when the deployment includes
# the closed-source `trinity-enterprise` submodule:
#
# docker compose -f docker-compose.prod.yml -f docker-compose.prod.enterprise.yml \
# build --no-cache backend frontend mcp-server scheduler
# docker compose -f docker-compose.prod.yml -f docker-compose.prod.enterprise.yml \
# up -d backend frontend mcp-server scheduler
#
# What this does:
# * Bind-mounts the host's `src/backend/enterprise/` (populated by
# `git submodule update --init --recursive` before compose runs)
# into `/app/enterprise/` inside the container.
# * Python's import path is `/app` (Dockerfile WORKDIR), so the file
# `/app/enterprise/backend/__init__.py` resolves the conditional
# `from enterprise.backend import register_enterprise` in main.py.
# * Read-only mount — the container must never write into the
# submodule tree. Defense-in-depth against accidental modification.
#
# Why a bind-mount overlay, not a Dockerfile COPY:
# * Keeps the BASE backend image bit-identical to the OSS prod image
# so the public image's contents stay auditable.
# * `build-without-submodule.yml` CI guards OSS-only boot; that gate
# would have to be revisited if the Dockerfile gained an enterprise
# COPY (even guarded by an ARG).
# * Updating to a new enterprise SHA only requires
# `git submodule update --remote && docker compose up -d backend` —
# no rebuild.
#
# For an air-gapped customer who needs a self-contained tarballed image,
# we'll add a Dockerfile.enterprise variant + build-arg gate as a
# follow-up. SaaS dev/staging deployments use this overlay.

services:
backend:
volumes:
- ./src/backend/enterprise:/app/enterprise:ro
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ services:
# owned by the developer UID, not container UID 1000, so the writes
# fail. macOS Docker Desktop masks the failure; set this for parity.
- PYTHONDONTWRITEBYTECODE=1
# #847 — force OSS-only mode even when the enterprise submodule
# is mounted. With this set, EntitlementService.is_entitled()
# returns False for every feature, and the /api/settings/feature-flags
# `enterprise_features` list is empty. Default (unset = 0): allow
# entitlements based on license (Phase 0 stub: all-entitled).
- TRINITY_OSS_ONLY=${TRINITY_OSS_ONLY:-0}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./src/backend:/app
Expand Down
Loading
Loading