[docs] close SM-06 checkpoint handoff

This commit is contained in:
Kunthawat Greethong
2026-08-16 00:12:29 +07:00
parent ced77afaf2
commit 372f3165ce
5 changed files with 811 additions and 0 deletions

View File

@@ -0,0 +1,502 @@
# Chatwoot Private Rebrand & Zero-Telemetry Implementation Plan
> **For Hermes:** Execute task-by-task with todo progress tracking and per-task commits. Use TDD for behavior changes. Run `requesting-code-review` before delivery. Do not push, publish, or deploy without explicit user approval.
**Goal:** สร้าง community-edition fork ที่ไม่มี Chatwoot branding ใน runtime/public product, ไม่ส่ง telemetry หรือ installation/application statistics ไปยัง Chatwoot/third parties และยัง rebase/merge จาก upstream `chatwoot/chatwoot` ได้โดยมี conflict ต่ำ
**Architecture:** คง core Rails/Vue architecture และ upstream internals ไว้ แต่สร้าง privacy boundary และ brand configuration layer ที่จุดรวม แทนการเปลี่ยนชื่อ internal namespace หลายพันจุดทันที. Network behavior ต้องเป็น allow-by-purpose: outbound ที่ผู้ดูแลตั้งค่าเพื่อใช้งาน channel/integration ยังคงอยู่ ส่วน telemetry, hub sync, analytics, remote changelog, hosted push relay และ script injection ถูกตัดออกจาก build/runtime.
**Tech Stack:** Ruby on Rails, Sidekiq, PostgreSQL, Redis, Vue 3, Vite, pnpm, RSpec, Vitest, Playwright.
## Small-model execution package
This document remains the architecture and acceptance source of truth. Implementation is split into bounded runbooks at [`chatwoot-private/README.md`](chatwoot-private/README.md). The package contains an execution contract plus `SM-*` tasks with dependencies, exact file scope, RED/GREEN commands, stop conditions, commit boundaries, release gates, and upstream-maintenance workflow. Give a small model only one `SM-*` task at a time; the orchestrator must review the exact diff and test evidence before advancing.
---
## 0. Audit baseline
- Repository: `https://github.com/chatwoot/chatwoot.git`
- Local path: `/Users/kunthawat/Gitea/Chatwoot`
- Audited branch: `develop`
- Audited commit: `9a73c1473ffa0ae6a9c7725046b8ca17922dcc83`
- Clone state: shallow — fetch full upstream history before the first merge/rebase rehearsal
- Application/toolchain baseline: Chatwoot 4.16.2; Ruby 3.4.4; Rails 7.2.3.1; Node 24; pnpm 10.2.0; PostgreSQL 16 + pgvector; Redis
- Working tree before report: clean
- Audit time: `2026-08-15 09:25:34 +0700`
- Root license: MIT (`LICENSE`)
- Enterprise subtree: proprietary (`enterprise/LICENSE`) — ห้ามถือว่าใช้งาน/ดัดแปลง/ขายได้ตาม MIT
### Scope decision
ฐานผลิตภัณฑ์ควรเป็น **Community Edition เท่านั้น**. หากต้องการ enterprise features ต้องทำ commercial agreement แยก; อย่าแก้ `enterprise/` แล้วนำไปใช้เพียงเพราะ source อยู่ใน repository.
---
# Audit findings
## 1. Architecture map
| Layer | Source of truth | Runtime role |
|---|---|---|
| Rails web/API | `app/controllers/`, `app/models/`, `app/services/`, `config/routes.rb` | Dashboard API, widget API, integrations, webhooks |
| Background workers | `app/jobs/`, `config/schedule.yml`, Sidekiq | scheduled sync, message processing, notifications |
| Dashboard frontend | `app/javascript/dashboard/`, `app/javascript/entrypoints/v3app.js` | Vue dashboard/auth |
| Widget/SDK | `app/javascript/widget/`, `app/javascript/sdk/` | customer chat widget and browser SDK |
| Shared frontend | `app/javascript/shared/` | branding config, links, common components |
| Runtime config | `config/installation_config.yml`, `GlobalConfig`, `DashboardController::GLOBAL_CONFIG_KEYS` | brand URLs/logos, analytics token, behavior flags |
| Hub integration | `lib/chatwoot_hub.rb`, `enterprise/lib/enterprise/chatwoot_hub.rb` | installation sync, registration, cloud/plan info |
| Build/deploy | `package.json`, `Gemfile`, `Dockerfile`, `docker/`, `Procfile` | Rails/Vite builds and services |
| Community tests | `spec/`, frontend tests under JS tree | RSpec/Vitest/Playwright gates |
## 2. Verdict: มี outbound telemetry จริง
**คำตอบสั้น:** มี. Production job ส่ง installation configuration และ usage metrics ไป Chatwoot Hub. การตั้ง `DISABLE_TELEMETRY=true` ในโค้ดปัจจุบัน **ไม่ได้หยุด request**; มันตัดเฉพาะ metrics บางส่วน แต่ยัง POST `instance_config` ไป Hub อยู่.
### 2.1 Chatwoot-controlled outbound inventory
| Severity | Flow | Destination | Trigger / call path | Payload / data | Current opt-out | Decision |
|---|---|---|---|---|---|---|
| Critical | Hub installation sync | `CHATWOOT_HUB_URL` default `https://hub.2.chatwoot.com` + `/ping` | `config/schedule.yml``Internal::TriggerDailyScheduledItemsJob``Internal::CheckNewVersionsJob``ChatwootHub.sync_with_hub` | installation identifier, version, installation URL, locale, active accounts/users/inboxes/conversations/messages, account/user/inbox/channel breakdowns | `DISABLE_TELEMETRY` removes `instance_metrics` only; request and `instance_config` remain | **Remove runtime flow entirely** |
| Critical | Chatwoot push relay | `https://hub.2.chatwoot.com/send_push` | notification job → `PushNotificationService``ChatwootHub.send_push` | installation config, FCM device token, title/body, conversation/navigation payload, platform options and analytics label | `ENABLE_PUSH_RELAY_SERVER` defaults true; bypassed only when own Firebase credentials exist | **Remove relay fallback; require own FCM/VAPID** |
| High | cwctl command telemetry | hardcoded `https://hub.2.chatwoot.com/events` | most `deployment/setup_20.04.sh` command branches → `report_event` | installation identifier + invoked cwctl action | no gate or opt-out | **Remove reporter and every callsite** |
| High | Signup-domain enrichment | signup email domain via HTTPS + DNS MX lookup | account create → `BrandingEnrichmentJob``WebsiteBrandingService` | source IP/request metadata leaks to inferred company domain/DNS; locally stores full email, site branding/social data and inferred mail provider in `brand_info` | always enqueued when email exists | **Remove from privacy baseline or make explicit disabled-by-default admin feature** |
| Low | Remote changelog | `https://hub.2.chatwoot.com/changelogs` | cloud-only sidebar mount → `dashboard/api/changelog.js#fetchFromHub` | browser request metadata/IP; no app metrics shown in call | normal self-hosted UI excludes it, but no privacy-wide master switch | **Remove Cloud promotional UI/fetch from private fork** |
| Medium | Optional onboarding registration | Hub registration endpoint | `Installation::OnboardingController` when `subscribe_to_updates` checked → `ChatwootHub.register_instance` | company, name, email, installation identifier | user checkbox | **Remove registration/subscription step** |
| Medium | Enterprise plan/license sync | same Hub path via enterprise prepend | daily Hub sync | community payload plus plan/license/support config response | coupled to Hub | **Exclude enterprise path unless separately licensed** |
**Evidence:**
- `lib/chatwoot_hub.rb:21-93`
- `app/jobs/internal/check_new_versions_job.rb:4-8`
- `app/jobs/internal/trigger_daily_scheduled_items_job.rb:4-19`
- `config/schedule.yml`
- `spec/lib/chatwoot_hub_spec.rb:17-33` explicitly proves `DISABLE_TELEMETRY` still POSTs `instance_config`
- `app/services/notification/push_notification_service.rb`
- `deployment/setup_20.04.sh:1133-1180,1281-1331`
- `app/controllers/api/v1/accounts_controller.rb:24-34,77-86`
- `app/jobs/account/branding_enrichment_job.rb`
- `app/services/website_branding_service.rb:10-54,117-145`
- `app/javascript/shared/constants/links.js:11`
- `app/javascript/dashboard/api/changelog.js:11-12`
- `app/controllers/installation/onboarding_controller.rb:30-36`
### 2.2 Third-party analytics/error/observability
| Surface | Initialization | Default behavior | Data risk | Decision |
|---|---|---|---|---|
| Amplitude browser analytics | `@amplitude/analytics-browser`, `app/javascript/dashboard/helper/AnalyticsHelper/index.js`, initialized from `v3app.js` | only when `CLOUD_ANALYTICS_TOKEN` is present | event names/properties, user/account identifiers depending helper context | replace helper with local no-op; remove SDK/token exposure |
| Frontend Sentry | `v3app.js` dynamic Sentry init from `window.errorLoggingConfig` | depends on configured DSN | browser errors, URL, user context/breadcrumbs | remove remote init; preserve local console/log handling |
| Backend Sentry | initializer + `ChatwootExceptionTracker` Sentry branch | depends on DSN | exceptions, request/job context | keep the existing tracker API as a local-logging facade; remove its Sentry branch and Sentry dependency/config |
| New Relic / Datadog / Scout / Elastic APM | optional gems/initializers/env configs | configuration-dependent | traces, request metadata, DB/job context | exclude from privacy build; document unsupported remote APM |
| Arbitrary dashboard scripts | `DASHBOARD_SCRIPTS` loaded by `DashboardController#set_dashboard_scripts` and injected in layout | blank unless configured | any script can exfiltrate dashboard data | remove runtime injection capability |
| Marketing tracking config | `MARKETING_CONVERSION_TRACKING_CONFIG` | blank unless configured | conversion/browser tracking | remove from config and layouts |
### 2.3 Outbound that is not telemetry
อย่าลบ network ทั้งหมดแบบเหมารวม. Chatwoot เป็น omnichannel app; ฟีเจอร์หลักต้องติดต่อภายนอกเมื่อ admin เปิดใช้:
- Meta/Facebook/Instagram/WhatsApp APIs
- Twilio/Telegram/Line/TikTok and channel providers
- SMTP/IMAP/email delivery
- Object storage (S3-compatible)
- user-configured webhooks
- OAuth/OIDC/SAML providers
- AI/LLM providers when explicitly configured
- CAPTCHA, maps, translation and other optional integrations
สิ่งเหล่านี้ควรเป็น **administrator-initiated, documented, credential-gated** ไม่ใช่ startup/daily telemetry. Verification ต้องแยก “unexpected egress” ออกจาก “feature-requested egress”.
### 2.4 Additional privacy-sensitive hooks
- `ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL`: optional webhook; can send security events externally.
- `INSTALLATION_EVENTS_WEBHOOK_URL`: installation lifecycle webhook, especially cloud/enterprise paths.
- `DASHBOARD_SCRIPTS`: remote/arbitrary JS injection.
- `MARKETING_CONVERSION_TRACKING_CONFIG`: browser conversion tracking.
- `SuperAdmin::SettingsController#refresh`: manual synchronous path into the same Hub sync, separate from the daily scheduler.
- Installer/cwctl updater: fetches `raw.githubusercontent.com/chatwoot/chatwoot/master/VERSION_CWCTL` and `get.chatwoot.app/linux/install.sh`; this is update traffic rather than telemetry, but a fully offline fork must replace or remove it and pin/verify any downloaded installer.
- Onboarding newsletter consent is rendered **checked by default** in `app/views/installation/onboarding/index.html.erb:62-66`.
Privacy edition should remove these from ordinary community runtime unless there is an explicit, audited first-party destination and admin consent.
---
## 3. Branding inventory
Branding is not one folder. It is spread across configuration, templates, runtime links, copy, assets, manifests, package names, technical APIs and tests.
### 3.1 Central runtime brand configuration
Primary file: `config/installation_config.yml`
High-value keys:
- `INSTALLATION_NAME`
- `BRAND_NAME`
- `BRAND_URL`
- `WIDGET_BRAND_URL`
- `LOGO`
- `LOGO_DARK`
- `LOGO_THUMBNAIL`
- `API_CHANNEL_NAME`
- `API_CHANNEL_THUMBNAIL`
- `TERMS_URL`
- `PRIVACY_URL`
- `DISPLAY_MANIFEST`
- `CLOUD_ANALYTICS_TOKEN`
- `CHATWOOT_INBOX_TOKEN`
Exposure path:
`config/installation_config.yml``GlobalConfig``DashboardController::GLOBAL_CONFIG_KEYS``app/views/layouts/vueapp.html.erb``window.globalConfig` → Vue store/components.
Change approach:
1. replace defaults with new product values/assets;
2. remove telemetry/script keys from public global config;
3. make missing brand assets fail build/test instead of falling back to Chatwoot URLs.
### 3.2 Visible frontend sources
| Category | Important sources | Change approach |
|---|---|---|
| Dashboard layout/title/favicon | `app/views/layouts/vueapp.html.erb`, `app/views/layouts/application.html.erb` | drive title/meta/icons from first-party config and local assets |
| Shared branding component | `app/javascript/shared/components/Branding.vue` | render own mark; remove Chatwoot fallback/powered-by URL |
| Hardcoded links | `app/javascript/shared/constants/links.js` | replace docs/help/changelog links; delete Hub changelog URL |
| Dashboard copy | `app/javascript/dashboard/i18n/locale/**` | convert product proper noun to interpolation/brand token; do not manually fork every translation where avoidable |
| Widget copy/powered-by | `app/javascript/widget/**`, widget locale files | remove powered-by row and Chatwoot URLs; replace bot/default image |
| Email/views | `app/views/**`, `app/mailers/**` | use brand config helpers and local URLs; remove Chatwoot support/docs links |
| Installation/onboarding | installation controller/views/frontend | remove update subscription and Chatwoot-specific onboarding text |
| Help center/portal | portal views/components/locales | replace visible name, favicon, metadata and link targets |
### 3.3 Assets and public metadata
Audit/replace at source:
- `public/manifest.json`
- `public/favicon*`
- `public/apple-touch-icon*`
- `public/assets/images/chatwoot_bot.png`
- logo files under `public/` and `app/javascript/**/assets/`
- mail/logo images and social/Open Graph images
- PWA icons and browser metadata
- screenshots/seed/demo content that ships into production
Do not edit generated Vite bundles. Replace the source asset then verify `public/` and built output.
### 3.4 Brand strings and URLs
Search gates must include at least:
```text
Chatwoot
chatwoot
CHATWOOT
chatwoot.com
chwt.app
hub.2.chatwoot.com
@chatwoot/
window.$chatwoot
window.chatwootSettings
window.chatwootSDK
```
The English dashboard locale alone has hundreds of Chatwoot references; other locales multiply this surface. Use an automated inventory with allowlist categories, not a one-time manual grep.
### 3.5 Important pushback: do not rename every internal identifier in Phase 1
“ไม่มีแบรนด์ในผลิตภัณฑ์” และ “ไม่มีคำว่า Chatwoot ใน source” เป็นคนละงาน.
Phase 1 acceptance should require:
- no visible Chatwoot text/logo/link in runtime;
- no Chatwoot-controlled telemetry/relay/changelog calls;
- no Chatwoot branding in public metadata, email, widget and built artifacts.
Keep these internal compatibility surfaces temporarily:
- Ruby modules/classes such as `ChatwootHub`/`ChatwootApp` until their behavior is neutralized;
- npm workspace/package imports such as `@chatwoot/utils`;
- public widget JS APIs such as `window.$chatwoot` and `window.chatwootSettings`, with deprecated aliases if a new API is introduced.
Renaming all internals immediately creates a permanent merge-conflict factory and breaks existing customer embed code. Do internal namespace migration only as a separate, measured phase after upstream sync is stable.
---
# Implementation plan
## Phase 1 — Fork topology and legal boundary
### Task 1: Establish remotes and branch policy
**Objective:** Make upstream syncing explicit without publishing anything yet.
**Git topology:**
- `upstream``https://github.com/chatwoot/chatwoot.git`
- `origin` → future private Gitea repository (user approval required before creation/push)
- immutable base tag: `upstream-audit-2026-08-15`
- working branches: `privacy/*`, `brand/*`, `ux/*`
**Steps:**
1. Convert the shallow checkout into a full-history clone (`git fetch --unshallow` or an equivalent full fetch) and verify the audited commit remains reachable.
2. Add/rename remotes only after private Gitea repository name is approved.
3. Create `privacy/zero-egress-baseline` from audited commit.
4. Commit by concern; never mix telemetry, branding and UX in one commit.
5. Keep a machine-readable upstream patch ledger in `docs/upstream-patches.yml`.
**Verification:**
```bash
git remote -v
git rev-parse --is-shallow-repository
git merge-base --is-ancestor upstream/develop HEAD
git status --short
```
### Task 2: Enforce Community Edition boundary
**Objective:** Prevent accidental reliance on proprietary enterprise code.
**Files likely affected:** build/deploy docs, CI configuration, feature flags; do not alter `enterprise/LICENSE`.
**Tests:** CI boot/build with community-only configuration; assert enterprise-only routes/features are absent or disabled.
---
## Phase 2 — Create privacy egress boundary
### Task 3: Add a centralized outbound-purpose policy
**Objective:** Every application-originated outbound path declares a purpose.
**Create:**
- `app/services/privacy/outbound_policy.rb`
- `spec/services/privacy/outbound_policy_spec.rb`
**Policy classes:**
- allowed only after admin configuration: channel, email, storage, webhook, OAuth, AI provider;
- denied unconditionally: telemetry, vendor hub sync, remote changelog, hosted push relay, marketing tracking, remote APM.
Do not monkey-patch `Net::HTTP` globally in production. Use the policy at first-party service boundaries and an egress test proxy/firewall in CI.
### Task 4: Remove Chatwoot Hub sync and registration
**Modify:**
- `app/jobs/internal/check_new_versions_job.rb`
- `app/jobs/internal/trigger_daily_scheduled_items_job.rb`
- `lib/chatwoot_hub.rb`
- `app/controllers/super_admin/settings_controller.rb`
- `app/controllers/installation/onboarding_controller.rb`
- related routes/views/specs
**Behavior:**
- no `/ping`, registration or Hub version request;
- version remains local from `Chatwoot.config[:version]`/build metadata;
- onboarding has no update-subscription payload;
- no environment flag can silently re-enable Hub communication.
**Tests first:**
- production daily job does not invoke any HTTP client;
- onboarding completion never calls Hub;
- repository scan rejects `hub.2.chatwoot.com` in runtime source.
### Task 5: Remove hosted push relay
**Modify:**
- `app/services/notification/push_notification_service.rb`
- `app/services/notification/push_test_service.rb`
- installation config/env docs/specs
**Behavior:** push works only with explicitly configured first-party FCM/VAPID credentials. Missing credentials produce clear local log/admin warning, not relay fallback.
**Tests first:** assert the relay path never calls `ChatwootHub.send_push` or `/send_push`; direct administrator-configured Firebase/Web Push paths still pass.
### Task 6: Remove remote changelog
**Modify:**
- `app/javascript/dashboard/api/changelog.js`
- changelog store/components/routes
- `app/javascript/shared/constants/links.js`
Either delete the feature or serve release notes from local static content controlled by this fork. No browser request to Chatwoot Hub.
### Task 6A: Remove cwctl telemetry and vendor updater dependencies
**Modify:** `deployment/setup_20.04.sh` and deployment tests/docs.
Delete `report_event`, its hardcoded Hub URL, installation-identifier lookup used by reporting, and every `report_event` call. For a fully private installer, replace the Chatwoot-hosted self-updater with fork-owned, checksum/signature-verified release artifacts or disable self-update entirely.
**Tests:** shell static check rejects Hub/get.chatwoot.app telemetry paths; invoke each cwctl command under a recording proxy and assert no reporting request.
### Task 6B: Remove automatic signup-domain enrichment
**Modify:**
- `app/controllers/api/v1/accounts_controller.rb`
- `app/jobs/account/branding_enrichment_job.rb`
- `app/services/website_branding_service.rb`
- `app/models/account.rb`
- onboarding enrichment frontend/state and related specs
For the strict privacy baseline, do not infer a company domain from signup email, fetch its website, issue an MX lookup, store the full email in `brand_info`, or block onboarding on enrichment. If retained later, it must be a disabled-by-default administrator feature with disclosure, SSRF/DNS-rebinding tests, data minimization and retention/deletion controls.
**Tests first:** creating an account performs no HTTP/DNS enrichment and stores no derived `brand_info`; onboarding proceeds without an enrichment state.
---
## Phase 3 — Remove third-party telemetry and script injection
### Task 7: Convert frontend analytics adapter to no-op
**Modify:**
- `app/javascript/dashboard/helper/AnalyticsHelper/index.js`
- `app/javascript/entrypoints/v3app.js`
- `package.json` and lockfile
- `DashboardController::GLOBAL_CONFIG_KEYS`
- `config/installation_config.yml`
Keep existing `AnalyticsHelper.track(...)` callsites initially to reduce upstream conflicts, but make the adapter side-effect-free and remove Amplitude SDK/token initialization.
**Tests:** spy on `fetch`, XHR and `sendBeacon`; dashboard actions produce zero analytics requests.
**Checkpoint status (2026-08-16):** Verified at `ced77af`. The exact committed range `a96b977..ced77af` adds the missing `.slice(*GLOBAL_CONFIG_KEYS)` before `app_config` merge, preventing stale/unrequested configuration values from reaching `window.globalConfig`. Fresh pre-commit and post-commit reviews passed with empty security and logic blocking arrays. Rails RSpec/RuboCop remain blocked by the local Ruby/Bundler mismatch.
### Task 8: Replace Sentry with local error tracking facade
**Create:** local `ErrorTracker` interfaces for Rails and frontend.
**Migrate in bounded commits:** initializer/config → service/job callsites → JS callsites → dependencies.
**Behavior:** structured local logs with secret/PII redaction; no remote DSN, envelope or SDK transport.
**Tests:** captured exception becomes local log event; network transport remains zero.
### Task 9: Remove remote APM and injected tracking scripts
**Modify:** Gemfile/package/config/initializers/layouts and runtime config for New Relic, Datadog, Scout, Elastic APM, `DASHBOARD_SCRIPTS`, and `MARKETING_CONVERSION_TRACKING_CONFIG`.
**Tests:** production boot with typical environment cannot initialize those agents; CSP/build scan sees no injected tracking script.
---
## Phase 4 — Centralized rebrand
### Task 10: Add a single brand manifest
**Create:** `config/brand.yml` (or equivalent typed config) with placeholders pending product name/assets:
- product name/short name
- canonical URL/support URL/docs URL
- logos light/dark/thumbnail
- favicon/PWA icons
- email sender display name
- widget powered-by policy
Runtime must fail validation if a required asset still points to Chatwoot-controlled domains.
### Task 11: Replace visible shell and metadata
**Modify:** layouts, manifest, favicons, logos, OG assets, shared `Branding.vue`, emails and widget assets.
**Verification viewports:** desktop plus mobile `320×568` and `500×768`; inspect dashboard login, onboarding, inbox, widget, help center and emails.
### Task 12: Migrate visible copy to brand interpolation
**Modify:** locale source files and view strings.
Do not blind replace technical API names. Replace customer-visible proper nouns and URLs using a locale-safe interpolation variable. Add a script that categorizes remaining hits as:
- allowed legal/upstream attribution;
- internal compatibility identifier;
- test fixture;
- **failure: customer-visible or runtime outbound**.
### Task 13: Preserve widget API compatibility
If a new public SDK namespace is required, add it while keeping deprecated aliases for `window.$chatwoot`, `window.chatwootSettings`, and related embed APIs for at least one migration window. Document that these are compatibility identifiers, not visible branding.
---
## Phase 5 — Verification gates
### Task 14: Static zero-brand/zero-vendor scan
**Create:** `script/privacy_audit` and CI job.
Scan source and built artifacts for forbidden domains/strings. Allowlist only license, upstream attribution, changelog documentation and internal compatibility identifiers. Fail on visible HTML/JS/CSS/email/manifest matches.
### Task 15: Dynamic egress test
Run Rails, Sidekiq, dashboard and widget behind a recording proxy or container firewall with DNS/network logs.
Exercise:
- fresh install/onboarding;
- account signup while recording both HTTP and DNS (must not probe the signup email domain);
- login/dashboard navigation;
- manual super-admin instance-status refresh;
- daily scheduled jobs;
- representative cwctl commands and installer/update checks;
- create account/inbox/contact/conversation/message;
- widget session;
- exception paths;
- push test without own credentials.
Expected unsolicited destinations: **none**. Then separately enable one integration at a time and confirm only its documented destinations.
### Task 16: Regression and visual QA
Run existing baseline before code changes, then after each phase:
```bash
bundle exec rspec <target specs>
pnpm test
pnpm lint
pnpm build
```
Final:
- targeted RSpec + full feasible suite;
- frontend unit/integration tests;
- Playwright E2E;
- production asset build;
- browser network capture;
- screenshot comparison at desktop, `320×568`, `500×768`.
### Task 17: Independent review and release gate
Run `requesting-code-review` pipeline:
1. static security/privacy scan;
2. tests/lint compared with baseline;
3. self-review for validation/error handling/debug leftovers;
4. independent reviewer subagent;
5. max two auto-fix cycles and re-verification.
No push/deploy/publication without explicit user approval.
---
# Upstream maintenance strategy
1. Keep `upstream/develop` untouched.
2. Rebase/sync monthly or per upstream release, not continuously.
3. Maintain small commits in this order: policy → Hub removal → relay removal → analytics → error tracking → brand config → visible assets/copy → UX.
4. Prefer no-op adapters and central configuration over deleting hundreds of callsites; this reduces recurring conflicts.
5. Add privacy tests that fail immediately when upstream reintroduces a vendor domain, analytics SDK or scheduled ping.
6. For every upstream sync, review diffs to:
- `lib/chatwoot_hub.rb`
- `config/schedule.yml`
- `config/installation_config.yml`
- `app/javascript/entrypoints/`
- analytics/error helpers
- push notification services
- `deployment/setup_20.04.sh`
- account signup branding-enrichment job/service
- layouts/manifest/assets
- package/Gem dependencies.
# Risks and decisions still needed
1. **Product identity:** final name, domains, logos, support/docs URLs and email sender name are not supplied; implementation can build the layer but cannot finish assets/copy truthfully.
2. **Widget compatibility:** recommended to keep old JS namespace aliases temporarily. Renaming immediately breaks customer embeds.
3. **Error observability:** strict local-only logs reduce central incident visibility. If later needed, support a user-operated self-hosted collector behind explicit admin configuration—not a vendor default.
4. **Push notifications:** removing Chatwoot relay means deployment must supply own FCM/VAPID setup or accept no mobile push.
5. **Enterprise features:** require separate legal/commercial decision; excluded from this plan.
6. **Trademark/support wording:** public site may truthfully state “compatible with / based on Chatwoot community edition” in legal attribution, but must not imply official Chatwoot affiliation. Product UI should remain independently branded.
# Definition of done
- No unsolicited HTTP/DNS connection to Chatwoot-controlled or telemetry/observability domains during the dynamic egress suite.
- `DISABLE_TELEMETRY` is unnecessary because telemetry code path is absent, not merely disabled.
- No customer-visible Chatwoot name, logo, URL, powered-by text, favicon, email branding or manifest metadata.
- Required feature integrations work only after explicit admin configuration and are documented by purpose/destination/data class.
- Community build, tests and E2E pass; baseline regressions are documented honestly.
- Upstream merge rehearsal succeeds and privacy scan catches a deliberately injected forbidden domain.

