Go to file
anonsaber 978288f8d7 build+docs: 用 HostStack agent 源码实证校正部署事实,砍掉 dig 的 42.7MB 膨胀
从本机缓存的 registry.hoststack.dev/hoststack/agent 镜像里直接读出运行时
配置,纠正了此前靠猜的四条部署事实:

hoststack.yaml
- 撤回上一版把 install: 合并进 build.command 的"修正"——schema 里
  install.command 本来就是合法键(agent 的 schema 与 yaml-parser.ts 均定义),
  install/build/start 是三条独立命令
- 补上为什么这个文件不能删:runtime: rust 的框架默认 start 命令是
  ./target/release/app,与 Cargo 包名无关。删掉 YAML 后 Start Command
  回落到 app,进程在绑定端口前就以 not found 退出
- 记录实测优先级:YAML 值生效,省略才回落框架默认值(Cargo.toml 只声明
  [[bin]] message-weave,仓库里既无 app 目标也无 app.rs)

Dockerfile
- 移除 bind9-dnsutils:连带 libicudata.so.72(29.8MB)等多级依赖,实测
  边际成本 42.7MB,是其余 7 个工具总和(19.7MB)的两倍多
- DNS 解析用基础镜像自带的 getent 已覆盖(getent hosts / ahostsv4 / ahostsv6)
- 注释写明 19.7MB 的实测口径

docs/deployment.md
- §5.1 重写:build 在 rust:slim-trixie、runner 是 debian:trixie-slim,
  本仓库 Dockerfile 完全不被执行
- 修正"没有 install: 键"与"validate 拦未知键"两处错误说法:agent 部署路径
  不应用 zod 校验,未知键被静默丢弃而非报错,validate 才是唯一防线
- §4.1 容器安全档改成源码实测值:readonlyRootfs/dropCapabilities/
  noNewPrivileges 全为 true,pidsLimit 256,tmpfs 选项 rw,nosuid,nodev,
  size=64m——原先写的 noexec 并不存在;并标注 dev 档 rootfs 可写
- §8.1 明确两条部署路径的镜像差异(bookworm vs trixie)
- §6 HEALTHCHECK 与二进制"内置 health 子命令"的过期说法
- 删除文档中指向不存在文件 app.rs 的引用

验证:GATE-DOCS PASS 6/6(PATH AUDIT 369 path-refs / 0);
runtime 阶段实测构建成功且 curl/ps/pgrep/ss/ip/jq/nc/getent 齐备、
dig 按预期缺席、getent hosts 可解析、uid 999;
8 个工具 dpkg Installed-Size 合计 7935kB 而全量 du 差 62.3MB,
剔除 dig 后 19.7MB——这个差额就是本次砍掉的膨胀。
2026-09-29 12:15:52 +08:00
.github/workflows feat: initial MessageWeave release 2026-09-25 09:14:04 +08:00
cloudflare-worker docs: consume all four self-checks; make the file-size rule enforceable 2026-09-29 01:16:11 +08:00
docs build+docs: 用 HostStack agent 源码实证校正部署事实,砍掉 dig 的 42.7MB 膨胀 2026-09-29 12:15:52 +08:00
scripts/docs_check fix(docs_check): make audit_paths' brace-glob handling actually work 2026-09-29 09:35:08 +08:00
src refactor: delete three zero-implementor 'stage 0 placeholder' channel traits 2026-09-29 02:23:38 +08:00
web docs: consume all four self-checks; make the file-size rule enforceable 2026-09-29 01:16:11 +08:00
.dockerignore feat: initial MessageWeave release 2026-09-25 09:14:04 +08:00
.env.example docs+fix: delete the legacy env compat layer; sync every doc that described it 2026-09-29 02:02:44 +08:00
.gitignore docs+tooling: no document may name a path that does not exist 2026-09-29 09:28:29 +08:00
AGENTS.md docs+tooling: no document may name a path that does not exist 2026-09-29 09:28:29 +08:00
Cargo.lock fix(redis): install explicit rustls crypto provider 2026-09-25 18:02:21 +08:00
Cargo.toml fix(redis): install explicit rustls crypto provider 2026-09-25 18:02:21 +08:00
Dockerfile build+docs: 用 HostStack agent 源码实证校正部署事实,砍掉 dig 的 42.7MB 膨胀 2026-09-29 12:15:52 +08:00
README.md docs+fix: delete the legacy env compat layer; sync every doc that described it 2026-09-29 02:02:44 +08:00
README.zh-CN.md docs+fix: delete the legacy env compat layer; sync every doc that described it 2026-09-29 02:02:44 +08:00
hoststack.yaml build+docs: 用 HostStack agent 源码实证校正部署事实,砍掉 dig 的 42.7MB 膨胀 2026-09-29 12:15:52 +08:00

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.

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.