Skip to content

Commit d7df080

Browse files
authored
Merge pull request #1539 from mnfst/chore/remove-local-mode
chore: remove local mode (sql.js, MANIFEST_MODE, manifest plugin)
2 parents 473e1f8 + 9d1a78a commit d7df080

File tree

108 files changed

+925
-5189
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

108 files changed

+925
-5189
lines changed

.changeset/remove-local-mode.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"manifest-model-router": patch
3+
---
4+
5+
Remove local mode and harden the Docker deployment.
6+
7+
Manifest now runs exclusively on PostgreSQL with Better Auth. The self-contained `manifest` OpenClaw plugin (embedded Nest server, SQLite via sql.js, loopback-trust auth) is deprecated and removed from the repository — it will receive no further releases. Self-hosted users should use the Docker image (`manifestdotbuild/manifest`) with the bundled Postgres container via `docker/docker-compose.yml`, or the cloud version at app.manifest.build.
8+
9+
Docker deployments now default to `NODE_ENV=production`, with migrations controlled by a new `AUTO_MIGRATE=true` env var instead of the previous `NODE_ENV=development` workaround. Production-mode defaults activate: `trust proxy` for reverse-proxied deployments, sanitized upstream error messages, no "Dev" badge in the header, and email verification enforcement when a provider is configured. Self-hosters upgrading must set `BETTER_AUTH_SECRET` via `docker/.env` — the compose file no longer ships a placeholder secret.
10+
11+
New unified `EMAIL_*` env var scheme (`EMAIL_PROVIDER`, `EMAIL_API_KEY`, `EMAIL_DOMAIN`, `EMAIL_FROM`) covers both Better Auth transactional emails (signup verification, password reset) and threshold alert notifications. Supports Resend (recommended for self-hosting — no domain setup), Mailgun, and SendGrid. Legacy `MAILGUN_*` env vars still work for backward compatibility.
12+
13+
Breaking: `MANIFEST_MODE`, `MANIFEST_DB_PATH`, `MANIFEST_UPDATE_CHECK_OPTOUT`, `MANIFEST_TRUST_LAN` env vars are removed (no-op if set). The `manifest` npm package is deprecated. The `manifest-model-router` plugin is unaffected and remains the recommended way to route OpenClaw requests through Manifest.

.github/workflows/ci.yml

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -69,32 +69,6 @@ jobs:
6969
directory: packages/backend/coverage
7070
fail_ci_if_error: false
7171

72-
backend-sqljs:
73-
name: Backend (sql.js / ${{ matrix.os }})
74-
runs-on: ${{ matrix.os }}
75-
strategy:
76-
matrix:
77-
os: [ubuntu-latest, macos-latest]
78-
79-
env:
80-
MANIFEST_MODE: local
81-
BETTER_AUTH_SECRET: ci-test-secret-at-least-32-characters-long!!
82-
NODE_ENV: test
83-
84-
steps:
85-
- uses: actions/checkout@v4
86-
- uses: actions/setup-node@v4
87-
with:
88-
node-version: 22
89-
cache: npm
90-
- run: npm ci
91-
- run: npm run build --workspace=packages/shared
92-
- run: npm run build --workspace=packages/backend
93-
- name: Unit tests (sql.js dialect)
94-
run: npm test --workspace=packages/backend
95-
- name: E2E tests (sql.js in-memory)
96-
run: npm run test:e2e --workspace=packages/backend
97-
9872
frontend:
9973
runs-on: ubuntu-latest
10074
steps:

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ dist/
44
build/
55
*.log
66
.env*
7+
!.env.example
8+
!docker/.env.example
79
.DS_Store
810
Thumbs.db
911
*.tmp