View File

@@ -0,0 +1,80 @@
# Runbook 06 — Remove Amplitude and Product Analytics
**Goal:** dashboard emits no user/account/page/event data to Amplitude or another analytics provider. Existing callers may keep a stable no-op adapter to reduce upstream conflicts.
## SM-06.01 — Convert analytics specs to a no-egress contract
**Modify:** `app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js`
Replace provider-behavior assertions with:
- importing helper does not import/initialize Amplitude;
- `init`, `identify`, `track`, and `page` return without network/provider calls;
- methods accept existing signatures so callsites need no bulk rewrite;
- passing a token cannot enable analytics.
Do not delete all tests. The no-op contract is the regression guard.
RED:
```bash
pnpm test app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js
```
## SM-06.02 — Make AnalyticsHelper provider-free no-op
**Modify:** `app/javascript/dashboard/helper/AnalyticsHelper/index.js`
Rules:
- remove `@amplitude/analytics-browser` import;
- retain exported class and default object for compatibility;
- no dynamic import, fetch, beacon, image pixel, local queue, user/email/account serialization;
- each public method is an explicit no-op.
GREEN:
```bash
pnpm test app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js
pnpm eslint app/javascript/dashboard/helper/AnalyticsHelper/index.js app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js
```
## SM-06.03 — Remove dashboard token delivery
**Modify:**
- `app/controllers/dashboard_controller.rb`
- `app/views/layouts/vueapp.html.erb`
- `spec/controllers/dashboard_controller_spec.rb`
Remove `CLOUD_ANALYTICS_TOKEN` from `GLOBAL_CONFIG_KEYS` and remove `window.analyticsConfig` serialization. Add response assertion that the rendered dashboard contains neither `analyticsConfig` nor configured test token.
Do not alter other global config keys.
Run:
```bash
bundle exec rspec spec/controllers/dashboard_controller_spec.rb
bundle exec rubocop app/controllers/dashboard_controller.rb spec/controllers/dashboard_controller_spec.rb
```
## SM-06.04 — Remove SDK and installation config
**Modify:**
- `package.json`
- `pnpm-lock.yaml` via `pnpm remove @amplitude/analytics-browser`
- `config/installation_config.yml`
Remove the `CLOUD_ANALYTICS_TOKEN` entry. Do not hand-edit unrelated lockfile entries.
GREEN:
```bash
pnpm install --frozen-lockfile
pnpm test app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js
rg -n '@amplitude/analytics-browser|CLOUD_ANALYTICS_TOKEN|analyticsConfig' app config package.json pnpm-lock.yaml --glob '!enterprise/**'
git diff --check
```
Expected runtime/config matches: zero; compatibility helper filenames may remain.
## Verified checkpoint status — 2026-08-16
- `a96b977` removed the Amplitude SDK, analytics token delivery, and `window.analyticsConfig`, but its controller regression exposed that an unrelated `CLOUD_ANALYTICS_TOKEN` returned by a config stub/source could still be serialized through `window.globalConfig`.
- `ced77af` fixes the root cause by applying `.slice(*GLOBAL_CONFIG_KEYS)` before merging explicit `app_config` values.
- Fresh pre-commit review `deleg_f9644984` and exact post-commit review `deleg_9b0a2f63` both passed with `security_concerns=[]` and `logic_errors=[]`.
- Ruby syntax, `git diff --check`, and the added-line security scan passed. Rails RSpec/RuboCop are blocked until the pinned Ruby 3.4.4/Bundler 2.5.16 toolchain is available.
Checkpoint commit: `privacy: disable product analytics by construction`

