起因:用户反馈会临时改启动命令(比如给 start 加 --debug)。而 hoststack.yaml 声明的 start.command 优先级最高且每次部署都覆盖控制台旧值,所以每次开关调试面 都得改 YAML、提交、推送、部署——最麻烦的那条路。 改法:把双因子的「开启信号」从单一 argv 变成「argv 或 env 二者之一」: - --debug 保留,本地开发照旧用 - 新增 DEBUG_ENABLED(1/true/TRUE/True/yes/YES,精确匹配),生产走控制台环境变量 - DEBUG_TOKEN 仍然必须非空。刻意不让 token 单独生效——token 是平台上的长期 Secret,让它自己开门就等于把双因子退化成单因子 收益:生产开关调试面 = 控制台改一个 env + 重新部署,不改代码、不改 hoststack.yaml、 不重新构建。启动命令从此静态,顺带消解了"YAML 还是控制台"的争论——命令不再需要变, 放哪都无所谓。 实现:把判定抽成纯函数 debug_surface_requested(debug_arg, debug_enabled_var), 不直接读全局 env。env 是进程级全局态,测试里读它会和同二进制内其他测试竞争; 调用方传入已取好的值,测试就能覆盖真值集合、假值集合(含 "TRUE " 带空格、 "debug" 这类近似值)与"开了开关但没 token 仍然关闭"。 文档:deployment.md §2.1 双因子条件重写 + 环境变量清单;§5 表格;§5.1 启动命令 说明;reference.md §3 表与调试面描述;design.md 架构图注与 LLM_ALLOW_NET 锚点; charter.md 两条 ID 描述。hoststack.yaml 的 start 注释从"临时加 --debug"改成 "用 DEBUG_ENABLED 切"。 顺带修掉加代码引发的锚点漂移(新代码把 main.rs 后面的行号整体后移 13): design.md:366、design.md:739、reference.md:205/335/336/380 共 6 处。 验证:cargo test --bin message-weave = 65 passed / 0 failed(新增 4 个测试, 含真值集合、假值集合、无 token 仍关闭)。GATE-DOCS PASS 6/6,259 line-refs 全绿。 |
||
|---|---|---|
| .github/workflows | ||
| cloudflare-worker | ||
| docs | ||
| scripts/docs_check | ||
| src | ||
| web | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Dockerfile | ||
| README.md | ||
| README.zh-CN.md | ||
| hoststack.yaml | ||
README.md
MessageWeave
中文文档 / Chinese documentation → README.zh-CN.md
One Telegram bot per mailbox. New mail arrives as metadata; the original is always fetched read-only from your own JMAP server.
1. What it is
MessageWeave sits between a JMAP mail server (for example Stalwart) and a Telegram bot.
- Inbound. A JMAP push subscription tells the service that a message changed. Nothing is polled at rest.
- Outbound. Each event becomes a short, metadata-only Telegram notification: sender, subject, timestamp. Mail bodies are never sent to Telegram.
- On demand. When you ask, the service reads the original through a fresh JMAP read call. Nothing is cached in between.
- Optionally summarised. If you explicitly opt in, an OpenAI-compatible LLM returns a short summary. Requests, responses and attachment bytes are never persisted.
State is an architectural boundary here, not a preference.
- Redis is the only state store. No database, no embedded store, no second store.
- The process writes nothing to disk. No log files, no data files, no local volume.
- No long-lived connections. No SSE, no WebSocket, no long polling.
- Logs go to stdout only. The platform's collector owns log storage and retention.
- What it is not. It is not a mail client, not a proxy, and not a relay. It forwards metadata to one bot; it does not read arbitrary URLs, open tunnels, or broker traffic.
2. 5-minute setup
Requires a reachable Redis 7 instance and a JMAP session plus a Telegram bot token.
This is the local/dev path. The repo's Dockerfile is for local docker run only --
production does not execute it. Live deploys run from hoststack.yaml with
runtime: rust, which HostStack builds in its own rust:slim-trixie image and runs in its
own debian:trixie-slim runner, so the Dockerfile's diagnostic tools, ENTRYPOINT and
EXPOSE do not apply there. See docs/deployment.md §3 and §5.1 for the boundary.
cp .env.example .env # fill REDIS_URL and CONFIG_ENCRYPTION_KEY
docker build -t messageweave:latest .
docker run --env-file .env -p 8080:8080 messageweave:latest
Then open http://localhost:8080 in a browser and enter your CONFIG_ENCRYPTION_KEY
value. The SPA exchanges it with POST /api/admin/session for a 900-second admin session,
then writes the business secrets to Redis with PUT /api/business-config. No environment
variable is read or written again for business configuration.
For local builds without Docker: cargo build --locked produces the message-weave binary.
If the service answers requests but does nothing useful, check the logs for a startup warning: a missing required variable does not crash the process. It serves a read-only setup router over in-memory state instead. See §3.
3. Configuration entry points
Three layers. They are not interchangeable.
Boot-time environment — 2 required, 1 optional.
| Variable | Required | Default |
|---|---|---|
REDIS_URL |
yes | — |
CONFIG_ENCRYPTION_KEY |
yes | — |
PORT |
no | 8080 |
There is no RUN_MODE: the legacy environment parser was removed, and webhook plus
reconcile traffic share one router (POST /reconcile is a standalone endpoint).
.env.example ships only the two required
variables; the rest are documented defaults.
Business configuration — Redis, written by the browser.
POST /api/bootstrap is the one-shot trust bootstrap; PUT /api/business-config hot-
reloads individual settings within about one second. The process never reads business
secrets from the environment in normal operation.
Health.
GET /healthz returns 200 unconditionally — it is liveness, not readiness. GET /ready
returns 503 until configuration, Redis, and both upstreams (a JMAP session GET and
Telegram getMe, 3s each) are reachable. Probe /ready, not /healthz, when deciding
whether to route traffic — but /ready is the heavier probe (~3s worst case), so a monitor
that must stay cheap should watch the gateway's aggregated /healthz.
Remote debug (opt-in, off by default).
/debug/* is an optional remote-debug surface: live JMAP and Telegram probes, the current
business config, and a single Telegram send. It sits behind a two-factor gate — the process
must be started with --debug and DEBUG_TOKEN must be set; miss either and the routes
do not exist at all (requests fall through to a generic 404). Nothing under /debug/* is on
the gateway allowlist, so it is reachable only on the backend origin itself. The only rule for
running it is to keep it off: leave --debug out of the start command and leave DEBUG_TOKEN
unset. If you do turn it on for a one-off diagnosis, configure the chat allowlist first — with
an empty allowlist the test send is not restricted to any chat. See
docs/deployment.md §2.1.
4. Security boundary, in one sentence
All state lives in Redis; the process reads only two environment variables and writes nothing to disk.
One corollary matters for setup: the SPA admin credential — and therefore the bootstrap
trust root — is CONFIG_ENCRYPTION_KEY itself, the single 32-byte hex value you supply at
startup. It is only compared in constant time, and is never echoed, logged, or stored. The
Redis ACL password, if you have one, authenticates the Redis connection alone and is not
the credential for any HTTP endpoint.
5. Where to read more
| Document | What it answers |
|---|---|
docs/design.md |
Why the system is shaped this way: data flow, JMAP semantics, Redis streams, AI consent rules |
docs/deployment.md |
How to deploy: Dockerfile, secrets, Redis hosting, webhook/push/reconcile routing, multi-instance load balancing |
docs/reference.md |
Single source of truth for verifiable facts — Redis keys and TTLs, error codes, routes, environment layers, budgets |
docs/roadmap.md |
Gaps, blockers and the next phase |
docs/retired.md |
What was tried and dropped — abandoned routes, unreleased designs, and names that never existed |
docs/charter.md |
Project charter: goals, locked technology choices, security invariants, prohibitions, and the stable-ID registry |
AGENTS.md |
Language-agnostic engineering norms: code style, config and secrets, build environment, testing gates, document governance |
Facts that matter are traceable. If two documents disagree, docs/reference.md wins.