docker/.env.example

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# ─── Required ──────────────────────────────────────────────────────────────
2+
3+
# Session signing secret. Generate with: openssl rand -hex 32
4+
BETTER_AUTH_SECRET=
5+
6+
7+
# ─── Deployment URL ────────────────────────────────────────────────────────
8+
9+
# Override when deploying to a domain (e.g., https://manifest.example.com).
10+
# Default: http://localhost:3001
11+
# BETTER_AUTH_URL=http://localhost:3001
12+
13+
14+
# ─── Database ──────────────────────────────────────────────────────────────
15+
16+
# Override the bundled Postgres password for hardening.
17+
# POSTGRES_PASSWORD=change-me
18+
19+
20+
# ─── Email provider (optional) ─────────────────────────────────────────────
21+
#
22+
# Unified config — used for BOTH login emails (signup verification, password
23+
# reset) AND threshold alert notifications. Set ONE provider block below.
24+
#
25+
# Without any email provider:
26+
# - Signup verification is waived (users created as unverified-but-usable)
27+
# - Password reset silently no-ops (use DB admin to reset)
28+
# - Threshold alerts still work if you configure per-user in the dashboard
29+
#
30+
# Per-user providers configured in the dashboard always take precedence over
31+
# these server-wide defaults, so cloud multi-tenant setups can still use
32+
# per-user config on top of a shared fallback.
33+
34+
# Option A: Resend (recommended for self-hosting — no domain setup required)
35+
# Sign up at https://resend.com, create an API key, and set:
36+
# EMAIL_PROVIDER=resend
37+
# EMAIL_API_KEY=re_your_api_key_here
38+
# EMAIL_FROM=noreply@yourdomain.com
39+
40+
# Option B: Mailgun (requires a verified sending domain)
41+
# EMAIL_PROVIDER=mailgun
42+
# EMAIL_API_KEY=key-your_api_key_here
43+
# EMAIL_DOMAIN=mg.yourdomain.com
44+
# EMAIL_FROM=noreply@mg.yourdomain.com
45+
46+
# Option C: SendGrid
47+
# EMAIL_PROVIDER=sendgrid
48+
# EMAIL_API_KEY=SG.your_api_key_here
49+
# EMAIL_FROM=noreply@yourdomain.com
50+
51+
52+
# ─── OAuth logins (optional) ───────────────────────────────────────────────
53+
#
54+
# Each provider activates automatically when both CLIENT_ID and CLIENT_SECRET
55+
# are set. Configure callback URL to: ${BETTER_AUTH_URL}/api/auth/callback/<provider>
56+
57+
# GOOGLE_CLIENT_ID=
58+
# GOOGLE_CLIENT_SECRET=
59+
# GITHUB_CLIENT_ID=
60+
# GITHUB_CLIENT_SECRET=
61+
# DISCORD_CLIENT_ID=
62+
# DISCORD_CLIENT_SECRET=

docker/Dockerfile

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ COPY packages/backend/package.json packages/backend/
2525
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --ignore-scripts && \
2626
rm -rf node_modules/typescript node_modules/@types \
2727
node_modules/ts-node node_modules/acorn \
28-
node_modules/create-require node_modules/v8-compile-cache-lib \
29-
node_modules/sql.js/dist/sql-asm* \
30-
node_modules/sql.js/dist/worker.* \
31-
node_modules/sql.js/dist/sql-wasm-browser* \
32-
node_modules/sql.js/dist/sql-wasm-debug* && \
28+
node_modules/create-require node_modules/v8-compile-cache-lib && \
3329
find . -path "*/node_modules/vitest" -type d -exec rm -rf {} + 2>/dev/null; \
3430
find . -path "*/node_modules/vite-node" -type d -exec rm -rf {} + 2>/dev/null; \
3531
find . -path "*/node_modules/rollup" -type d -exec rm -rf {} + 2>/dev/null; \

docker/docker-compose.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# with: openssl rand -hex 32
55
# - Set SEED_DATA=false to stop seeding the demo agent on every boot.
66
# - Change the seeded admin password (admin@manifest.build / manifest).
7+
# - Set NODE_ENV=production to enable trust proxy, error sanitization,
8+
# and strict Better Auth verification.
79