View File

@@ -0,0 +1,66 @@
# Chatwoot Private Fork — Small-Model Runbook Index
> ใช้ไฟล์นี้เป็นสารบัญเท่านั้น ห้ามส่ง runbooks ทั้งหมดให้โมเดลเล็กในครั้งเดียว
## Baseline
- Repository: `/Users/kunthawat/Gitea/Chatwoot`
- Branch/commit ที่ audit: `develop` / `9a73c1473ffa0ae6a9c7725046b8ca17922dcc83`
- Clone เป็น shallow; การ fetch history เป็นงานแยกที่ต้องมี network และต้องไม่ปนกับ code changes
- Master plan: `../2026-08-15_092534-chatwoot-private-rebrand.md`
## วิธีใช้
1. Orchestrator อ่าน `00-execution-contract.md` และ runbook ที่จะทำ
2. ส่งให้โมเดลเล็ก **ครั้งละหนึ่ง `SM-*` task** พร้อม current `git status --short`
3. โมเดลเล็กแก้เฉพาะ `allowed_files`
4. รันคำสั่ง RED/GREEN ตาม task
5. Orchestrator ตรวจ diff และผล test ก่อนส่ง task ถัดไป
6. Commit เฉพาะ checkpoint ที่ runbook ระบุ; ห้าม push/deploy
7. หาก command/path ไม่ตรงกับ tree ปัจจุบัน ให้หยุดและคืน `BLOCKED` ห้ามเดา
## Dependency order
| Order | Runbook | Gate |
|---:|---|---|
| 0 | `00-execution-contract.md` | ต้องแนบทุกครั้ง |
| 1 | `01-baseline-and-privacy-tests.md` | baseline evidence |
| 2 | `02-remove-hub-sync.md` | Hub `/ping`, registration, events = 0 |
| 3 | `03-remove-push-relay.md` | direct push ผ่าน; Hub relay ไม่มี |
| 4 | `04-remove-cwctl-telemetry.md` | cwctl ไม่ส่ง Hub event |
| 5 | `05-remove-signup-enrichment.md` | signup ไม่มี HTTP/DNS enrichment |
| 6 | `06-remove-amplitude.md` | analytics adapter no-op; SDK removed |
| 7 | `07-remove-sentry-and-apm.md` | local errors only; no remote SDK/agent |
| 8 | `08-remove-script-injection.md` | dashboard cannot inject tracking script |
| 9 | `09-brand-config-foundation.md` | ต้องมี brand inputs ก่อน GREEN |
| 10 | `10-visible-rebrand-and-compatibility.md` | no visible Chatwoot brand; aliases retained |
| 11 | `11-egress-e2e-and-release-gate.md` | dynamic egress + full review |
| 12 | `12-upstream-maintenance.md` | full history + merge rehearsal + anti-regression |
## Checkpoint status (2026-08-16)
- SM-06 `06-remove-amplitude.md`: verified checkpoint `ced77af`; exact post-commit review `deleg_9b0a2f63` passed. The dashboard config path now allowlists `GLOBAL_CONFIG_KEYS` before serializing `window.globalConfig`.
- SM-07 and the remaining SM-08 dashboard/config work: in progress in the unstaged worktree; keep them out of the SM-06 checkpoint.
- SM-09/10: blocked pending approved product identity, domains, assets and legal values.
- SM-11/12: pending full egress/release and upstream-history gates.
## Parallelism
Safe only after baseline:
- `03`, `04`, `05` can run in separate worktrees.
- `06`, `07`, `08` can run in separate worktrees.
- `09` must finish before `10`.
- `11` runs only after all merged into one exact tree.
- `12` establishes the recurring upstream workflow after the first verified release tree; its anti-regression gates apply to every later sync.
Do not parallel-edit `package.json`, `pnpm-lock.yaml`, `config/installation_config.yml`, `app/views/layouts/vueapp.html.erb`, or `DashboardController`; serialize tasks touching these files.
## Stop conditions
Stop immediately when:
- existing baseline test fails for reasons unrelated to the task;
- a required file differs materially from the audited commit;
- enterprise code becomes necessary;
- migration/schema change appears necessary;
- task needs credentials, external service, push, deploy, or public action;
- more than three unlisted files appear in the diff.

