The earlier '407 passed/15 failed' record was an artifact of running the frontend suite without the TZ=UTC the package.json test script sets; the 15 timezone-sensitive failures (availabilityHelpers, timeHelper, snoozeHelpers, ReportsDataHelper, ReportContainer) all pass under UTC and none are in changed privacy/rebrand paths. Verified: TZ=UTC full run passes 414 files / 4176 tests, 0 failed (exit 0).
15 KiB
15 KiB
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
- Confirmed clone origin, branch, commit and clean working tree.
- Mapped Rails, Sidekiq, Vue/Vite, widget, runtime configuration, manifests and deployment entry points.
- Traced scheduled installation sync to
ChatwootHub.sync_with_hub. - Confirmed from implementation and specs that
DISABLE_TELEMETRYremoves metrics but still POSTs installation configuration to Chatwoot Hub. - Traced Chatwoot-hosted push relay, remote changelog and optional onboarding registration.
- Audited Amplitude, frontend/backend Sentry, optional APM integrations, arbitrary dashboard script injection and marketing tracking configuration.
- Classified feature-required outbound integrations separately from unsolicited telemetry.
- Mapped branding configuration, views, frontend components, locale copy, assets, manifest, mail/widget surfaces and technical compatibility identifiers.
- Wrote implementation plan at
.hermes/plans/2026-08-15_092534-chatwoot-private-rebrand.md. - Incorporated independent outbound-audit findings: silent cwctl command reporting, prechecked onboarding subscription, manual super-admin Hub refresh, and unconditional signup-domain website/DNS enrichment.
- 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 review1a3697f..3458272passed with empty security and logic blocking arrays. - SM-05 community enrichment removal checkpoint:
d9bf4c4; split exact post-commit reviewsdeleg_5e2578c5(backend) anddeleg_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 enterpriseWebsiteBrandingServiceload failure remains an explicit architecture/legal blocker. - SM-06 Product Analytics removal is verified at
ced77af:a96b977removed the Amplitude/token delivery surface, andced77afadded the missingGLOBAL_CONFIG_KEYSallowlist beforewindow.globalConfigserialization. Fresh pre-commit reviewdeleg_f9644984and exact post-commit reviewdeleg_9b0a2f63both passed with empty security and logic blocking arrays. SM-07 local-only observability remains uncheckpointed. SM-08 community Help Center GTM removal is committed as5619cc3and exact post-commit independent reviewdeleg_dd3db5b7passed; 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..ced77afchanged one controller line. The root cause was rawGlobalConfig.get(...).merge(app_config)allowing an unrequestedCLOUD_ANALYTICS_TOKENfrom a stubbed/configured hash to reach serializedwindow.globalConfig; the remediation applies.slice(*GLOBAL_CONFIG_KEYS)before the explicitapp_configmerge. Raw merge/sliced-allowlist reproductions were RED/GREEN, committed Ruby syntax andgit diff --checkpassed, 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_5e2578c5and frontend slicedeleg_ee3e217fboth 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
rspecexecutable). 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_SCRIPTSreader 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 run with
TZ=UTC(the env thepackage.jsontestscript sets) passes: 414 files / 4176 tests passed, 0 failed. The earlier recorded "407 passed/15 failed" baseline was an artifact of running withoutTZ=UTC; the 15 timezone-sensitive failures all pass under UTC and none are in changed paths. - Frontend privacy batch: 14 tests, 14 passed.
SM-05 gate closure
- Exact committed range
3458272..d9bf4c4is verified by two fresh read-only reviews: backenddeleg_5e2578c5and frontenddeleg_ee3e217f; both returnedpassed=true,security_concerns=[], andlogic_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) removedCLOUD_ANALYTICS_TOKENfrom the requested config keys but leftGlobalConfig.get(*GLOBAL_CONFIG_KEYS).merge(app_config)vulnerable to unrelated keys returned by a stub/config source being serialized throughwindow.globalConfig. - The regression was reproduced with
analytics-test-token; the one-line remediation inced77afapplies.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_9b0a2f63overa96b977..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.
2026-08-16 — SM-07/SM-08 closure and residual Hub gate
Verified implementation checkpoints
- SM-07 committed at
19dc449; exact post-commit review of372f316..19dc449returned a complete pass withsecurity_concerns=[]andlogic_errors=[]. Focused frontend tests passed 6/6, ESLint passed, Ruby syntax passed, and the offline frozen-lockfile install passed. Rails RSpec/RuboCop remain blocked by Ruby 2.6.10 and missing Bundler 2.5.16. - SM-08 dashboard/script removal committed at
364e72f; exact post-commit review returned a complete pass with empty blocking arrays.DASHBOARD_SCRIPTSreader, catalog entry, and HTML sink are absent. Vite production build completed with exit code 0. Full Vitest withTZ=UTCpasses 414 files / 4176 tests, 0 failed (the "407/15" earlier baseline was a non-UTC run artifact; the 15 timezone-sensitive tests all pass under UTC).
Residual Hub worktree gate
- Remaining Community Hub URL/push/changelog code was removed in the worktree. The shared Enterprise billing callers were preserved:
ChatwootHub.billing_urlnow accepts only explicit HTTPSCHATWOOT_BILLING_URL; both shared views suppress the link when unset or invalid. - Deterministic evidence passed: changelog Vitest 1/1, ESLint, Ruby/Bash syntax, isolated billing validation harness (unset/HTTPS/http/malformed/userinfo), cwctl privacy test, privacy audit harness, default privacy audit
findings=0, and added-line security scan. The residual Vite production build also completed successfully with4,736 modules transformed,✓ built in 3m 49s, exit code 0; only Browserslist/chunk-size warnings remain. - The residual diff was intentionally not committed until a fresh independent review returned a valid approval. Fail-closed policy treats interrupted/timeout/incomplete/no-payload reviews as no verdict.
2026-08-16 — SM-07 remediation and residual Hub gate closure
SM-07 remediation (committed 2ef6fa5)
- A fresh backend review (
deleg_96a5daef,372f316..19dc449) returnedpassed=falsewith one logic blocker:FilteredCountInstrumentationdiscarded its documented local observability signals (computedduration_msand discarded it, mutated a transientThread.currentsummary then discarded it, andincrementsilently no-oped for non-aggregated signals). - Remediation (user chose in-process local sink): added a thread-safe (
Mutex) per-account in-process store with a publiclast_request_summary(account_id:)reader;record_request_summarynow stores the completed summary (status +duration_ms+ counts) instead of discarding it. Regression specs assert the completed summary is readable and an unknown account returnsnil. - Verification: Ruby syntax passed, isolated sink probe
SINK_PROBE=pass(dedup/status/counts/duration/unknown-account all asserted), and a grep for NewRelic/new_relic/record_metric/record_custom_event/record_event/Net::HTTP/RestClient/HTTPClientreturned zero matches (no egress). Rails RSpec/RuboCop remain blocked by Ruby 2.6.10 / missing Bundler 2.5.16; no runtime pass is claimed. - Independent exact committed-diff review
deleg_087e4a3fover19dc449..2ef6fa5is pending; the SM-07 gate is not fully closed until that returnspassed=truewith empty blocking arrays.
Residual Hub removal (committed 8101395)
- A fresh independent review
deleg_49d6ee2eof/tmp/chatwoot-residual-index-FINAL.txt(regenerated from the exact current index; verified byte-identical to the staged blobs with zero mismatches) returnedpassed=true,security_concerns=[],logic_errors=[]. It verified: Community Hub URL/push/sync/registration/event/changelog egress removed;changelog.jsis a local empty-feed adapter exporting the defaultChangelogApiwith no axios/fetch/network and its spec traps bothglobal.fetchandaxios.get;billing_urlis HTTPS-only with host and no userinfo and never Hub-falls-back; both ERB templates guard with.present?and escaped<%= %>;chatwoot_hub_specusessingleton_class.instance_methods(false)for edition-safebase_urlassertion without modifying the proprietary Enterprise file;privacy_audituses a narrow per-file/per-rule (hub-url) Enterprise exception while all other rules still scan it; deployment privacy guard unchanged; no secrets/injection/dummy service/unrelated scope. - Committed as
8101395(10 paths) with the approval review ID referenced in the message. The older reviewdeleg_589b22d1(alsopassed=true) covered a prior snapshot and is superseded by the fresh final-index gate. Commit is clean: only the 10 residual paths, with SM-07 already committed separately at2ef6fa5. - A post-commit exact committed-diff gate over
2ef6fa5..8101395remains to be run to fully close the committed-bytes gate (staged review saw the index; a range review sees the final committed bytes).
Release and compatibility blockers
- SM-09/10 cannot change branding defaults/assets/metadata until approved product name, domains, logos/icons, sender, legal URLs and primary color are supplied. Current visible-branding report remains 2,878 findings; it is report-only, not a privacy-rule pass.
- Enterprise
WebsiteBrandingServiceremains proprietary and currently fails withNameError: uninitialized constant WebsiteBrandingService; no dummy service or remote enrichment was added. - SM-11/12 static inventory: repository is shallow (
git rev-parse --is-shallow-repository=true); Docker, Syft, Trivy, Cosign and Gitleaks are unavailable; Dockerfile base images are mutable tags. Full-history fetch, isolated HTTP/DNS capture, production-like DB rehearsal, image digest and SBOM gates remain blocked. No network action was performed.