feat(enterprise): audit log dashboard + entitlement seam (#941, #847)#910
Merged
Conversation
dolho
marked this pull request as draft
May 21, 2026 12:45
dolho
force-pushed
the
feature/847-enterprise-sso-poc
branch
2 times, most recently
from
May 25, 2026 07:11
0668bfb to
7af2583
Compare
Closed
8 tasks
…oC (#847 Phase 0) Issue #847 spike. Establishes the open-core split between the public Trinity backend and a private companion repo `Abilityai/trinity-enterprise` that ships compliance-gating features (SSO, SCIM, SIEM) without merging them into the public codebase. The spike research is in `docs/planning/OSS_ENTERPRISE_SPLIT_RESEARCH.md` (521 lines, six load-bearing decisions, stress-tested against eight other open issues). The condensed decision record is `docs/planning/ENTERPRISE_ARCHITECTURE.md`. What lands in this PR (Phase 0): Public-repo seam (the integration mechanism): * `src/backend/services/entitlement_service.py` — `EntitlementService` Phase 0 stub. Returns True for every `is_entitled(feature_id)` check unless `TRINITY_OSS_ONLY=1` env is set, in which case every check flips False. Module-level singleton + `_set_for_testing` test seam. * `src/backend/dependencies.py:requires_entitlement(feature_id)` — FastAPI dependency factory mirroring `require_role`. Raises HTTP 403 naming the missing feature so the UI can surface a "license required" toast. Lazy-imports the service so tests can swap singletons. * `src/backend/main.py` — conditional `try: from enterprise import register_enterprise except ImportError`. Loaders mounted under `/api/enterprise/*` when the submodule is present; OSS-only builds log an informational message and continue. Idempotent via `app.state.enterprise_registered`. * `src/backend/routers/settings.py` — `/api/settings/feature-flags` extended with `enterprise_features: list[str]`. Drives UI tab visibility. * `.gitmodules` + new submodule mount at `src/backend/enterprise` pointing to the private repo via SSH. * `docker-compose.yml` env pass-through for `TRINITY_OSS_ONLY`. * `.github/workflows/build-without-submodule.yml` — boots the backend without the enterprise submodule and asserts `/health` responds, `/api/settings/feature-flags` returns `enterprise_features: []`, `/api/enterprise/sso/providers` returns 404, and the OSS-only log line is emitted. Catches regressions where the conditional import becomes hard-required. Private-repo PoC content (in `Abilityai/trinity-enterprise`, mounted at `src/backend/enterprise/`): * `__init__.py` — `register_enterprise(app)` single entry point. * `sso/router.py` — `/api/enterprise/sso/{providers,login/{id}}` stubs. `GET /providers` returns the in-process registry (empty by default); `POST /login/{id}` returns 501 "PoC stub" or 404 for unknown id. Both gated by `requires_entitlement("sso")` with a lazy-import fallback so the private repo tests in isolation. * `sso/providers.py` — `SSOProvider` ABC + `StubProvider`. Tests (`tests/unit/test_847_entitlement_seam.py`, 14 cases): * EntitlementService default + TRINITY_OSS_ONLY deny path * Parametrised truthy/falsy env spellings * `requires_entitlement` allow/deny (skip-on-no-passlib for local) * `_set_for_testing` singleton swap * Static check that main.py uses conditional ImportError guard Docs: * `docs/planning/ENTERPRISE_ARCHITECTURE.md` — condensed decision * `docs/planning/OSS_ENTERPRISE_SPLIT_RESEARCH.md` — long-form research * `docs/dev/ENTERPRISE_LOCAL_DEV.md` — 15-min clone-to-running guide * `docs/memory/requirements.md` §34.1 Live verification (local instance): * Submodule mounted at `src/backend/enterprise/` * `GET /api/enterprise/sso/providers` → `[]` * `GET /api/settings/feature-flags` → `enterprise_features: ["sso","scim","siem"]` * `POST /api/enterprise/sso/login/foo` → HTTP 404 "Unknown SSO provider 'foo'" * With `TRINITY_OSS_ONLY=1`: `/api/enterprise/sso/providers` returns 403 "Enterprise feature 'sso' is not licensed", `enterprise_features: []` * Restored default: re-entitled Out of scope (separate follow-up issues): * Phase 1: Ed25519-signed license token + verify path + admin License UI * Phase 2: extract `audit_log` into the submodule as first real enterprise module * Phase 3: prove "core-primitive + enterprise-knob" pattern via #834 * Phase 4: real SSO/SAML implementation (replaces PoC stubs) * MCP entitlement edge for the TypeScript MCP server * Fix repo license-of-record (currently NOASSERTION) — owner decision Related to #847 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e 0.5)
Add the frontend half of the enterprise seam. The private repo
`Abilityai/trinity-enterprise` was restructured into `backend/` and
`frontend/` subdirs, and the public repo now mounts it as TWO submodules
at different paths — same URL, different mount points — so each
consumer reads only its own subdir:
src/backend/enterprise/ → backend/ consumed by Python (`main.py`)
src/frontend/src/enterprise/ → frontend/ consumed by Vite (`main.js`)
One private codebase to version; two clean import surfaces. Disk waste
is ~2× the repo size (the same code cloned twice) which is far cheaper
than two private repos drifting out of sync.
Changes:
Backend (import path bump):
* `src/backend/main.py` — `from enterprise.backend import register_enterprise`
(was `from enterprise import ...`).
* `tests/unit/test_847_entitlement_seam.py` — static-check asserts
the new import path.
Frontend (new):
* `src/frontend/src/main.js` — conditional
`import.meta.glob('./enterprise/frontend/index.js', { eager: false })`.
Empty in OSS-only builds; calls `mod.registerEnterprise(router, app)`
when present. Logs which mode it's in.
* `src/frontend/src/stores/enterprise.js` — new Pinia store. Loads
`/api/settings/feature-flags` after auth, caches
`enterprise_features: list[str]`. Getters: `isEntitled(featureId)`,
`hasAnyEnterprise`. Test seam `_setFeaturesForTest`.
* `src/frontend/src/components/NavBar.vue` — new `Enterprise` link
`v-if="enterpriseStore.isEntitled('sso')"` with `PRO` badge.
Hidden in OSS-only mode and when `TRINITY_OSS_ONLY=1`.
Submodule:
* `.gitmodules` — second entry at `src/frontend/src/enterprise/` for
the same private repo URL.
CI:
* `.github/workflows/build-without-submodule.yml` — asserts BOTH
mount points are empty (`backend/__init__.py` AND
`frontend/index.js`) and that the OSS-only log line + 404 + empty
`enterprise_features` invariants hold.
Docs:
* `docs/planning/ENTERPRISE_ARCHITECTURE.md` — directory tree + why
one repo + dual-mount config + frontend seam description.
* `docs/dev/ENTERPRISE_LOCAL_DEV.md` — three-submodule table,
"working on the enterprise repo" steps for dual-mount sync.
* `docs/memory/requirements.md` §34.1 — frontend integration in
Key Features + private-repo layout.
Private repo content (Abilityai/trinity-enterprise) restructured in
a sibling commit (`refactor: split backend/ and frontend/ subdirs`):
* `backend/__init__.py` + `backend/sso/` — was at repo root.
* `frontend/index.js` — `registerEnterprise(router, app)`. Adds the
`/enterprise/sso` route pointing at `EnterpriseSSO.vue`.
* `frontend/views/EnterpriseSSO.vue` — Vue component. Fetches
`/api/enterprise/sso/providers` via the public repo's shared
`api.js`. Empty-state UI with the issue link.
Live verification (local instance):
* `/api/enterprise/sso/providers` → `[]` (backend still works after
import path change).
* Vite serves `/src/enterprise/frontend/index.js` (200) and
`/src/enterprise/frontend/views/EnterpriseSSO.vue` (200) with the
`api.js` import resolved to `/src/api.js`.
* `import.meta.glob` returns a populated object → enterprise module
loaded → `[enterprise] frontend module loaded` console log.
Tests: 12 passed + 2 skipped (no-passlib local). No new test surface
for the frontend store/component — playwright e2e would catch
regressions but is out of scope for this Phase 0.5.
Related to #847
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Reframe the open-core boundary: enterprise FRONTEND lives in the public OSS bundle and is gated server-side via the `enterprise_features` feature-flag. Only the private backend stays behind the submodule. Vue components have no algorithmic IP — the moat is the private backend logic (license verify, SAML signature checks, OAuth flows). This collapses the previous "dual-mount" complexity (same private repo at two mount points) back to a single submodule mount, and matches the existing feature-flag pattern (`session_tab_enabled`, `voice_available`). Trade-off accepted: enterprise Vue source is readable by anyone with the public repo, but the static UI files are not the load-bearing IP. The new closure mechanism is the EntitlementService **registry**: - Each enterprise backend module calls `entitlement_service.register_module(feature_id)` on boot. - `list_entitled_features()` returns the registered set (sorted). - OSS-only builds never call `register_module` → empty set → `enterprise_features: []` → OSS frontend hides every enterprise surface. - `TRINITY_OSS_ONLY=1` is a hard override (denies even when modules ARE registered). Closes three Phase-0 issues: 1. OSS users no longer see broken "Enterprise" nav — registry is empty without the submodule, feature-flag empty, NavBar hides. 2. Adding non-SSO features (SCIM, SIEM) is now additive: ship Vue file in OSS, add backend module, call `register_module(id)`. 3. Login-page SSO button extension point is straightforward — same pattern: OSS Login.vue reads the providers list from a backend API when entitled (out of scope for this PR, but the seam exists). Changes: Public repo: * `src/backend/services/entitlement_service.py` — replaces hardcoded `["sso","scim","siem"]` with a registry. `register_module(id)`, `is_entitled(id)`, `list_entitled_features()` all read from the set. Idempotent registration. TRINITY_OSS_ONLY=1 still hard- overrides everything. * `src/frontend/src/views/enterprise/SSO.vue` (new) — Vue PoC view, moved from the private repo's `frontend/views/EnterpriseSSO.vue`. Same UI; api.js import path adjusted to the OSS location (`../../api`). * `src/frontend/src/router/index.js` — static route entry for `/enterprise/sso` with `meta.requiresEntitlement: 'sso'`. `beforeEach` guard checks the entitlement store before navigation; redirects to `/` when not entitled (defence-in-depth against direct URL visits). * `src/frontend/src/main.js` — drops the `import.meta.glob('./enterprise/frontend/...')` block. Routes are static now. * `.gitmodules` — removes the `src/frontend/src/enterprise/` submodule entry. Only `src/backend/enterprise/` remains. * `src/frontend/src/enterprise` (submodule pointer) — deleted. * CI workflow `build-without-submodule.yml` — checks only the single backend mount; OSS-only frontend Vue files still bundle. * Docs (architecture, local-dev, requirements §34.1) — updated structure diagram, rationale, mount layout. * Tests (`test_847_entitlement_seam.py`) — registry contract: empty-by-default denies, `register_module` enables, idempotent. Updated allow-path test to `register_module("sso")` before asserting. Private repo (sibling commit, not in this PR): * `frontend/` subdir removed (moved to OSS). * `backend/__init__.py` now calls `entitlement_service.register_module("sso")` after mounting the SSO router. Live verification (local): * Default mode: `GET /api/settings/feature-flags` → `enterprise_features: ["sso"]` (only registered modules). `GET /api/enterprise/sso/providers` → `[]`. Enterprise nav link visible. * `TRINITY_OSS_ONLY=1`: `enterprise_features: []`, `GET /api/enterprise/sso/providers` → 403, nav link hidden. * Restored default — re-entitled. Tests: 13 passed + 2 skipped (no-passlib local). Related to #847 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PoC enhancement to make the SSO surface demo-realistic. Backend seeds two mock providers (Okta + Azure AD) and exposes three new read endpoints (claim-mapping, session-policy) the OSS admin page renders. All action buttons remain disabled with tooltips pointing at issue #847 — no real OIDC/SAML implementation yet. Public repo changes (this commit): * `src/frontend/src/views/enterprise/Index.vue` (new) — Enterprise catalogue landing page. Cards layout with status badges (SSO Available; SCIM/SIEM/License/Audit "Coming soon"). Linked from NavBar's `Enterprise` link. * `src/frontend/src/views/enterprise/SSO.vue` (rewrite) — full admin UI: - Header with "+ Add provider" button (opens modal) - Configured providers list with protocol badge, enabled/disabled indicator, issuer/metadata URL, last-login timestamp, and disabled Test/Edit/⋮ actions per row - Identity → Role Mapping table (4 rules: trinity-admins→admin, trinity-developers→creator, trinity-readonly→user, fallback) - Session Policy panel (force-SSO checkbox, session lifetime, admin-reauth checkbox — all disabled) - Add provider modal: protocol radio (OIDC/SAML), display name, provider ID, issuer URL, client ID/secret, scopes, callback URL with Copy button, enabled-on-save toggle, Cancel + Save buttons (Save disabled) * `src/frontend/src/views/Login.vue` — adds an "or sign in with" section under the email form when SSO providers are reachable. Fetches /api/enterprise/sso/providers unauthenticated (endpoint is gated by entitlement, not by user auth, so the pre-login page can call it). Two stub buttons render: "Continue with Okta" / "Continue with Azure AD". Both disabled with tooltip. * `src/frontend/src/router/index.js` — landing route + per-feature routes with two gate modes: - `meta.requiresAnyEntitlement` for the catalogue landing - `meta.requiresEntitlement: '<id>'` for per-feature pages Guard bounces non-entitled feature visits to /enterprise (when any feature is entitled) or / (otherwise). * `src/frontend/src/components/NavBar.vue` — link points at the catalogue landing (`/enterprise`). `v-if` uses `hasAnyEnterprise` so the Enterprise nav entry shows whenever any enterprise feature is registered. * `src/backend/enterprise` (submodule bump) — pulls in private repo's `feat(sso): seed PoC providers + claim-mapping + session-policy endpoints` (commit 3e90ddc). Live verification: - Default (submodule mounted): * `/api/enterprise/sso/providers` → 2 providers * `/api/enterprise/sso/claim-mapping` → 4 rules * `/api/enterprise/sso/session-policy` → SessionPolicy defaults * `/enterprise` landing renders 5 cards (SSO available, others "Coming soon") * `/enterprise/sso` renders full admin UI with all buttons disabled * `/login` shows "Continue with Okta/Azure AD" buttons under email form - `TRINITY_OSS_ONLY=1`: providers endpoint 403, NavBar enterprise link hidden, Login SSO buttons hidden. Related to #847 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI issues from the previous push:
1. `lint (sys.modules pollution check)` — 6 bare
`del sys.modules[...]` calls in `test_847_entitlement_seam.py`
exceeded baseline (`tests/lint_sys_modules.py` requires either
`monkeypatch.delitem(sys.modules, ..., raising=False)` or the
sanctioned `_STUBBED_MODULE_NAMES` pattern).
Replaced all six with `monkeypatch.delitem` so the auto-restore
on teardown also stops the test from leaking stale module
imports into sibling tests. `_import_requires_entitlement_or_skip`
helper now takes `monkeypatch` as a parameter so the pattern
works from inside the helper too.
2. `backend boots without enterprise submodule` — `/api/token`
returned 403 on a fresh DB because `is_setup_completed()` (in
`routers/auth.py:212`) gates admin login behind first-time
setup, which the previous workflow didn't complete. Added a new
"Complete first-time setup" step that:
- greps the setup token from `docker logs trinity-backend` (the
token is printed to stdout at boot per `main.py:336-343`,
gated on `setup_completed != true`)
- POSTs to `/api/setup/admin-password` with the
setup_token + password + confirm_password fields (schema in
`routers/setup.py:28-32`)
Replaced the hex `ADMIN_PASSWORD` with a known-strong value that
meets the OWASP ASVS 2.1 complexity check the setup endpoint
enforces (length 12+, upper, lower, digit, special).
Also dropped the Bearer auth from the "SSO router not mounted"
assertion — the `/api/enterprise/sso/*` router is
entitlement-gated, not user-gated, so the unauthenticated 404
check still proves the conditional import correctly skipped the
mount.
Tests: 13 passed + 2 skipped locally (no-passlib).
Lint: clean against baseline.
Related to #847
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The print() in main.py:336-343 that emits the setup token to stdout is Python-block-buffered when stdout is a pipe (docker logs capture), so the grep on a short timing window after /health returns OK is unreliable. The previous attempt extracted an empty SETUP_TOKEN and failed. Bypass the API entirely: docker exec into the backend container and write directly to the DB using the same helpers the production endpoint uses (dependencies.hash_password + db.set_setting + db.update_user_password). The flow: - set system_settings.setup_completed = 'true' - hash ADMIN_PASSWORD via the production helper - update admin user's password_hash The password is passed via docker -e to avoid shell quoting issues with the OWASP-compliant value (contains !). Verified locally: docker exec into trinity-backend, the python snippet runs cleanly and the bcrypt warning visible in CI logs is benign (passlib version mismatch, doesn't break hashing). Related to #847
The enterprise conditional-import block in main.py runs at module init, which is BEFORE `lifespan` calls `setup_logging()`. Default Python logging is at WARNING level, so `logger.info(...)` records are silently dropped — neither the registered nor the OSS-only log line appeared in docker logs, breaking the CI workflow that greps for them. Switch to `print(..., flush=True)` so the output goes to stdout regardless of logger state. docker logs captures it (operators see the boot mode), and the build-without-submodule CI workflow's grep succeeds. Verified locally: `docker logs trinity-backend | grep 'Trinity Enterprise'` now shows the registered/OSS-only line. Related to #847
…check The latest main.py commit (print(flush=True) + rationale comment) pushed `except ImportError` past the 400-char window the test was using to verify the conditional import is guarded. Widen to 1500 to absorb future small additions without breaking the static check, which is guarding the GUARD shape (try/except), not its byte position. Related to #847
Walk-through of how the open-core split works at runtime: - boot chain (conditional import → register_enterprise → registry) - request-time gating (feature-flags endpoint, requires_entitlement) - frontend wiring (Pinia store, NavBar, route guard, Login.vue, Vue views) - failure modes (OSS-only, TRINITY_OSS_ONLY=1) - adding a new enterprise feature recipe - test surfaces + CI All sections link to file:line in the public repo and the private trinity-enterprise repo on GitHub. Complements ENTERPRISE_ARCHITECTURE.md (the 'why') and ENTERPRISE_LOCAL_DEV.md (15-min onboarding) with the 'how' at the code level. Related to #847
Replaces the #847 Phase 0 SSO mock stub with the first concrete enterprise feature: an admin-facing audit log dashboard. Backend (public repo, stays OSS): - New GET /api/audit-log/distinct/event-types - New GET /api/audit-log/distinct/actor-types - Both admin-gated (Depends(require_admin)); populate dashboard filter dropdowns without hardcoding the AuditEventType enum on the frontend. - Registered BEFORE /{event_id} catch-all (invariant #4). Frontend (public OSS bundle, entitlement-gated route): - views/enterprise/Audit.vue — paginated table + filter form + side detail panel. - stores/auditLog.js — domain store (entries, filters, distinct lists, pagination, selectedEntry). Default time window = last 24h. - router/index.js — /enterprise/audit route with meta.requiresEntitlement = 'audit'. - views/enterprise/Index.vue — audit card flipped to Available; SSO card kept as Coming soon. - Login.vue — removed the SSO provider buttons mock. - Deleted views/enterprise/SSO.vue (350-line mock). Submodule (trinity-enterprise): - register_module("audit") replaces register_module("sso"). - Deleted backend/sso/{router,providers,__init__}.py. - See trinity-enterprise#feature/941-audit-registration. Entitlement model: backend stays OSS (audit_log endpoints predate the seam via SEC-001 / #20 — retroactive gating would break OSS admins). Only the OSS-side dashboard route is enterprise-gated. Tests: - New tests/unit/test_847_audit_dashboard.py (6 cases): distinct DB ops, router ordering invariant, admin gate, no-entitlement-gate pin. - Updated test_847_entitlement_seam.py: 'sso' → 'audit' assertions, new submodule static check. - New e2e/audit-dashboard.spec.js (Playwright, 4 cases). Docs: - audit-trail.md — Phase 5 row + Frontend Layer section. - enterprise-modules.md — current-state note + audit registration code. - architecture.md — distinct endpoints added to audit-log table. PR #910 scope expands to close both #847 (seam) and #941 (dashboard). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No critical or high findings. 3 low-severity stale-doc items, already deferred during /review (I3). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
audit-trail.md: document the new test_847_audit_dashboard.py cases + Playwright audit-dashboard.spec.js coverage. feature-flows.md index: bump audit-trail row to Phase 5 (dashboard), update Phases 1–4 row to "Merged" rather than "to follow". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dolho
force-pushed
the
feature/847-enterprise-sso-poc
branch
from
May 26, 2026 13:03
7af2583 to
ec348f5
Compare
… export (#941 v2) Expands the v1 dashboard from "viewer" to "review tool" using only existing backend endpoints — zero backend changes. Added: - Stats tiles header (Total / Top event_type / Top actor_type / Time window). Backed by GET /api/audit-log/stats; same time-window semantics as the table. Top-event/Top-actor tiles are clickable drill-downs. - Time preset chips (Last 1h / 24h / 7d / 30d / All time). Manual edits to the time fields flip the active chip to "Custom". - Inline cell drill-down: clicking an event_type cell sets that filter; clicking the actor cell filters by actor_id (or actor_type fallback). Stops row-click propagation so the side panel still opens elsewhere on the row. - Hash-chain verify badge in header. Manual button to verify the visible id range via POST /api/audit-log/verify; pill turns green ✓ on success or red ✗ with the first-invalid id on failure. - Export buttons (CSV / JSON) in the filter footer. Uses fetch + Blob + object URL so we can attach the JWT (a plain <a href> can't). Filename: audit-log-{iso-timestamp}.{ext}. Store additions: - stats / statsLoading + loadStats() - verifyState / verifyResult + verifyChain() - exporting + downloadExport(format) - activePreset + applyTimePreset(key) - drilldownFilter(key, value) helper Tests: - 2 new Playwright @smoke cases (preset chip click, event_type drill-down). Existing 4 cases unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 26, 2026
…ldable card (#941 v3) Adds two new admin-only audit-log endpoints and a unified foldable Activity card on the dashboard. Backend (OSS — same pattern as the existing /stats and /distinct/*): - GET /api/audit-log/heatmap — sparse 7×24 dow×hour grid - GET /api/audit-log/calendar — sparse per-day list (GitHub-style) Both honor start_time / end_time / event_type / actor_type so the two views stay coherent with the table + stats under drill-down. Frontend (OSS, entitlement-gated by 'audit'): - Single foldable Activity card with Weekly | Calendar tabs - v-show keeps both heatmaps mounted — tab swap is instant - Calendar cell click → drilldownToDay(date) narrows the dashboard to one UTC day and reloads list + stats + both heatmaps together Tests: +8 unit tests covering bucketing, filter pass-through, router ordering vs /{event_id} catch-all (invariant #4), empty-window contract. Full suite: 14 pass. Docs: architecture.md endpoint table + audit-trail.md Phase 5 v3 / v3.1 rows + endpoint descriptions + test catalog. Related to #941. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI fixes on PR #910: 1. Lint (sys.modules pollution): the audit-dashboard fixture had three bare sys.modules writes that exceeded the per-file baseline. Routed them through monkeypatch.setitem / monkeypatch.delitem so the baseline diff goes from +3 → 0. 2. Audit dashboard e2e suite was hard-coded to fail when the private enterprise submodule isn't checked out (the default on PR-time CI without a deploy-key secret). Added a per-test skip that probes for the Enterprise nav and skips the suite cleanly when absent. The route-guard / nav-hiding behavior in OSS-only mode is already covered by the unit tests in tests/unit/test_847_audit_dashboard.py. The api-keys-copy.spec.js failures in the same e2e run are a pre-existing modal-overlay flake unrelated to #941 — touched outside this PR. Related to #941. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dolho
marked this pull request as ready for review
May 27, 2026 08:48
The Deploy-to-Dev workflow currently bypasses the enterprise submodule entirely: it doesn't init it, and the Dockerfile doesn't COPY it. The dev VM has been silently OSS-only since the seam landed. This adds an enterprise-overlay pattern that keeps the public prod image bit-identical to OSS while making enterprise features available on dev: - docker-compose.prod.enterprise.yml — single-service overlay that bind-mounts ./src/backend/enterprise into /app/enterprise (ro). The conditional `from enterprise.backend import register_enterprise` in main.py resolves the bind-mounted path on container start. - .github/workflows/deploy-dev.yml — init the submodule after pull (soft-fail with a clear marker so a missing deploy key doesn't brick the deploy), then layer the overlay onto build + up. Compose merges the volumes additively; no override of existing prod.yml mounts. - docs/dev/ENTERPRISE_LOCAL_DEV.md — new "Dev VM deploy" section with the one-time deploy-key setup (deploy key on trinity-enterprise + ~/.ssh/config alias on the VM) and the post-deploy verification step. Verified locally: `docker compose -f docker-compose.prod.yml -f docker-compose.prod.enterprise.yml config` merges cleanly; the import path resolves in the existing local container (which already bind-mounts the wider src/backend tree); /api/settings/feature-flags returns `enterprise_features: ["audit"]`. OSS path unchanged. The base Dockerfile and docker-compose.prod.yml are untouched, so build-without-submodule.yml CI guards stay green. Related to #847. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dolho
force-pushed
the
feature/847-enterprise-sso-poc
branch
from
May 27, 2026 09:16
37f839a to
1ec5d88
Compare
…#677) The two @smoke tests in `api-keys-copy.spec.js` were timing out at the final cleanup step on slow GH-Actions runners. The pattern: Create modal → fill → Create → success modal → Copy → readClipboard → close → cleanup (revoke modal → confirm → delete modal → confirm) is ~9 sequential UI ops. On a constrained runner each takes 1-3s, so the per-test 30s budget would drain right around cleanup, and `page.waitForTimeout(200)` would resolve into "Target page closed". Replace the UI cleanup walk with `page.request.delete('/api/mcp/keys/{id}')` using the JWT from `localStorage['token']` (`stores/auth.js:201`). The backend DELETE handler hard-removes the row regardless of active / revoked state, so the UI-enforced revoke-then-delete sequence isn't needed for cleanup. The clipboard assertions earlier in the test still cover the actual #677 / #859 regression — UI cleanup was never under test. Reduces cleanup time from ~6s of UI clicks to one HTTP DELETE. Related to #677, #859. Unblocks PR #910 CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vybe
approved these changes
May 27, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
Validated via /validate-pr — all 16 CI checks green, security clean, docs (architecture + feature-flows + index) updated, dedicated CSO --diff report attached. Deferred items explicitly tracked in body's Out-of-scope section.
2 tasks
3 tasks
pull Bot
pushed a commit
to bryanwills/trinity
that referenced
this pull request
Jul 9, 2026
…bilityai#1447) test_submodule_registers_audit_not_sso pinned the enterprise submodule's __init__.py to "no .sso import" — a stale assertion from the Abilityai#910/Abilityai#941 era when only the removed SSO PoC stub existed. Real SSO-OIDC (Abilityai#32) has since landed via Abilityai#1303, so the pin failed on every entitled clone with the submodule mounted (CI is unaffected — it skips when the submodule is absent). Update the pin to the current contract: - keep the direct register_module("audit") assertion (UI-gating module) - replace the two .sso-absence assertions with presence checks on `from .sso import register as register_sso` + `register_sso(app)` (SSO self-registers its feature_id inside .sso.register, so register_module("sso") is intentionally not called in __init__.py) - rename ..._not_sso -> ..._and_sso and refresh the docstring Verified against the mounted submodule: passes with it, skips cleanly without. Fixes Abilityai#1447 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Related to #847 (enterprise edition architecture spike) and #941 (audit log dashboard).
This PR ships two things:
Abilityai/trinity-enterprisemounted atsrc/backend/enterprise/. Loads conditionally so OSS-only builds (no submodule access) work unchanged./enterprise/audit. Frontend ships in the OSS bundle, gated by theenterprise_featuresflag exposed via/api/settings/feature-flags. Backend audit log endpoints stay OSS (they predate the seam via SEC-001/Audit Trail System (SEC-001) #20).The original Phase 0 PoC mounted a
/api/enterprise/sso/*mock router with seeded providers; that stub is removed in favour of a real, useful first enterprise demonstration. SSO returns in a later PR with a real implementation, not a stub.What lands
Public-repo seam (#847)
services/entitlement_service.py—EntitlementServicewithregister_module(feature_id)registry.TRINITY_OSS_ONLY=1env flips every check to False.dependencies.py:requires_entitlement(feature_id)— FastAPI dependency raising HTTP 403 with the missing feature name.main.py— conditionaltry: from enterprise.backend import register_enterprise except ImportError./api/settings/feature-flagsextended withenterprise_features: list[str]..gitmodulesentry,docker-compose.ymlenv pass-through forTRINITY_OSS_ONLY.build-without-submodule.ymlasserts OSS-only boot.Frontend enterprise scaffolding (#847)
views/enterprise/Index.vue— landing page with feature catalogue cards.stores/enterprise.js— Pinia store cachingenterprise_featuresfrom the feature-flags API.NavBar.vue— Enterprise nav link, hidden when no features entitled.router/index.js— entitlement-gated routes (requiresEntitlement/requiresAnyEntitlementmetas +beforeEachguard).Audit log dashboard (#941)
views/enterprise/Audit.vue— paginated table + filter form + side detail panel + hash-chain disclosure.stores/auditLog.js— domain store (entries, filters defaulting to last 24h, distinct-value lists, pagination, selected entry).routers/audit_log.py— two new admin-only endpoints for filter dropdowns:GET /api/audit-log/distinct/event-types→ sortedlist[str]GET /api/audit-log/distinct/actor-types→ sortedlist[str]db/audit.py—get_distinct_event_types/get_distinct_actor_types(parameterless DISTINCT queries against indexed columns).register_module("sso") → register_module("audit")(Abilityai/trinity-enterprise#feature/941-audit-registration, commit1e916f6).v2 — stats, presets, drill-down, verify, export (commit
745a5a9b)event_type,actor_id, oractor_typecell narrows the filter and reloads.POST /api/audit-log/verify.GET /api/audit-log/export.v3 — heatmaps in a foldable card (commit
ccb65df7)GET /api/audit-log/heatmap— sparse 7×24 day-of-week × hour-of-day grid (SQLitestrftime('%w' / '%H')). Honorsstart_time/end_time/event_type/actor_typefilters.GET /api/audit-log/calendar— sparse per-day list (GitHub-style calendar). Same filter passthrough.v-showkeeps both heatmaps mounted — tab swap is instant).drilldownToDay(date)narrows the dashboard to one UTC day and reloads list + stats + both heatmaps together./{event_id}catch-all (invariant fix: add missing logging_config.py to backend Dockerfile #4); admin-gated.SSO mock surface — removed
views/enterprise/SSO.vue— deleted (350-line mock).Login.vue— removed SSO provider buttons block and the unauthenticated provider fetch.router/index.js—/enterprise/ssoroute removed.backend/sso/router.py,providers.py,__init__.py— deleted.Tests
tests/unit/test_847_entitlement_seam.py— entitlement-service mechanics +requires_entitlementdependency + main.py conditional-import static check + new static check that the submodule registersauditnotsso. 22/22 pass.tests/unit/test_847_audit_dashboard.py— distinct DB ops, router-ordering invariant, admin gate, no-entitlement-gate pin, plus v3 heatmap + calendar bucketing / filter pass-through / empty-window contract / ordering. 14/14 pass.src/frontend/e2e/audit-dashboard.spec.js— Playwright@smokesuite covering nav → click → filter → row → side panel.Docs
docs/planning/ENTERPRISE_ARCHITECTURE.md— open-core decision record.docs/planning/OSS_ENTERPRISE_SPLIT_RESEARCH.md— long-form research.docs/dev/ENTERPRISE_LOCAL_DEV.md— 15-min clone-to-running guide.docs/memory/feature-flows/audit-trail.md— Phase 5 / 5 v2 / 5 v3 / 5 v3.1 rows + Frontend Layer section + heatmap & calendar endpoint sections + tests catalogue.docs/memory/feature-flows/enterprise-modules.md— boot chain +register_module("audit")snippet + current-state note.docs/memory/feature-flows.mdindex — Phase 5 entry.docs/memory/architecture.md— distinct + heatmap + calendar rows in the audit-log endpoint table.docs/security-reports/cso-diff-2026-05-26-941.md—/cso --diffreport (no critical/high findings).Entitlement model
Frontend route only. Backend audit log endpoints stay OSS (they predate the seam via SEC-001/#20 — retroactive gating would break OSS admins relying on curl access). Only the OSS-side dashboard route is enterprise-flagged; registering
register_module("audit")from the private submodule flips the route from hidden to visible.This is a deliberate revision of the original premise during
/autoplan: gating already-shipped OSS endpoints retroactively is a breaking change with no compensating security benefit (admins already needed admin auth).UI gating works in three layers (verified by
router/index.jsguard,NavBar.vuev-if, andviews/enterprise/Index.vuecard state): NavBar link hidden, catalogue card shown as "soon", direct URL bounces to/.Test plan
pytest tests/unit/test_847_audit_dashboard.py tests/unit/test_847_entitlement_seam.py -v→ 36/36 (22 seam + 14 dashboard)cd src/frontend && npm run test:e2e -- audit-dashboard.spec(requires live stack)/enterpriseshows audit card "Available" → click → table renders → filter by event_type → click row → side panel shows JSON details + hash chain disclosureTRINITY_OSS_ONLY=1, verify/api/settings/feature-flagsreturnsenterprise_features: []and frontend nav hides the Enterprise linkbuild-without-submodule.ymlpasses1e916f6reachable onAbilityai/trinity-enterprise(pushed)Out of scope (follow-up issues to file)
feature-flows/enterprise-modules.mdhistorical SSO sections — deferred during/review(I3)requirements.mdentry for the audit dashboard requirement — deferred during/review(I2)Related to #847
Related to #941
🤖 Generated with Claude Code