77
HANDOFF.md Normal file
View File

@@ -0,0 +1,77 @@
# HANDOFF
## Current state
Implementation is in progress in `/Users/kunthawat/Gitea/Chatwoot`; source changes are present and remain unpushed.
### Verified checkpoints
- `8ebb320` — privacy/branding audit harness.
- `832a7fd` — Hub sync/registration/telemetry removal.
- `1a3697f` — restored only the compatibility APIs required by direct push/billing callers.
- `8b1a033` — removed the Chatwoot Hub push relay; exact post-commit independent review passed.
- `3458272` — removed cwctl telemetry from the installer; exact post-commit independent review passed.
- `d9bf4c4` — removed community signup/onboarding website-branding enrichment; split exact post-commit reviews `deleg_5e2578c5` (backend) and `deleg_ee3e217f` (frontend) passed with empty blocking arrays.
- `ced77af` — SM-06 Product Analytics removal and dashboard config allowlist remediation; pre-commit review `deleg_f9644984` and exact post-commit review `deleg_9b0a2f63` passed with empty security/logic arrays.
### Current work / uncheckpointed work
- SM-05 community signup enrichment removal is checkpointed at `d9bf4c4`; pre-commit review, commit hooks, and split exact post-commit reviews passed. The enterprise service/spec remain unchanged and fail with `NameError: uninitialized constant WebsiteBrandingService`. This remains an explicit architecture/legal blocker; do not add a dummy service or restore remote enrichment.
- SM-06 is verified at `ced77af`. Its follow-up fix applies `.slice(*GLOBAL_CONFIG_KEYS)` before merging `app_config`, preventing stale/unrequested values such as `CLOUD_ANALYTICS_TOKEN` from reaching serialized `window.globalConfig`. SM-07 local-only observability and the remaining SM-08 dashboard/config script-injection work are still uncheckpointed. The community Help Center SM-08 correction is committed as `5619cc3`: remove GTM remote-container execution, reject/remove legacy GTM config, and retain only fixed allow-listed provider snippets for explicit admin configuration. Exact post-commit review `deleg_dd3db5b7` passed with empty blocking arrays. Enterprise marketing conversion tracking remains outside this community scope pending legal/compatibility review.
### Evidence and blockers
- SM-03 exact post-commit review passed for `1a3697f..8b1a033`; reviewer suggestions are to add explicit missing/blank/partial Firebase negative coverage and clarify legacy FCM configuration names in `.env.example`.
- SM-04 exact post-commit review passed for `1a3697f..3458272`; Bash 3.2 compatibility and fail-closed privacy-test behavior were verified.
- SM-05 exact committed range `3458272..d9bf4c4` passed split independent reviews: backend `deleg_5e2578c5` and frontend `deleg_ee3e217f`; both returned complete five-key verdicts with empty security and logic arrays. Frontend targeted run passed 9 suites/8 tests. Backend RSpec rerun is environment-blocked because Ruby 3.4.4 is required but only 3.4.10 is installed and the bundle has no usable `rspec` executable.
- SM-06 exact committed range `a96b977..ced77af` changed one controller line and passed fresh pre-commit/post-commit independent review. Raw merge leaked a stubbed analytics token; the allowlist slice now removes unrequested keys before layout serialization. Committed Ruby syntax, diff check, and security scan passed. Rails RSpec/RuboCop remain blocked because the active Ruby is 2.6.10 and Bundler 2.5.16 is unavailable.
- SM-08 exact post-commit review `deleg_dd3db5b7` passed for `d9bf4c4..5619cc3`; 57 portal model/controller examples, GTM regression 3/3, Ruby/ERB syntax, RuboCop, ESLint/Prettier, Vite build and privacy-audit harness passed. Full Vitest remains 407 passed/15 failed in six unrelated date/time/report-snapshot tests.
- No push or deploy has occurred.
- Approved product name, domains, logos/icons and sender/legal values are still missing; stop before SM-09/10.
- Full Rails/frontend suites, production-like DB checks, HTTP/DNS egress capture, `git fetch --unshallow`, and upstream merge rehearsal remain outstanding. Current full Vitest baseline has 407 passed/15 failed in six unrelated date/time and report-snapshot tests; do not report it as a clean suite until those baseline failures are resolved or formally waived.
- Temporary test services/configuration stay under `/tmp`; never commit them or secrets.
### Repository baseline
- Path: `/Users/kunthawat/Gitea/Chatwoot`
- Origin: `https://github.com/chatwoot/chatwoot.git`
- Branch: `develop`
- Initial baseline commit: `9a73c1473ffa0ae6a9c7725046b8ca17922dcc83`
- Latest checkpoint: `ced77af` (SM-06 Product Analytics plus dashboard config allowlist remediation; exact post-commit review `deleg_9b0a2f63` passed). SM-08 Help Center remains separately verified at `5619cc3`.
- Clone is shallow; fetch full upstream history before merge/rebase rehearsal.
### Primary deliverable
- `.hermes/plans/2026-08-15_092534-chatwoot-private-rebrand.md`
- Small-model execution index: `.hermes/plans/chatwoot-private/README.md`
- The execution package contains `00-execution-contract.md` plus runbooks `01``12` with 58 unique `SM-*` tasks. Send only one task at a time and review its exact diff/test evidence before advancing.
### Most important finding
`DISABLE_TELEMETRY=true` is not a complete opt-out. `lib/chatwoot_hub.rb` still posts `instance_config` to the Hub; it only omits `instance_metrics`. The corresponding behavior is asserted in `spec/lib/chatwoot_hub_spec.rb`.
### Other unsolicited/vendor-controlled egress to remove
- Chatwoot Hub daily sync and onboarding registration
- Chatwoot Hub `/send_push` relay fallback
- Hub-hosted changelog fetch
- silent cwctl command reporting to the Hub
- automatic signup email-domain website/DNS branding enrichment
- Amplitude browser analytics
- Sentry/frontend/backend remote error reporting
- optional remote APM agents
- arbitrary dashboard scripts and remote GTM container execution
- enterprise marketing conversion tracking (proprietary/cloud-only; pending separate legal/compatibility decision)
### Implementation order
1. Follow dependency order in `.hermes/plans/chatwoot-private/README.md`.
2. Establish the baseline and privacy audit harness before behavior changes.
3. Remove Hub sync/registration/manual refresh, push relay, cwctl telemetry, signup-domain enrichment and remote changelog.
4. Neutralize/remove analytics, remote error reporting/APM and script injection.
5. Stop at the Runbook 09 input gate until approved brand values/assets are supplied.
6. Add central brand defaults and replace visible assets/copy without renaming compatibility APIs.
7. Run static and isolated dynamic egress gates, positive feature-traffic tests, full review and visual QA.
8. Complete upstream-history and merge-rehearsal workflow; never mix upstream merge with customization commits.
### User decisions required before implementation
- Product name, canonical/support/docs domains, logos/icons and email sender name
- Private Gitea repository name
- Whether to keep deprecated widget API aliases (recommended: yes)
- Whether local-only logs are sufficient or a self-hosted observability endpoint is required
- Push notification plan and own FCM/VAPID credentials
### Safety/legal notes
- Root is MIT, but `enterprise/` is proprietary; do not ship enterprise features without a separate license.
- No remote repository was created, no files were pushed and no deployment was performed.

