diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bb7ec4a..c4bb3da4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,12 @@ jobs: exit 1 fi + printf '%s\n' '.github/workflows/deploy-production.yml' > "$fixture" + if node scripts/ci/community-boundary.mjs --paths-file "$fixture"; then + echo "Community boundary accepted an official deployment workflow." >&2 + exit 1 + fi + mkdir -p packages/boundary-fixture printf '%s\n' "import '@humanly-ee/not-allowed';" > packages/boundary-fixture/index.ts if pnpm boundary:audit; then diff --git a/README.md b/README.md index b224c22e..89753d17 100644 --- a/README.md +++ b/README.md @@ -53,20 +53,22 @@ Humanly has two first-party web apps and a public certificate surface: Humanly Community contains the complete Writer, Publisher, and Verifier workflow plus the tools needed to run it on your own infrastructure. The -managed service at `writehumanly.net` is operated through a separate private -Humanly Cloud control plane. Production credentials and managed-service -deployment automation are intentionally not stored in this public repository. +managed service at `writehumanly.net` is developed and operated from the +separate private `Humanly-Lab/humanly-cloud` repository. Production +credentials, official-environment topology, and managed-service deployment +automation are intentionally not stored in this public repository. ## Editions -Humanly ships Community and Cloud from one public codebase. The core product -under `packages/` is shared by both editions; Cloud-only implementations live -under `ee/` so the two editions do not drift into separate application forks. +Humanly Community and Humanly Cloud are maintained as separate products. This +public repository is the Community source and self-hosting surface. The private +`Humanly-Lab/humanly-cloud` repository owns the managed SaaS source, commercial +features, and every workflow that deploys an official Humanly environment. -| Edition | Distribution | Source license | +| Edition | Repository | Distribution | | --- | --- | --- | -| Humanly Community | Self-hosted with the quickstart or manual deployment guide | MIT for all content outside `ee/` | -| Humanly Cloud | Managed SaaS at [writehumanly.net](https://writehumanly.net/) | MIT core plus source-available `ee/` code under PolyForm Free Trial 1.0.0 | +| Humanly Community | Public `Humanly-Lab/humanly` | Self-hosted with the quickstart or manual deployment guide | +| Humanly Cloud | Private `Humanly-Lab/humanly-cloud` | Managed SaaS and institution-ready commercial deployments | The PolyForm license for `ee/` is **not an open-source license**. It permits a time-limited evaluation under its stated terms; use outside those terms @@ -74,10 +76,10 @@ requires a commercial license. See the [Humanly Enterprise Server README](ee/README.md) for the exact boundary and current contents. -The private `Humanly-Lab/humanly-cloud-infra` repository is a deployment control -plane only. It contains managed infrastructure, production configuration, and -private artifacts such as model weights; it does not contain a second copy of -the Humanly application source. +Community CI, security scanning, package publishing, generic Docker assets, and +self-hosting tools remain here. These are not managed Cloud deployment +automation. Community CI rejects official-environment deployment workflows and +managed-service topology if they are added to this repository. ## Features diff --git a/docs/EDITIONS_DEVELOPMENT.md b/docs/EDITIONS_DEVELOPMENT.md deleted file mode 100644 index f32a6e57..00000000 --- a/docs/EDITIONS_DEVELOPMENT.md +++ /dev/null @@ -1,180 +0,0 @@ -# Editions Development Guide (internal) - -How to work with the Community/Cloud edition split day to day. Written for Humanly -team members and coding agents. This repo is public, so nothing secret belongs here; -operational runbooks and production config live in the private infra repo. - -## Architecture at a glance - -Repository topology and the Community/EE separation: - -```mermaid -flowchart LR - subgraph humanly["Humanly-Lab/humanly · public"] - core["packages/* — MIT core - backend · frontend ×2 · shared · editor - tracker · inference · create-humanly - (complete multi-user product: - auth / email / certificates / detector framework)"] - ee["ee/ — PolyForm Free Trial (paid, source-visible) - packages/billing (@humanly-ee/*) - migrations/9000+ · docker/"] - ee -- "depends on @humanly/* (one-way; - reverse imports blocked by CI)" --> core - end - infra["humanly-cloud-infra · private - community.lock (pinned SHA) - deploy compose / nginx · deploy workflow - secrets & private artifacts — zero app code"] - infra -. "checks out the pinned SHA to build" .-> humanly -``` - -One source tree, two builds — the concrete seams: - -```mermaid -flowchart TB - src["single source tree: humanly:main"] - src --> c["EDITION=community (default)"] - src --> k["EDITION=cloud"] - c --> cb["docker/*.Dockerfile - no COPY ee/ · optionalDependencies tolerate absence - webpack alias → community stub (notFound)"] - k --> kb["ee/docker/*.Dockerfile - builds @humanly-ee dist first - webpack alias → real EE UI"] - cb --> ci2["Community image - /api/v1/billing → explicit 404 - purity asserts: no ee/ content, no EE UI marker"] - kb --> ki["Cloud image (:SHA-cloud) - dynamic import mounts billing routes - migrations run both dirs (core : ee)"] -``` - -End-to-end delivery workflow: - -```mermaid -flowchart TB - pr["developer PR → humanly:main"] --> ci["CI matrix (every PR) - community + cloud build/test - boundary audit · image-purity asserts"] - ci --> main["merge to main"] - main --> sh["Community path: self-hosters - quickstart (EDITION=community)"] - main --> lock["Cloud path: infra repo - advances community.lock"] - lock --> build["GitHub runner - checks out pinned SHA · builds via ee/docker - image tags SHA-cloud"] - build --> bundle["runtime bundle - compose + nginx + core/ee migrations"] - bundle --> vm["production VM deploy - (the VM never touches git)"] - vm --> verify["verify health edition=cloud - billing/plan 200 · record lock"] - sh --> shv["health edition=community"] -``` - -## Mental model: three boundaries, two repos - -| Boundary | Question it answers | Where it lives | -|---|---|---| -| Code evolution | Does product code ever fork? | Never — all product code in this repo | -| Licensing | Who may use which code for free? | `packages/*` = MIT; `ee/` = PolyForm Free Trial 1.0.0 | -| Operations/secrets | What does our production look like? | Private infra repo (`humanly-cloud-infra`) — deployment only, zero app code | - -**The edition boundary is a commercial-value boundary, not a deployment boundary.** -If something differs between self-host and our SaaS only by *configuration* (SMTP -provider, OAuth client IDs, hostnames, storage bucket), it is core code + env config — -not an `ee/` feature. - -## The switch - -- Backend: `EDITION=community|cloud` (default community), read once in - `packages/backend/src/config/env.ts`. -- Frontends: `NEXT_PUBLIC_EDITION`, normalized in each app's `src/lib/edition.ts`. -- Registry: `packages/shared/src/config/edition.ts` — `Edition`, `EditionFeature`, - `hasFeature(edition, feature)`. **All gating goes through `hasFeature`**; never - scatter raw `process.env.EDITION` checks (the only exceptions are literal - `process.env.NEXT_PUBLIC_EDITION === 'cloud'` comparisons at frontend branch sites, - which Next.js needs for dead-code elimination). - -## Where things live - -``` -packages/* MIT core. The complete, working multi-user product. -ee/packages/* Paid features (@humanly-ee/*). May import @humanly/*. -ee/migrations/ Cloud-only SQL (numbered 9000+). -ee/docker/ Cloud image compositions (EDITION=cloud). -``` - -Hard rule (CI-enforced by `scripts/ci/community-boundary.mjs`): -**`packages/*` never imports from `ee/`.** EE plugs into core through seams core -defines (dynamic-import registration, webpack alias), never the reverse. - -## Recipe: add a new Cloud feature - -Worked example — the billing skeleton (use it as the template): - -1. **Flag** — add the name to `EditionFeature` and cloud's list in - `packages/shared/src/config/edition.ts`. -2. **Package** — `ee/packages//` with scope `@humanly-ee/`, - `"license": "SEE LICENSE IN ../../LICENSE"`. -3. **Backend** — register behind the flag in `packages/backend/src/app.ts` using the - existing dynamic-import pattern (`loadBillingModule` style: import via a variable - so community builds never resolve the package). Return an explicit 404 for the - route prefix in community. -4. **Frontend** — export UI from the ee package; resolve it through a build-time - webpack alias in `next.config.js` that points at the ee source in cloud builds and - a community stub otherwise (see `@humanly-edition/billing-ui`); add a runtime - `notFound()` guard in the page. -5. **DB** — migration in `ee/migrations/` (9000+ prefix). Cloud deploys pass both - dirs via colon-separated `MIGRATIONS_DIR`. - -One PR. The CI matrix (community + cloud) plus the image-purity check -(`scripts/ci/assert-edition-image.sh`) prove community is untouched. - -**If the feature must *change* core behavior** (not just add routes/pages): add an -extension point in core (provider interface + community default implementation) and -let the ee package register its implementation. Do not sprinkle `if (cloud)` through -core logic — edition awareness belongs only at registration sites. - -## Frequently confused placements - -| Thing | Placement | Why | -|---|---|---| -| Password reset, email sending | Core | Config-driven (`EMAIL_SERVICE=console\|sendgrid\|smtp`); self-hosters bring SMTP. Our SendGrid key is infra-repo config. | -| Google/GitHub login | Core | Credential-config-driven; `/auth/oauth/providers` hides unconfigured buttons. Self-hosters may bring their own client IDs. | -| Enterprise SSO (SAML/OIDC/SCIM) | Future `ee/` | The canonical open-core paid line (GitLab: OmniAuth free, SAML paid). | -| File storage (local/GCS) | Core | Adapter chosen by config; both editions may use either. | -| Plan/quota enforcement, team workspaces, cross-tenant analytics | `ee/` | Value exists only in a paid context. | -| Managed typing-detector model/weights | Infra repo / private bucket | Secret artifacts; `ee/` carries only the managed-client glue. | -| Detector framework, `packages/inference` | Core | Described by the paper; the OSS release must keep it (bring-your-own endpoint). | - -Reference for the pattern, not the split: OpenHands puts auth/email/billing in -`enterprise/` because their OSS build is a **single-user local tool with no accounts**. -Humanly Community is a multi-user server — auth and email are core product here. - -## Running each edition locally - -```bash -# Community (default — everything as before) -npm run dev:backend / dev:frontend / dev:frontend-user - -# Cloud -EDITION=cloud npm run dev:backend -NEXT_PUBLIC_EDITION=cloud npm run dev:frontend-user -MIGRATIONS_DIR="packages/backend/src/db/migrations:ee/migrations" scripts/run-migrations.sh -``` - -Health endpoint reports the running edition (`edition: "community" | "cloud"`). - -## Invariants CI will hold you to - -1. Typecheck + runnable tests pass for **both** editions (matrix). -2. No `packages/* → ee/` imports; no managed-production paths/hostnames in Community - sources (`community-boundary.mjs`). -3. Community images contain no `ee/` content (`assert-edition-image.sh`). -4. Community must boot and pass tests with `ee/` absent. - -When you change a seam (registry, alias, migration runner, CI scripts), update this -guide and `docs/EDITIONS_REFACTOR_PLAN.md` in the same PR. diff --git a/docs/EDITIONS_NEXT_STEPS.md b/docs/EDITIONS_NEXT_STEPS.md deleted file mode 100644 index b6fd2b29..00000000 --- a/docs/EDITIONS_NEXT_STEPS.md +++ /dev/null @@ -1,134 +0,0 @@ -# Editions: Post-Refactor Work Order - -**Status: COMPLETE — independently verified in production 2026-07-14.** -Infra PR #11 merged; `community.lock` advanced past the PolyForm swap (now -auto-advancing, at `c414771a`); production health reports `edition: "cloud"` and -`GET /api/v1/billing/plan` returns 200; `AI_ENCRYPTION_KEY` boot guard + tracker -hostname fix landed (#1054); repo renamed `humanly-cloud-infra` (#1055/#12); -Dockerfile cross-references (#1058); nullglob guard in `build-runtime-bundle.sh`. -Known follow-up nit: the AI-key guard rejects empty/all-zero keys but does not -enforce the 64-hex format its error message promises. Kept for history. -**Audience:** implementation agent (Codex). Self-contained; do not assume chat context. - -## Deep-review verdict (2026-07-14) - -The edition seam is sound and matches industry practice. Verified: - -- `packages/backend/src/app.ts`: `hasFeature` gate, injectable `loadBillingModule` - (testable), dynamic import via variable (community runtime never resolves - `@humanly-ee/billing`), explicit 404 for `/api/v1/billing` in community. -- Frontend: build-time webpack alias `@humanly-edition/billing-ui` swaps between - `ee/packages/billing/src/writer.tsx` (cloud) and a community stub — EE UI is - physically absent from community bundles; runtime `notFound()` double-guards. -- `scripts/run-migrations.sh`: colon-separated multi-dir with duplicate-filename guard. -- CI: edition matrix, `community-boundary.mjs` (forbidden production paths), - `assert-edition-image.sh` (image purity per edition). -- `ee/LICENSE` = PolyForm Free Trial 1.0.0 (real, unmodified license — good choice). -- No Stripe/paid references outside `ee/`; no build artifacts tracked in git. - -**Findings to address (ordered; updated after the line-level second pass):** - -1. **Resolved: production now runs the Cloud edition.** Infra PR #11 was rebased - with `community.lock` advanced past `d500df8f`, the Cloud images passed edition - purity checks, and production health reports `edition: "cloud"` with the - billing route mounted. -2. **Resolved: `AI_ENCRYPTION_KEY` production guard.** It previously had an - all-zeros default with no production guard - (`packages/backend/src/config/env.ts`). Email config has a production-boot - validation pattern (`getEmailConfigurationErrors`); the encryption key — which - protects user-owned AI provider keys at rest — now follows the same pattern and - refuses production boot when unset or all-zero (#1054). -3. **Resolved: hardcoded managed hostname in Community-facing copy.** - `packages/backend/src/controllers/tracker.controller.ts` (~line 425) tells - admins where to open generated tracking-code instructions. It now derives the - URL from `env.frontendAdminUrl`; the intentionally workflow-only hostname audit - was not widened (#1054). -4. **`ee/docker/*.Dockerfile` duplication drift.** The three cloud Dockerfiles are - near-copies of `docker/*.Dockerfile`. Add a comment header in both pointing at the - counterpart, and a CI reminder (e.g., checksum-diff warning) or accept the drift - risk consciously. Also: `build-runtime-bundle.sh` in the infra repo does - `cp ee/migrations/*.sql` — fails if that directory is ever empty; guard the glob. - -**Retracted:** an earlier draft flagged the publisher billing seam as unwired. It is -fully wired (`packages/frontend/next.config.js` aliases -`@humanly-edition/publisher-ui`, imported in the admin root layout); `publisher.tsx` -is intentionally a hidden marker component that the image-purity check greps for -(`HUMANLY_CLOUD_UI_MARKER`). No action needed. - -## Boundary decisions (settled — do not relitigate) - -**Auth, password reset, and email stay in Community.** Verified current design is -already correct and must be preserved: - -- `email.service.ts` is provider-config-driven (`EMAIL_SERVICE=console|sendgrid|smtp` - with production guards). Self-hosters bring their own SMTP; the SendGrid account is - Cloud *configuration* (lives in the infra repo), not Cloud *code*. -- OAuth (`oauth.service.ts`, Google + GitHub) is credential-config-driven; the backend - reports available providers via `/auth/oauth/providers` and the login page renders - buttons only for configured ones. A self-host without Google credentials gets - email+password automatically. Self-hosters may bring their own OAuth client IDs. -- Why OpenHands differs: OpenHands' OSS build is a **single-user local tool with no - accounts**, so their `enterprise/` contains the entire multi-user SaaS server - (OAuth, SMTP, billing, orgs). Humanly Community **is** a multi-user server - (instructor/student flows are the paper's core use case) — auth/email are core - product, not SaaS extras. We copy OpenHands' repo mechanics, not their feature split. - -**The edition boundary is a commercial-value boundary, not a deployment boundary.** -Deployment differences (which SMTP, which OAuth client, which hostnames) are env -config in the infra repo. Candidates for future `ee/` features: enterprise SSO -(SAML/OIDC/SCIM), plan/quota enforcement, team/org workspaces, cross-tenant admin -analytics, managed AI model pool. Not candidates: auth, email, storage adapters -(local/GCS are both config-driven), rate limiting, tracker, certificates, detector -framework. - -## Tasks - -### Task 1 — Land Cloud-edition deploy in the infra repo (completed) -In `Humanly-Lab/humanly-cloud-infra`: -1. Rebase PR #11 (`feat/1043-cloud-edition-deploy`) onto current main. The only - conflict is the single-line `community.lock` (both sides moved it); everything - else in the PR (ee dockerfiles, `-cloud` tags, edition env, ee-migrations bundle, - composition guards) reviewed line-by-line and sound. -2. Resolve the lock conflict by advancing to `d500df8f` or later — NOT the PR's - `23754da5`, which predates the PolyForm `ee/LICENSE` (finding 1). -3. Build images from the pinned revision using `ee/docker/*.Dockerfile` - (`EDITION=cloud`, `NEXT_PUBLIC_EDITION=cloud`); reuse - `scripts/ci/assert-edition-image.sh` from the product repo as a deploy-time gate. -4. Deploy; verify health reports `edition: "cloud"` and `GET /api/v1/billing/plan` - responds. Verify the Community quickstart still boots independently - (`edition: "community"`). - -### Task 2 — Community hardening fixes (completed) -Production-boot guard for `AI_ENCRYPTION_KEY` (mirror the email-config pattern) and -replace the hardcoded `developer.writehumanly.net` in the tracker instructions with -the env-derived admin URL. One small PR in the product repo. - -### Task 3 — Rename the Cloud control plane to `humanly-cloud-infra` (completed) -The GitHub repository, local checkout, and explicit references now use -`Humanly-Lab/humanly-cloud-infra`. The production VM has no checkout of the old -infra repository, so no VM remote required migration; its existing Git checkouts -correctly remain Community repositories. - -### Task 4 — `ee/LICENSE` final read-through (maintainer) -PolyForm Free Trial 1.0.0 replaced the placeholder — better than homemade. Maintainer -gives it one final read (notably the 32-day trial term) and confirms it matches the -pricing-page story. - -### Task 5 — Documentation upkeep -- Keep `docs/EDITIONS_REFACTOR_PLAN.md` as the boundary record (already updated). -- `docs/EDITIONS_DEVELOPMENT.md` is the developer guide for edition work — update it - whenever the seams (registry, alias, CI checks) change. -- README "Editions" section and `docs/SELF_DEPLOY.md` describe Community accurately. - -### Task 6 — Directory naming (settled: keep `ee/`) -`enterprise/` was considered and rejected: "Enterprise" is a specific pricing tier -while `ee/` hosts all paid features (including Pro-tier billing), and `ee` is the -majority convention (GitLab, PostHog, Cal.com, n8n). - -## Standing guardrails - -- Community remains fully usable multi-user (auth, email via SMTP, certificates, - detector framework); README/paper claims stay true. -- `packages/*` never imports `ee/` (CI-enforced). -- No secrets or managed-production topology in this repo. -- All product code lives here; the infra repo carries deployment only. diff --git a/docs/EDITIONS_REFACTOR_PLAN.md b/docs/EDITIONS_REFACTOR_PLAN.md deleted file mode 100644 index 745590b4..00000000 --- a/docs/EDITIONS_REFACTOR_PLAN.md +++ /dev/null @@ -1,192 +0,0 @@ -# Editions Refactor Plan: Humanly Community + Humanly Cloud - -**Status:** implemented architecture; maintained as the edition-boundary record -**Owner:** @ShenzheZhu -**Audience:** implementation agent (Codex). This document is self-contained; do not assume other context. - ---- - -## 1. Goal - -Ship two editions from **one codebase** in this repository: - -| | Humanly Community | Humanly Cloud | -|---|---|---| -| License | MIT (everything outside `ee/`) | PolyForm Free Trial License 1.0.0 (`ee/` only) | -| Distribution | Self-hosted (quickstart / SELF_DEPLOY) | Managed SaaS at writehumanly.net, deployed from `Humanly-Lab/humanly-cloud-infra` | -| Code visibility | Public | **Public (source-available)** — visible in this repo, paid to use | - -Model to imitate: **OpenHands** (MIT repo, `enterprise/` directory source-available under a paid license) and **PostHog** (MIT repo, `ee/` directory under `ee/LICENSE`). Anti-goal: the pre-2019 GitLab setup (two repos, EE forked from CE) — GitLab abandoned it because of constant merge conflicts and release desync; we will not maintain cloud application code in a second repo. - -The `Humanly-Lab/humanly-cloud-infra` repo **stays deploy-only** (compose files, -nginx, deploy workflow, secrets, private artifacts such as model weights). -Recent PRs #1034/#1035/#1037 already moved managed-production infra there. No -application code lives in the infra repository. - -## 2. Feature policy (what goes where) - -Using an external provider is **not** sufficient reason to move a feature into -Enterprise. Community must retain everything required to operate a complete, -secure self-hosted workflow. Enterprise owns organization governance, -managed-service operations, commercial entitlements, and paid implementations. - -| Feature | Community | Cloud | Mechanism | Status | -|---|---|---|---|---| -| Writing environment, tracking, certificates, replay, anomaly-pattern detector | ✅ | ✅ | stays in `packages/*` (MIT) | Current | -| Local accounts, password hashing, sessions, email verification, and password reset | ✅ | ✅ | stays in backend core; Cloud may extend through core-owned identity hooks | Current | -| Configurable OAuth login | ✅ | ✅ | provider configuration and public adapters stay in core | Current | -| Generic email delivery for account lifecycle messages | ✅ | ✅ | delivery contract and self-hosted console/SMTP/provider configuration stay in core | Current | -| Organization identity governance (workspaces, RBAC, SSO/SAML/OIDC policy, SCIM, domain claims) | ❌ | ✅ | future `ee/` identity/governance packages registered through core hooks | Planned | -| Managed email operations (organization invitations, tenant branding, billing/quota alerts, telemetry, suppression, bounce handling) | ❌ | ✅ | future `ee/` notification package plus private provider credentials in infrastructure | Planned | -| AI provider interfaces and bring-your-own key | ✅ | ✅ | stays in core; external AI use alone is not an Enterprise boundary | Current | -| Humanly-managed AI credits, tenant quotas, metering, and entitlements | ❌ | ✅ | `ee/` billing/entitlement implementation plus private provider credentials | Planned | -| Product APIs required by self-hosting | ✅ | ✅ | core routes and public contracts stay MIT | Current | -| Managed production API keys, tenant service accounts, rate tiers, webhooks, and SLA-backed access | ❌ | ✅ | future `ee/` API-governance implementation | Planned | -| Typing-detector **framework** (interfaces, config keys, `packages/inference` service) | ✅ (bring-your-own inference endpoint) | ✅ | stays MIT — the paper describes this component; the OSS release must keep it | Current | -| Typing-detector **managed model** (Humanly-hosted weights + serving) | ❌ | ✅ | weights/serving config live in `humanly-cloud-infra`; `ee/` carries managed-client and entitlement glue | Planned after 2026-08-04 | -| Billing route, plan seam, and schema scaffold | ❌ | ✅ | `ee/packages/billing` and `ee/migrations` | Current skeleton | -| Production billing providers, plan enforcement, usage limits, and invoicing | ❌ | ✅ | extend `ee/packages/billing`; credentials stay in private infrastructure | Planned | -| Managed-hosting glue (multi-tenant config, hostname audit exceptions) | ❌ | ✅ | `ee/` + `humanly-cloud-infra` | Current foundation | -| Tenant storage quotas, retention policy, legal hold, organization audit export, and consolidated analytics | ❌ | ✅ | future `ee/` governance packages; storage credentials and topology stay private | Planned | -| Managed LMS/SIS provisioning, supported connectors, onboarding, and custom integrations | ❌ | ✅ | public extension points stay in core; paid implementations and operations live in `ee/` | Planned | - -Rule of thumb: **interfaces, complete self-hosted account flows, and usable -defaults in MIT core; organization governance, managed operations, and paid -implementations in `ee/`; secrets, provider credentials, production topology, -and model weights in `humanly-cloud-infra`.** - -Authentication and email require an explicit distinction: - -- Community owns the local account lifecycle. A self-hosted deployment must be - able to register users, verify addresses, authenticate, and recover passwords - without importing Enterprise code. -- Enterprise may add organization identity policy, provisioning, invitations, - tenant-branded notifications, delivery operations, and commercial alerts. -- Provider credentials are deployment secrets. Their presence in SMTP, - SendGrid, OAuth, or another external system does not change the source-code - ownership of the underlying capability. - -## 3. Target layout - -``` -humanly/ -├── LICENSE # MIT, amended: "except the ee/ directory" -├── packages/ # existing 8 packages, MIT, unchanged locations -├── ee/ -│ ├── LICENSE # PolyForm Free Trial 1.0.0 (source-available) -│ ├── README.md # what ee/ is, pointer to pricing -│ ├── packages/ -│ │ └── billing/ # first EE package: @humanly-ee/billing -│ └── migrations/ # cloud-only SQL migrations -└── pnpm-workspace.yaml # adds "ee/packages/*" -``` - -Package namespace: `@humanly-ee/*`. EE packages may depend on `@humanly/*`; **`packages/*` must never import from `ee/`** (enforced in CI, see Phase 5). - -## 4. Edition switch - -- Backend: env `EDITION=community|cloud` (default `community`). -- Frontends: build arg / env `NEXT_PUBLIC_EDITION=community|cloud` (default `community`). Passed as a Docker build arg like the existing `NEXT_PUBLIC_API_URL`. -- Shared registry in `@humanly/shared` (new file `packages/shared/src/config/edition.ts`): - -```ts -export type Edition = 'community' | 'cloud'; -export type EditionFeature = 'billing' | 'managedTypingDetector'; - -const EDITION_FEATURES: Record = { - community: [], - cloud: ['billing', 'managedTypingDetector'], -}; - -export const normalizeEdition = (v: string | undefined | null): Edition => - v === 'cloud' ? 'cloud' : 'community'; - -export const hasFeature = (edition: Edition, f: EditionFeature): boolean => - EDITION_FEATURES[edition].includes(f); -``` - -All gating anywhere in the stack goes through `hasFeature` — no ad-hoc `process.env.EDITION` checks outside the two entry points that read the env once. - -## 5. Implementation phases (one PR each) - -### Phase 0 — Licensing and scaffold -1. Amend root `LICENSE`: keep MIT text, prepend a scope note: *"This license applies to all content of this repository **except** the `ee/` directory, which is licensed under `ee/LICENSE`."* (Match PostHog's LICENSE wording style.) -2. Create `ee/LICENSE` using the unmodified PolyForm Free Trial License 1.0.0. - The standard license permits evaluation for less than 32 consecutive - calendar days; use outside its terms requires a separate commercial license. -3. Create `ee/README.md` (edition explanation + link to /pricing). -4. Add `ee/packages/*` to `pnpm-workspace.yaml`. -5. README.md: add an "Editions" section (Community vs Cloud table, licensing note). - -**Accept:** `pnpm install` still green; repo licensing is unambiguous; no behavior change. - -### Phase 1 — Edition registry in shared -1. Add `packages/shared/src/config/edition.ts` as in §4; export from shared index. -2. Unit tests for `normalizeEdition` / `hasFeature`. - -**Accept:** `pnpm --filter @humanly/shared build && test` green; no consumer changes yet. - -### Phase 2 — Backend gating -1. Read `EDITION` once at startup (config module), expose via existing config pattern. -2. In `packages/backend/src/app.ts`, register EE routes conditionally with **dynamic import** so a community build/runtime never requires `ee/` to be present: - ```ts - if (hasFeature(edition, 'billing')) { - const { registerBillingRoutes } = await import('@humanly-ee/billing'); - registerBillingRoutes(app); - } - ``` -3. Migrations: cloud-only SQL lives in `ee/migrations/`. `scripts/run-migrations.sh` already accepts `MIGRATIONS_DIR`; extend it to accept a colon-separated list (`MIGRATIONS_DIR="packages/backend/src/db/migrations:ee/migrations"` for cloud). Community default unchanged. -4. Expose edition in the health endpoint payload (`edition: 'community'`) for ops visibility. - -**Accept:** community boots with no `ee/` packages installed (verify by temporarily removing `ee/` in CI); cloud boots with billing routes mounted; migration script applies both dirs in cloud mode. - -### Phase 3 — Frontend gating -1. Plumb `NEXT_PUBLIC_EDITION` through both Next.js apps (frontend, frontend-user); helper `getEdition()` in each app's lib reading the env once. -2. Gate EE UI behind `hasFeature(...)`; use literal `process.env.NEXT_PUBLIC_EDITION` comparisons at the branch site so Next.js dead-code-eliminates EE component imports from community bundles. -3. Establish the UI pattern for gated features: **hidden entirely in community** (no upsell teasers inside the product for now — marketing/pricing pages already communicate editions). - -**Accept:** community bundle contains no EE component code (`grep` the `.next` output for an EE-only marker string); cloud build renders the gated nav/pages. - -### Phase 4 — First EE package: `@humanly-ee/billing` (walking skeleton) -Prove the seam end-to-end with a minimal package: -1. `ee/packages/billing/` — `package.json` (name `@humanly-ee/billing`, license field pointing at `ee/LICENSE`), `src/index.ts` exporting `registerBillingRoutes(app)` with a stub `GET /api/v1/billing/plan` returning the current plan (`free`), and one `ee/migrations/9000-billing-schema.sql` placeholder table. -2. Frontend-user: a gated `/settings/billing` stub page (cloud only). -3. Stripe integration is **out of scope** for this phase — the goal is the wiring, not the billing product. - -**Accept:** cloud build serves the stub route + page; community build has neither; typecheck green in both editions. - -### Phase 5 — CI matrix and purity guards -1. Extend `.github/workflows/ci.yml` (jobs: script-audit, lint, typecheck, test-runnable) to a **matrix over `EDITION=community|cloud`** for typecheck + tests; lint once. -2. Add a **boundary lint**: fail CI if anything under `packages/` imports from `ee/` (simple grep or eslint no-restricted-imports). -3. Add a **community purity check**: build the community Docker images and assert `ee/` content is absent from the image filesystem and bundles. -4. Docker: community Dockerfiles do not `COPY ee/`; add cloud build args - (`EDITION=cloud`) — the cloud deploy workflow in `humanly-cloud-infra` - consumes them. -5. Tag/release convention: single version tag per release; images published as `humanly-*:` (community) and `humanly-*:-cloud`. - -**Accept:** PRs run both editions; a `packages/ → ee/` import breaks CI; community image is provably ee-free. - -## 6. Guardrails - -- **Never regress Community.** Community must remain a genuinely usable product (writing env, tracking, certificates, anomaly-pattern detector, BYO-endpoint typing detector). The EMNLP paper and README describe the open-source release; their claims must stay true. -- **No secret code in this repo.** Anything that must stay private (model - weights, tenant secrets, prod config) belongs in `humanly-cloud-infra` or a - private bucket — `ee/` is public. -- **One codebase, no forks.** If a change needs core + ee edits, it ships as one PR in this repo. -- **License changes require maintainer sign-off.** Keep the canonical PolyForm - text unmodified; any future custom commercial terms require legal review. -- Keep the existing managed-hostname audit (from #1036/#1037) green: community sources must not reference managed-production hostnames; ee/ files are exempt. - -## 7. Out of scope / later - -- Stripe/actual billing product (Phase 4 only lays the wiring). -- A license-key/entitlement server for self-hosted *cloud* customers (OpenHands-style paid self-host). Initially, Cloud = deployed by us; entitlement is implicit. -- Marketing copy changes (pricing page already distinguishes editions). - -## References - -- GitLab, "Why we merged CE and EE into a single codebase": https://about.gitlab.com/blog/a-single-codebase-for-gitlab-community-and-enterprise-edition/ -- PostHog LICENSE / ee/LICENSE: https://github.com/PostHog/posthog/blob/master/LICENSE -- OpenHands (MIT + source-available `enterprise/`): https://github.com/OpenHands/OpenHands/blob/main/LICENSE -- PolyForm Free Trial License 1.0.0: https://polyformproject.org/licenses/free-trial/1.0.0 -- OCV licensing handbook: https://handbook.opencoreventures.com/startup-manual/fundamentals/licensing-and-distribution diff --git a/scripts/ci/community-boundary.mjs b/scripts/ci/community-boundary.mjs index 577496ce..10c1dddd 100644 --- a/scripts/ci/community-boundary.mjs +++ b/scripts/ci/community-boundary.mjs @@ -107,7 +107,7 @@ if ( || referenceViolations.length > 0 || editionImportViolations.length > 0 ) { - console.error('Community boundary audit failed. Managed Cloud deployment belongs in humanly-cloud-infra.'); + console.error('Community boundary audit failed. Official Humanly deployment belongs only in the private humanly-cloud repository.'); for (const violation of pathViolations) { console.error(`- forbidden path: ${violation}`); }