Trinity is open-core. The open-source platform in this repository is complete
and fully functional on its own. Customers with an enterprise agreement
additionally receive access to a private companion repository
(Abilityai/trinity-enterprise) that mounts into this repo as an optional
git submodule at src/backend/enterprise/.
This document covers the generic installation mechanism only: how the optional submodule mounts, how the backend detects it, and how to verify which edition an instance is running.
Why there's no feature catalog here. By a standing open-core rule
(trinity-enterprise#45, enforced by
.github/workflows/enterprise-docs-guard.yml), this public repo documents the
seam — never the catalog of which capabilities are paid, their private
enterprise_* schema, or the monetization rationale. Entitled customers and
core-team members find the per-module catalog and designs in the private
enterprise repository (docs/memory/ENTERPRISE_DOCS.md there). For a live,
edition-specific answer to "what's actually enabled on this instance," ask the
running backend: GET /api/version and GET /api/settings/feature-flags both
report the enterprise feature-ids your build has registered (empty in an
OSS-only build).
src/backend/main.pywraps the enterprise loader in a conditional import: if the submodule is present,register_enterprise(app)runs and each enterprise module registers itself with the entitlement registry (services/entitlement_service.py); if the submodule is absent, theImportErroris caught and the platform runs OSS-only — this is normal, not an error.- Registration state drives two API surfaces (same source, never divergent):
GET /api/settings/feature-flags→enterprise_features: [...](empty list in OSS-only builds)GET /api/version→edition: "oss" | "enterprise"plus the sameenterprise_featureslist (#1443)
- A bug during enterprise registration never takes down the platform: the
backend logs
Trinity Enterprise registration FAILED — continuing OSS-onlyand boots with whatever registered before the failure (possibly nothing).
The submodule entry in .gitmodules sets update = none, so:
git clone --recurse-submodules https://github.com/abilityai/trinity.git # OK
# or
git clone https://github.com/abilityai/trinity.git
cd trinity
git submodule update --init --recursive # OK — prints "Skipping submodule"both complete without credentials. The src/backend/enterprise/ directory
stays empty and the backend boots OSS-only, logging:
Trinity Enterprise submodule not present — OSS-only build (this is normal; enterprise modules are an optional private submodule)
update = nonemeans git skips this submodule on every plaingit submodule update, including--init— and any init path (plain--init, one-shot--init --checkout,clone --recurse-submodules) copiesnoneinto your local config, so later updates keep skipping. Set the local override first; it is durable and wins over.gitmodules.
git config submodule.src/backend/enterprise.update checkoutOption A — SSH (default URL in .gitmodules): ensure your SSH key has
read access to the private repository, then initialize:
git submodule update --init src/backend/enterpriseOption B — HTTPS with a personal access token (CI hosts, servers without SSH keys): override the submodule URL locally, then initialize:
git config submodule.src/backend/enterprise.url \
"https://x-access-token:<YOUR_GITHUB_PAT>@github.com/Abilityai/trinity-enterprise.git"
git submodule update --init src/backend/enterpriseThe URL override lives only in your clone's .git/config — never commit a
token anywhere. Prefer a credential helper over embedding the token in the URL
where possible.
Option A fails one layer earlier than you expect (#2246). SSH verifies the server's identity before it offers your key, so a host with no
github.comentry in its~/.ssh/known_hostsdies at:Host key verification failed. fatal: Could not read from remote repository.That message is not about access — authentication was never attempted — but git helpfully appends "Please make sure you have the correct access rights", which sends most people after credentials they already have. It is also asymmetric:
.gitmodulesuses HTTPS for.claudeand SSH only forsrc/backend/enterprise, so this is the one submodule that can fail this way. The dev VM shipped OSS-only for weeks on exactly this. Either trust the key:ssh-keyscan github.com >> ~/.ssh/known_hosts…or take Option B, which needs no
known_hostsentry and clears the authorization layer in the same step. On an unattended host, prefer B.
A deploy host is the case where "it silently degraded" costs the most, because nobody is watching the log. Two things make it durable:
Pin the transport in the host's clone, once. Option B's git config above
survives pulls, rebuilds of the containers, and every future
git submodule update — but not a rebuild of the VM itself. Re-applying it
belongs in whatever provisions the host, next to the checkout step; otherwise the
next rebuild silently reintroduces the OSS-only deploy.
Or hand the token to the workflow instead of the host. deploy-dev.yml reads
an optional ENT_SUBMODULE_PAT repo secret and, when present, rewrites the SSH URL
to HTTPS for the duration of that one command:
git -c "url.https://x-access-token:${PAT}@github.com/.insteadOf=git@github.com:" \
submodule update --init --recursive src/backend/enterprisegit -c exports GIT_CONFIG_PARAMETERS, which the submodule's own clone process
inherits (verified — with the rewrite in place the child clones the rewritten
URL), so the token never lands in the host's .git/config. With the secret
unset the command is byte-identical to the plain form, so this is additive: the
host-side override above remains a valid, independent fix.
Whichever route you take, the deploy now fails rather than warning when the
submodule is not mounted — an unentitled dev instance means the recorded gitlink
is never exercised before prod. Set the repo variable DEPLOY_ALLOW_OSS_ONLY=true
if a particular host legitimately has no enterprise access.
The superproject fetch never recurses (#2578). The workflow's own git fetch,
git checkout and git pull carry --no-recurse-submodules. Without it, git's
default on-demand recursion fires on every commit that moves the enterprise
pointer, dials the submodule's stored SSH URL before the PAT transport above
exists, dies at host-key verification, and — under set -e — stops the deploy
at the pull. It hid for weeks because the superproject refs advance before the
recursion runs, so the next unrelated push deploys the bump one push late. The
submodule block is the only sync path, by design. Pulling on the host by hand
follows the same shape: git pull --ff-only --no-recurse-submodules origin dev,
then sync the submodule with a transport that works on that host — the
git -c "url.https://x-access-token:${PAT}@github.com/.insteadOf=git@github.com:" submodule update --init src/backend/enterprise form above, or Option B pinned in
the clone — because the host's submodule origin is the SSH URL, and a plain
git submodule update dies at the same host-key check.
A sync that fails on a moved pointer leaves the old enterprise tree checked
out. The deploy reports that as Enterprise submodule STALE, builds the version
with a .dirty suffix, and fails after the health check (#2578): the platform
code is the new commit, the enterprise code is the old pin, and every later deploy
fails the same way until the transport is restored. There is no escape hatch for a
stale tree — a host meant to run OSS-only unmounts it
(git submodule deinit -f src/backend/enterprise) and sets
DEPLOY_ALLOW_OSS_ONLY=true.
The enterprise tree reaches the backend via a bind-mount, never the image —
the backend Dockerfile copies an explicit allowlist that excludes
enterprise/, keeping the published image bit-identical to the OSS build.
What to do depends on your stack:
Dev stack (docker-compose.yml) — it already bind-mounts ./src/backend
into /app, so the freshly-initialized submodule is inside the mount. Just
restart the backend:
docker compose restart backendProd stack (docker-compose.prod.yml) — there is no source mount; add the
enterprise overlay (which bind-mounts ./src/backend/enterprise read-only
into /app/enterprise) to every compose invocation:
docker compose -f docker-compose.prod.yml -f docker-compose.prod.enterprise.yml up -d backendSee the comments in docker-compose.prod.enterprise.yml for the full
build/update recipe.
# Boot log — one line per outcome:
docker logs trinity-backend 2>&1 | grep "Trinity Enterprise"
# "Trinity Enterprise modules registered" → enterprise active
# "Trinity Enterprise submodule not present …" → OSS-only
# "Trinity Enterprise registration FAILED …" → degraded to OSS-only (see traceback above it)
# API (any authenticated user):
curl -s -H "Authorization: Bearer <token>" http://localhost:8000/api/version
# → { "edition": "enterprise", "enterprise_features": ["..."], ... }With the step-1 override in place, routine updates are two commands — and the pull never recurses (#2578):
git pull --no-recurse-submodules
git submodule update --init --recursiveWithout the flag, git's default on-demand recursion fetches the submodule inside the pull whenever the pointer moved, using the submodule's stored URL and whatever transport that host happens to have — the exact failure the dev deploy hit. Keeping the two steps separate means the second one runs with the transport you chose in step 2.
Existing clones (mounted before #1443): the update = none default
applies to your clone too — plain git submodule update starts skipping
the enterprise submodule (exit 0, working tree left stale). Run the step-1
git config line once to restore normal syncing.
Advancing the pointer is the one change public CI cannot check: it never mounts the submodule, so nothing in this repo's build sees what the new pointer contains. Run the migration-graph guard locally first — you already have the submodule checked out, which is why this is a procedure rather than a CI job:
python3 scripts/ci/check_alembic_heads.py \
src/backend/enterprise/backend/migrations/versions
# → "… 1 head (…) — PASS." ship it
# → "FAIL — … resolves to 2 heads" do NOT advance the pointerTwo migration revisions that branch off the same parent leave the version
line with two heads. The upgrade run is head, singular: it resolves its
target before applying anything, so it applies nothing and raises —
and that error is caught and downgraded to a warning, so the platform boots
happily with every entitled feature switched off. Git shows no conflict for
this, because each revision file is valid on its own.
After the deploy, confirm the outcome rather than assuming it — the boot log
in 4. Verify must show Trinity Enterprise modules registered
and no registration FAILED line, and GET /api/version must report
"edition": "enterprise". A green /health and green containers prove
nothing here: a failed registration leaves both untouched.
TRINITY_OSS_ONLY=1 (backend environment) empties the entitlement registry
even when the submodule is mounted — every enterprise feature reports
not-entitled, enterprise_features returns [], and edition reports
"oss". Useful for compliance lockdowns and for CI that exercises the
OSS-only path (.github/workflows/build-without-submodule.yml).
The dev-tooling submodule at .claude/ (skills, methodology guides) is also
private and also update = none, so OSS clones skip it too. Core-team setup
is documented in CLAUDE.md → "Development Skills". External contributors
don't need it — the public
abilities marketplace ships the
dev-methodology plugin with the equivalent workflow skills.