86
engineering-log.md Normal file
View File

@@ -0,0 +1,86 @@
# Engineering Log
## 2026-08-15 — Chatwoot privacy and rebrand audit
### Objective
Clone/inspect the official Chatwoot source and produce an evidence-based plan for a private-label community fork with no Chatwoot telemetry or visible product branding while preserving upstream syncability.
### Baseline
- Repository: `https://github.com/chatwoot/chatwoot.git`
- Branch: `develop`
- Commit: `9a73c1473ffa0ae6a9c7725046b8ca17922dcc83`
- Root license: MIT
- `enterprise/` license: proprietary and excluded from the recommended baseline
### Actions performed
1. Confirmed clone origin, branch, commit and clean working tree.
2. Mapped Rails, Sidekiq, Vue/Vite, widget, runtime configuration, manifests and deployment entry points.
3. Traced scheduled installation sync to `ChatwootHub.sync_with_hub`.
4. Confirmed from implementation and specs that `DISABLE_TELEMETRY` removes metrics but still POSTs installation configuration to Chatwoot Hub.
5. Traced Chatwoot-hosted push relay, remote changelog and optional onboarding registration.
6. Audited Amplitude, frontend/backend Sentry, optional APM integrations, arbitrary dashboard script injection and marketing tracking configuration.
7. Classified feature-required outbound integrations separately from unsolicited telemetry.
8. Mapped branding configuration, views, frontend components, locale copy, assets, manifest, mail/widget surfaces and technical compatibility identifiers.
9. Wrote implementation plan at `.hermes/plans/2026-08-15_092534-chatwoot-private-rebrand.md`.
10. Incorporated independent outbound-audit findings: silent cwctl command reporting, prechecked onboarding subscription, manual super-admin Hub refresh, and unconditional signup-domain website/DNS enrichment.
11. Split the master plan into a small-model execution package with an execution contract, dependency index, 12 runbooks and 58 unique bounded tasks covering baseline, privacy removals, observability, branding, E2E egress, release gates and upstream maintenance.
### Key decisions
- Use Community Edition as the legal/technical baseline.
- Remove telemetry code paths rather than rely on environment opt-out.
- Preserve internal namespaces and widget API aliases initially to avoid upstream conflict and customer embed breakage.
- Use no-op adapters/central policy for high-churn analytics callsites.
- Remove hosted push relay; require own push credentials.
- Remove cwctl event reporting and automatic signup-domain enrichment from the strict privacy baseline.
- Validate zero unexpected egress dynamically, not by grep alone.
### Source changes
Application changes are tracked in the verified checkpoints and the uncheckpointed working tree described below.
### Implementation state (updated 2026-08-16)
- SM-01 audit harness checkpoint: `8ebb320`.
- SM-02 Hub sync/registration/telemetry removal checkpoint: `832a7fd`; compatibility correction checkpoint: `1a3697f`.
- SM-03 push-relay removal checkpoint: `8b1a033`; exact post-commit independent review passed with empty security and logic blocking arrays.
- SM-04 cwctl privacy checkpoint: `3458272`; exact post-commit independent review `1a3697f..3458272` passed with empty security and logic blocking arrays.
- SM-05 community enrichment removal checkpoint: `d9bf4c4`; split exact post-commit reviews `deleg_5e2578c5` (backend) and `deleg_ee3e217f` (frontend) both passed with empty security and logic blocking arrays. Enterprise compatibility fails because proprietary code still depends on the deleted community base class.
- SM-05 boundary decision: do not modify `enterprise/` or restore a compatibility stub in the community surface. The community-only checkpoint is independently verified; the enterprise `WebsiteBrandingService` load failure remains an explicit architecture/legal blocker.
- SM-06 Product Analytics removal is verified at `ced77af`: `a96b977` removed the Amplitude/token delivery surface, and `ced77af` added the missing `GLOBAL_CONFIG_KEYS` allowlist before `window.globalConfig` serialization. Fresh pre-commit review `deleg_f9644984` and exact post-commit review `deleg_9b0a2f63` both passed with empty security and logic blocking arrays. SM-07 local-only observability remains uncheckpointed. SM-08 community Help Center GTM removal is committed as `5619cc3` and exact post-commit independent review `deleg_dd3db5b7` passed; fixed, allow-listed provider snippets remain explicit admin-configured traffic, while the GTM container path is removed because its remote container can execute arbitrary tags/scripts.
- SM-08 enterprise marketing conversion tracking was classified as fixed-endpoint, cloud-only, limited attribution traffic inside proprietary enterprise code; it remains untouched pending a separate legal/compatibility decision.
- SM-09/10 remain blocked until approved product brand inputs are supplied.
- No push or deployment has occurred.
### Current verification notes
- SM-06 exact committed range `a96b977..ced77af` changed one controller line. The root cause was raw `GlobalConfig.get(...).merge(app_config)` allowing an unrequested `CLOUD_ANALYTICS_TOKEN` from a stubbed/configured hash to reach serialized `window.globalConfig`; the remediation applies `.slice(*GLOBAL_CONFIG_KEYS)` before the explicit `app_config` merge. Raw merge/sliced-allowlist reproductions were RED/GREEN, committed Ruby syntax and `git diff --check` passed, and the added-line security scan was clean. Rails RSpec/RuboCop remain environment-blocked because the active Ruby is 2.6.10 and Bundler 2.5.16 is unavailable.
- SM-03 post-commit exact-range review `1a3697f..8b1a033`: passed; exactly five requested paths changed; 7 targeted RSpec examples, Ruby syntax, RuboCop and git diff checks passed; no secrets or Hub relay fallback remain.
- SM-04 post-commit exact-range review `1a3697f..3458272`: passed; exactly two requested deployment paths changed; Bash 3.2 syntax, normal privacy test, fail-closed grep-status checks, secret scan and diff checks passed.
- SM-05 pre-commit staged review: passed for exactly 16 community-only paths; commit hook completed ESLint/RuboCop checks.
- SM-05 post-commit split review: backend slice `deleg_5e2578c5` and frontend slice `deleg_ee3e217f` both passed with explicit empty blocking arrays; combined committed range is exactly the intended 16 paths.
- SM-05 frontend targeted run: 9 suites and 8 tests passed. Backend RSpec rerun was blocked by the local toolchain (installed Ruby 3.4.10 versus Gemfile Ruby 3.4.4; bundle environment has no usable `rspec` executable). Historical controller baseline remains 28 examples/5 failures versus HEAD baseline 27/5; the new no-enrichment regression passed.
- SM-07 targeted Rails specs: 7 examples, 0 failures; Ruby/JS lint and syntax checks pass.
- SM-08 dashboard controller specs: 7 examples, 0 failures; runtime denylist confirms no `DASHBOARD_SCRIPTS` reader or sink in community files.
- SM-08 Help Center correction: 57 portal model/controller examples, targeted GTM regression 3/3, Ruby/ERB syntax, RuboCop, ESLint/Prettier and exact staged security/scope scans pass; Vite production build passes. Full Vitest has 407 passed/15 failed across six unrelated date/time and report-snapshot tests; those failures are not in the changed Help Center path.
- Frontend privacy batch: 14 tests, 14 passed.
### SM-05 gate closure
- Exact committed range `3458272..d9bf4c4` is verified by two fresh read-only reviews: backend `deleg_5e2578c5` and frontend `deleg_ee3e217f`; both returned `passed=true`, `security_concerns=[]`, and `logic_errors=[]`.
- Community signup/onboarding no longer performs automatic website/branding enrichment or its Redis marker; enterprise compatibility remains intentionally unresolved and must not be repaired by restoring remote enrichment.
### Remaining gates
- Resolve the SM-05 enterprise/legal boundary without restoring remote enrichment.
- Complete the SM-07 local-only observability checkpoint and the remaining SM-08 dashboard/config script-injection work; keep provider comments, marketing conversion tracking, and enterprise compatibility as separate scoped decisions.
- Run full Rails/frontend suites, production-like copied-database checks, HTTP/DNS egress capture, upstream history fetch and merge rehearsal.
## 2026-08-16 — SM-06 checkpoint closure
### Root cause and fix
- The first SM-06 commit (`a96b977`) removed `CLOUD_ANALYTICS_TOKEN` from the requested config keys but left `GlobalConfig.get(*GLOBAL_CONFIG_KEYS).merge(app_config)` vulnerable to unrelated keys returned by a stub/config source being serialized through `window.globalConfig`.
- The regression was reproduced with `analytics-test-token`; the one-line remediation in `ced77af` applies `.slice(*GLOBAL_CONFIG_KEYS)` before merging the explicit application config.
### Verification
- Pre-commit independent review `deleg_f9644984`: `passed=true`, `security_concerns=[]`, `logic_errors=[]`.
- Exact post-commit review `deleg_9b0a2f63` over `a96b977..ced77af`: `passed=true`, `security_concerns=[]`, `logic_errors=[]`.
- Committed scope: one path, one line; Ruby syntax, diff check, and added-line security scan passed.
- Rails RSpec/RuboCop: blocked by local runtime mismatch (`ruby 2.6.10`, missing Bundler 2.5.16); no runtime pass is claimed.
### Next action
- Start SM-07 as a separate isolated task/checkpoint. Do not mix its unstaged files with the verified SM-06 commits.