810
services:
911
manifest:
@@ -17,7 +19,21 @@ services:
1719
- BETTER_AUTH_URL=http://localhost:3001
1820
- SEED_DATA=true
1921
- NODE_ENV=development
20-
- MANIFEST_TRUST_LAN=true
22+
- AUTO_MIGRATE=true
23+
# Email provider (optional). Covers both login/verification emails
24+
# and threshold alert notifications. Set one provider block — see
25+
# DOCKER_README.md "Email setup". Supports resend, mailgun, sendgrid.
26+
- EMAIL_PROVIDER=${EMAIL_PROVIDER:-}
27+
- EMAIL_API_KEY=${EMAIL_API_KEY:-}
28+
- EMAIL_DOMAIN=${EMAIL_DOMAIN:-}
29+
- EMAIL_FROM=${EMAIL_FROM:-}
30+
# OAuth providers (optional). Activate when both ID and SECRET are set.
31+
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
32+
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
33+
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
34+
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
35+
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID:-}
36+
- DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET:-}
2137
depends_on:
2238
postgres:
2339
condition: service_healthy

packages/backend/.env.example

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ BETTER_AUTH_URL=http://localhost:3001
1212
# FRONTEND_PORT= # Extra trusted origin port for Better Auth
1313

1414
# ── Database ─────────────────────────────────────────
15-
# DB_POOL_MAX=20 # PostgreSQL connection pool size (default: 20)
15+
# AUTO_MIGRATE=true # Run TypeORM migrations on startup (dev/test auto-run; set true for self-hosted prod first boot)
16+
# DB_POOL_MAX=20 # PostgreSQL connection pool size (default: 20)
1617

1718
# ── Rate Limiting ────────────────────────────────────
1819
# THROTTLE_TTL=60000 # Rate limit window in ms (default: 60000)
@@ -21,11 +22,35 @@ BETTER_AUTH_URL=http://localhost:3001
2122
# ── API Key (optional) ──────────────────────────────
2223
# API_KEY= # Secret for programmatic access via X-API-Key header
2324

24-
# ── Email / Mailgun (optional) ──────────────────────
25-
# Required for email verification and password reset.
26-
# Without these, the app runs but emails are silently skipped.
27-
# MAILGUN_API_KEY= # Starts with key-
28-
# MAILGUN_DOMAIN= # e.g. mg.manifest.build
25+
# ── Email provider (optional, unified) ──────────────
26+
# Used for BOTH Better Auth transactional emails (login verification,
27+
# password reset) and threshold alert notifications when no per-user
28+
# config exists. Set ONE provider block below.
29+
#
30+
# Without any email provider: signup verification waived, password reset
31+
# no-ops. Threshold alerts still work if users configure their own provider
32+
# in the dashboard Notifications → Email Provider.
33+
34+
# Option A: Resend (recommended — no domain setup required)
35+
# EMAIL_PROVIDER=resend
36+
# EMAIL_API_KEY=re_your_api_key_here
37+
# EMAIL_FROM=noreply@yourdomain.com
38+
39+
# Option B: Mailgun
40+
# EMAIL_PROVIDER=mailgun
41+
# EMAIL_API_KEY=key-your_api_key_here
42+
# EMAIL_DOMAIN=mg.yourdomain.com
43+
# EMAIL_FROM=noreply@mg.yourdomain.com
44+
45+
# Option C: SendGrid
46+
# EMAIL_PROVIDER=sendgrid
47+
# EMAIL_API_KEY=SG.your_api_key_here
48+
# EMAIL_FROM=noreply@yourdomain.com
49+
50+
# ── Legacy Mailgun (deprecated, use EMAIL_* instead) ─
51+
# Still honored for backward compat with older deployments.
52+
# MAILGUN_API_KEY=
53+
# MAILGUN_DOMAIN=
2954
# NOTIFICATION_FROM_EMAIL=noreply@manifest.build
3055

3156
# ── OAuth Providers (all optional) ──────────────────
@@ -45,16 +70,11 @@ BETTER_AUTH_URL=http://localhost:3001
4570
# DISCORD_CLIENT_SECRET=
4671

4772
# ── Plugin ──────────────────────────────────────────
48-
# PLUGIN_OTLP_ENDPOINT= # Custom OTLP endpoint for the plugin
73+
# PLUGIN_OTLP_ENDPOINT= # Custom OTLP endpoint for the plugin
4974

5075
# ── Data Seeding ────────────────────────────────────
51-
# SEED_DATA=true # Seed demo data on startup
52-
53-
# ── Mode (local development) ────────────────────────
54-
# MANIFEST_MODE=local # 'local' (SQLite + loopback auth) or 'cloud' (default, PostgreSQL + Better Auth)
55-
# MANIFEST_DB_PATH= # SQLite file path for local mode (default: in-memory)
56-
# MANIFEST_FRONTEND_DIR= # Custom path to frontend dist directory
57-
# MANIFEST_EMBEDDED= # Set to skip auto-start (used by embedded server)
76+
# SEED_DATA=true # Seed demo data + admin user on startup (self-hosted first boot)
5877

59-
# ── Update Check ──────────────────────────────────────
60-
# MANIFEST_UPDATE_CHECK_OPTOUT=1 # Set to '1' to disable npm version checks in local mode
78+
# ── Internal ────────────────────────────────────────
79+
# MANIFEST_FRONTEND_DIR= # Custom path to frontend dist directory
80+
# MANIFEST_EMBEDDED= # Set to skip auto-start (used by embedded server)

packages/backend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"react-dom": "^19.2.4",
4444
"reflect-metadata": "^0.2.0",
4545
"rxjs": "^7.8.0",
46-
"sql.js": "^1.12.0",
4746
"typeorm": "^0.3.0",
4847
"uuid": "^11.0.0"
4948
},

packages/backend/src/analytics/controllers/agents.controller.spec.ts

Lines changed: 8 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
1-
jest.mock('../../common/constants/local-mode.constants', () => ({
2-
readLocalApiKey: jest.fn().mockReturnValue(null),
3-
LOCAL_AGENT_NAME: 'local-agent',
4-
}));
5-
61
import { Test, TestingModule } from '@nestjs/testing';
72
import { CACHE_MANAGER, CacheModule } from '@nestjs/cache-manager';
83
import { ConfigService } from '@nestjs/config';
94
import type { Cache } from 'cache-manager';
10-
import { BadRequestException, ConflictException, ForbiddenException } from '@nestjs/common';
5+
import { BadRequestException, ConflictException } from '@nestjs/common';
116
import { QueryFailedError } from 'typeorm';
127
import { AgentsController } from './agents.controller';
138
import { TimeseriesQueriesService } from '../services/timeseries-queries.service';
149
import { AgentLifecycleService } from '../services/agent-lifecycle.service';
1510
import { ApiKeyGeneratorService } from '../../otlp/services/api-key.service';
16-
import { readLocalApiKey } from '../../common/constants/local-mode.constants';
1711
import { TenantCacheService } from '../../common/services/tenant-cache.service';
1812

19-
const mockReadLocalApiKey = readLocalApiKey as jest.MockedFunction<typeof readLocalApiKey>;
20-
2113
describe('AgentsController', () => {
2214
let controller: AgentsController;
2315
let cacheManager: Cache;
@@ -29,13 +21,6 @@ describe('AgentsController', () => {
2921
let mockRenameAgent: jest.Mock;
3022
let mockTenantResolve: jest.Mock;
3123

32-
const origMode = process.env['MANIFEST_MODE'];
33-
34-
afterEach(() => {
35-
if (origMode === undefined) delete process.env['MANIFEST_MODE'];
36-
else process.env['MANIFEST_MODE'] = origMode;
37-
});
38-
3924
beforeEach(async () => {
4025
mockGetAgentList = jest.fn().mockResolvedValue([
4126
{ agent_name: 'bot-1', agent_id: 'id-1', message_count: 100 },
@@ -113,15 +98,15 @@ describe('AgentsController', () => {
11398
expect(mockGetKeyForAgent).toHaveBeenCalledWith('u1', 'bot-1');
11499
});
115100

116-
it('returns full apiKey in local mode for default agent', async () => {
117-
mockReadLocalApiKey.mockReturnValue('mnfst_full_local_key');
118-
mockConfigGet.mockImplementation((key: string, fallback?: string) =>
119-
key === 'MANIFEST_MODE' ? 'local' : (fallback ?? ''),
120-
);
101+
it('returns full apiKey when service returns fullKey', async () => {
102+
mockGetKeyForAgent.mockResolvedValueOnce({
103+
keyPrefix: 'mnfst_test1234',
104+
fullKey: 'mnfst_full_decrypted',
105+
});
121106
const user = { id: 'u1' };
122-
const result = await controller.getAgentKey(user as never, 'local-agent');
107+
const result = await controller.getAgentKey(user as never, 'bot-1');
123108

124-
expect(result).toMatchObject({ keyPrefix: 'mnfst_test1234', apiKey: 'mnfst_full_local_key' });
109+
expect(result).toMatchObject({ keyPrefix: 'mnfst_test1234', apiKey: 'mnfst_full_decrypted' });
125110
});
126111

127112
it('returns full apiKey when service returns fullKey', async () => {
@@ -144,18 +129,6 @@ describe('AgentsController', () => {
144129
expect(result).not.toHaveProperty('apiKey');
145130
});
146131

147-
it('does not return full apiKey in local mode for non-default agent', async () => {
148-
mockReadLocalApiKey.mockReturnValue('mnfst_full_local_key');
149-
mockConfigGet.mockImplementation((key: string, fallback?: string) =>
150-
key === 'MANIFEST_MODE' ? 'local' : (fallback ?? ''),
151-
);
152-
const user = { id: 'u1' };
153-
const result = await controller.getAgentKey(user as never, 'bot-1');
154-
155-
expect(result).toMatchObject({ keyPrefix: 'mnfst_test1234' });
156-
expect(result).not.toHaveProperty('apiKey');
157-
});
158-
159132
it('returns empty agents array when no agents exist', async () => {
160133
mockGetAgentList.mockResolvedValue([]);
161134

@@ -200,27 +173,6 @@ describe('AgentsController', () => {
200173
expect(cacheManager.del).toHaveBeenCalledWith('u1:/api/v1/agents');
201174
});
202175

203-
it('throws ForbiddenException when deleting default agent in local mode', async () => {
204-
mockConfigGet.mockImplementation((key: string, fallback?: string) =>
205-
key === 'MANIFEST_MODE' ? 'local' : (fallback ?? ''),
206-
);
207-
const user = { id: 'u1' };
208-
await expect(controller.deleteAgent(user as never, 'local-agent')).rejects.toThrow(
209-
ForbiddenException,
210-
);
211-
expect(mockDeleteAgent).not.toHaveBeenCalled();
212-
});
213-
214-
it('allows deleting non-default agents in local mode', async () => {
215-
mockConfigGet.mockImplementation((key: string, fallback?: string) =>
216-
key === 'MANIFEST_MODE' ? 'local' : (fallback ?? ''),
217-
);
218-
const user = { id: 'u1' };
219-
const result = await controller.deleteAgent(user as never, 'bot-1');
220-
expect(result).toEqual({ deleted: true });
221-
expect(mockDeleteAgent).toHaveBeenCalledWith('u1', 'bot-1');
222-
});
223-
224176
it('passes agent_category and agent_platform to onboardAgent', async () => {
225177
const mockOnboard = jest.fn().mockResolvedValue({
226178
tenantId: 't1',

0 commit comments

Comments
 (0)