Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ dsh-mneme 是 DSH 宿主的记忆插件(蒸馏 / 注入 / 检索 / 巩固 / sc
| 检索辅助 | `src/search/{bm25,adaptive}.js`、`src/vector-index.js`、`src/embedding.js`、`src/local-embedder.js`、`src/reranker.js` | BM25 / 自适应阈值 / 向量索引 / 嵌入 / 重排 |
| 热度 | `src/heat.js` | 纯函数遗忘曲线(opt-in) |
| 复用统计 | `src/recall-stats.js` | recall_runs 只读聚合(#217,Top-N 召回 + 僵尸率) |
| 工作流 | `src/pipeline.js` + `.mneme/pipelines/*.json`(项目级) | M.O.(手法)读侧:两层来源(项目层优先、包内 `pipelines/` 为全局兜底)→ `mneme.pipeline/v1` 投影([docs/WORKFLOWS.md](dsh-mneme/docs/WORKFLOWS.md)) |
| 工作流 | `src/pipeline.js` + `.mneme/pipelines/*.json`(项目级) | M.O. 读侧:两层来源(项目层优先、包内 `pipelines/` 为全局兜底)→ `mneme.pipeline/v1` 投影([docs/WORKFLOWS.md](dsh-mneme/docs/WORKFLOWS.md)) |
| 绕行与候选 | `src/candidates.js`(+ `<memoryDir>/candidates/<项目键>/`) | M.O. 的出口与草稿层:绕行的分档判定(R0/R1 记账先走、R2/R3 停下问人 + 机械兜底清单 + 闸门保护)、候选的归并/归档/升格(进仓库要人批) |
| 冷启动 | `src/bootstrap.js` | 从仓库文件反向构建初始记忆(POST /bootstrap) |
| 配置 | `src/config.js`(schema + lightMode)、`src/settings.js`(feature flags 白名单) | 一切行为开关的家 |
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

| 日期 | 版本 | 头条 |
|------|------|------|
| 2026-10-17 | **v0.9.0** | **M.O.(手法)beta** — 你的操作手法不再住在记忆库的一行文字里,而是住进项目仓库的 `.mneme/pipelines/<id>.json`:能提交、能 diff、能 PR、能被 CI 读。面板画的图和 agent 读的是同一份数据。配套「候选分层」:草稿先在 `<memoryDir>/candidates/` 攒着,走过两次**且**写得出反事实才提议升格——**升格不是降低标准,是补齐标准**。 |
| 2026-10-17 | **v0.9.0** | **M.O. beta** — 你的工作流不再住在记忆库的一行文字里,而是住进项目仓库的 `.mneme/pipelines/<id>.json`:能提交、能 diff、能 PR、能被 CI 读。面板画的图和 agent 读的是同一份数据。配套「候选分层」:草稿先在 `<memoryDir>/candidates/` 攒着,走过两次**且**写得出反事实才提议升格——**升格不是降低标准,是补齐标准**。 |
| 2026-10-17 | **v0.9.0** | **设置面板重排**([#427](https://github.com/slow-stack/mneme/issues/427))— 折叠线从「冷门」挪到「新手之外」:首屏只留 3 个总开关 + 作用域组,其余整组收进「全部设置」;22 个条目名从 `bm25SearchEnabled` 改说人话,并标注「默认 / 已改」。 |
| 2026-10-17 | 加固 | **安全审计第一批**([#462](https://github.com/slow-stack/mneme/pull/462))— 外部访问 API 的 39 条路由全部改成**声明式鉴权**:漏写策略在装配期直接抛错,而不是运行时静默放行;`/export` 与 `GET /external-api` 补上鉴权门(此前未鉴权即可导出含归档/已遗忘的整库、并就地铸造 token);请求体加 4 MiB 上限。 |
| 2026-10-17 | 加固 | **路由策略重构**([#464](https://github.com/slow-stack/mneme/pull/464))— 鉴权从「每个 handler 自己记得写」变成「路由注册的必填字段」,23 处手写校验归零。供应链侧同步收紧:OSV 扫描改为有漏洞即失败、npm 发布带 `--provenance` 签名、secret scanning 与 Dependabot 打开。 |
Expand Down Expand Up @@ -243,7 +243,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790
| 配置说明(全键参考) | [dsh-mneme/docs/CONFIGURATION.md](dsh-mneme/docs/CONFIGURATION.md) |
| 蒸馏原文留存与记忆级溯源(#399) | [dsh-mneme/docs/DISTILL.md](dsh-mneme/docs/DISTILL.md) |
| 实体结构化设计 | [dsh-mneme/docs/ENTITIES.md](dsh-mneme/docs/ENTITIES.md) |
| 工作流(M.O. · 手法):本体住仓库、证据住记忆库 | [dsh-mneme/docs/WORKFLOWS.md](dsh-mneme/docs/WORKFLOWS.md) |
| 工作流(M.O.):本体住仓库、证据住记忆库 | [dsh-mneme/docs/WORKFLOWS.md](dsh-mneme/docs/WORKFLOWS.md) |
| 语义架构 | [dsh-mneme/docs/SEMANTIC.md](dsh-mneme/docs/SEMANTIC.md) |
| 本地模型部署指南 | [dsh-mneme/docs/LOCAL_MODEL.md](dsh-mneme/docs/LOCAL_MODEL.md) |
| 与 Zep / Mem0 / Letta / Hindsight 的能力对比 | [docs/COMPARISON.md](docs/COMPARISON.md) |
Expand All @@ -253,7 +253,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790
## 🗺️ 路线图

```
🧬 记忆基因 → 🛡️ 审计加固 → 💤 睡眠维护 → 🕸️ 召回融合与图谱 → ✨ 面板增强 → 🌡️ 自进化记忆 → 🔐 作用域隔离 → 🌐 生态(MCP / daemon)→ 🧭 M.O. 手法(项目级工作流)
🧬 记忆基因 → 🛡️ 审计加固 → 💤 睡眠维护 → 🕸️ 召回融合与图谱 → ✨ 面板增强 → 🌡️ 自进化记忆 → 🔐 作用域隔离 → 🌐 生态(MCP / daemon)→ 🧭 M.O.(项目级工作流)
```

| 版本 | 主题 | 状态 |
Expand All @@ -264,7 +264,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790
| **v0.6** | 会话生命周期:删对话 ≠ 删记忆 | ✅ |
| **v0.7** | 自进化记忆:热度衰减 + 睡眠双保护 + 桌面端工作台/功能开关 | ✅ |
| **v0.8** | 作用域隔离(agent/workspace 双维隔离 + 检索加权 + opt-in 硬过滤)+ 冲突队列人工裁决 + 归属显式声明 + 生态化(stdio MCP server / 图召回轴 / 冷启动 / 注入截断与状态条 / 蒸馏可靠性 / 注入形态与 agent 主动整理接口 / 配置说明与 issue 模板) | ✅ 已发布(至 v0.8.16) |
| **v0.9** | M.O.(Modus Operandi · 手法)beta:项目级工作流本体 + 候选分层与升格判据 + 面板清单/图同屏;设置面板按「新手之外」重划折叠线(#427);安全审计与供应链加固(声明式路由鉴权、OSV 阻断、发布签名);**v0.9.1 补上写侧**:执行窗口、运行账、归属候选、观察旁挂与脱节检出、步骤 `assert`/`optional`、沉淀链草稿挖掘、人格定义页(按 agent 多卡);**v0.9.2 让看不见的失败显形**:功能开关第三态「待重启」、作用域级可逆闸门(#471)、巩固 `max_tokens` 超上限自动降级(#478) | ✅ 已发布(v0.9.2) |
| **v0.9** | M.O.(Modus Operandi)beta:项目级工作流本体 + 候选分层与升格判据 + 面板清单/图同屏;设置面板按「新手之外」重划折叠线(#427);安全审计与供应链加固(声明式路由鉴权、OSV 阻断、发布签名);**v0.9.1 补上写侧**:执行窗口、运行账、归属候选、观察旁挂与脱节检出、步骤 `assert`/`optional`、沉淀链草稿挖掘、人格定义页(按 agent 多卡);**v0.9.2 让看不见的失败显形**:功能开关第三态「待重启」、作用域级可逆闸门(#471)、巩固 `max_tokens` 超上限自动降级(#478) | ✅ 已发布(v0.9.2) |

> 完整逐小版本说明见 [CHANGELOG](dsh-mneme/CHANGELOG.md)。

Expand Down
8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ The token is a lock on the **write** surface, not a wall around the whole API.

- **The real boundary is loopback + the DSH host listener.** The host-side routes (`/api/dsh-mneme/*`) are mounted on the DSH port, which binds `127.0.0.1` and is fenced by the host against DNS rebinding and cross-site browser requests. That fence does **not** establish identity: any local process, and any page running in the host's own origin, is inside the perimeter.
- **Authentication is opt-in and fail-open.** `apiToken` defaults to empty; while it is empty, `isAuthorized()` returns true for every request — reads *and* writes. Setting a token gates the write routes and the sensitive reads; it does not gate plain reads.
- **Open reads (no token even when one is configured)**: `GET /`, `/info`, `/version-check`, `/list`, `/recall-stats`, `/search`, `/semantic`, `/semantic/llm-audit`, `/semantic/llm-audit/stats`, `/semantic/graph/*`, `/entities`, `/memories/entities`, `/dream-status`, `/inject-status`, `/inject-preview`, `/pipeline`, `/candidates`, `/llm-providers`, plus the `GET`/`HEAD` branches of `/profile`, `/rules`, `/persona`, `/mode`, `/features`, `/commands`. Treat them as readable by anything local.
- **Gated routes (when a token is configured)**: every write — `/create`, `/update`, `/delete`, `/import`, `/vector-config`, `/vector-reindex`, `/runtime/provision`, `/maintenance/reclaim`, `/test-model`, `/conflicts/resolve`, and the non-`GET` branches of `/profile`, `/rules`, `/persona`, `/mode`, `/features`, `/commands` — plus four reads: `/export`, `/external-api`, `/conflicts`, `/health`.
- **Open reads (no token even when one is configured)**: `GET /`, `/info`, `/version-check`, `/list`, `/recall-stats`, `/search`, `/semantic`, `/semantic/llm-audit`, `/semantic/llm-audit/stats`, `/semantic/graph/*`, `/entities`, `/memories/entities`, `/dream-status`, `/inject-status`, `/inject-preview`, `/pipeline`, `/candidates`, `/llm-providers`, plus the `GET`/`HEAD` branches of `/profile`, `/rules`, `/persona`, `/mode`, `/features`, `/commands`, `/nudge`. Treat them as readable by anything local.
- **Gated routes (when a token is configured)**: every write — `/create`, `/update`, `/delete`, `/import`, `/vector-config`, `/vector-reindex`, `/runtime/provision`, `/maintenance/reclaim`, `/test-model`, `/conflicts/resolve`, and the non-`GET` branches of `/profile`, `/rules`, `/persona`, `/mode`, `/features`, `/commands`, `/nudge` — plus four reads: `/export`, `/external-api`, `/conflicts`, `/health`.
- **The standalone data plane** (`127.0.0.1:8790`) is the opposite shape: every route except `GET /health` requires its own Bearer token. Pointing `externalApiHost` at a non-loopback address therefore turns it into a plaintext, token-only HTTP surface — an explicit operator decision.
- **Consequence**: if you expose the plugin beyond loopback (reverse proxy, non-loopback bind), set `apiToken` *and* check that the plain reads above are acceptable to expose — they are not covered by it.
- **How this list stays true**: every route declares its own `policy` (`open` / `token` / `token-write`) at registration, and `createRouteRegistrar` throws when a route has none or reuses a path. A new route that forgets its classification **fails to boot** instead of silently opening. The two lists above are compared against the routes actually mounted by `dsh-mneme/test/security-doc.test.js` in both directions, so adding a route without updating this section fails CI rather than quietly drifting — the matrix is derived from code, not from anyone's memory.
Expand Down Expand Up @@ -534,8 +534,8 @@ Token 是**写**面上的锁,不是把整个 API 围起来的墙。

- **真正的边界是回环 + 宿主监听器。** 宿主侧路由(`/api/dsh-mneme/*`)挂在 DSH 端口上,该端口绑 `127.0.0.1`,并由宿主挡掉 DNS rebinding 与跨站浏览器请求。这道围栏**不建立身份**:任何本地进程、以及任何跑在宿主同源页面里的脚本,都在这道围栏之内。
- **鉴权是 opt-in 且 fail-open。** `apiToken` 默认为空;为空期间 `isAuthorized()` 对每个请求都返回 true——读和写都放行。配了 token 之后,它守的是写路由与敏感读,不守普通读。
- **开放读(即使配了 token 也不需要)**:`GET /`、`/info`、`/version-check`、`/list`、`/recall-stats`、`/search`、`/semantic`、`/semantic/llm-audit`、`/semantic/llm-audit/stats`、`/semantic/graph/*`、`/entities`、`/memories/entities`、`/dream-status`、`/inject-status`、`/inject-preview`、`/pipeline`、`/candidates`、`/llm-providers`,以及 `/profile`、`/rules`、`/persona`、`/mode`、`/features`、`/commands` 的 `GET`/`HEAD` 分支。把它们当作「本地任何东西都能读」。
- **受门控的路由(配了 token 之后)**:全部写操作——`/create`、`/update`、`/delete`、`/import`、`/vector-config`、`/vector-reindex`、`/runtime/provision`、`/maintenance/reclaim`、`/test-model`、`/conflicts/resolve`,以及 `/profile`、`/rules`、`/persona`、`/mode`、`/features`、`/commands` 的非 `GET` 分支——外加四条读:`/export`、`/external-api`、`/conflicts`、`/health`。
- **开放读(即使配了 token 也不需要)**:`GET /`、`/info`、`/version-check`、`/list`、`/recall-stats`、`/search`、`/semantic`、`/semantic/llm-audit`、`/semantic/llm-audit/stats`、`/semantic/graph/*`、`/entities`、`/memories/entities`、`/dream-status`、`/inject-status`、`/inject-preview`、`/pipeline`、`/candidates`、`/llm-providers`,以及 `/profile`、`/rules`、`/persona`、`/mode`、`/features`、`/commands`、`/nudge` 的 `GET`/`HEAD` 分支。把它们当作「本地任何东西都能读」。
- **受门控的路由(配了 token 之后)**:全部写操作——`/create`、`/update`、`/delete`、`/import`、`/vector-config`、`/vector-reindex`、`/runtime/provision`、`/maintenance/reclaim`、`/test-model`、`/conflicts/resolve`,以及 `/profile`、`/rules`、`/persona`、`/mode`、`/features`、`/commands`、`/nudge` 的非 `GET` 分支——外加四条读:`/export`、`/external-api`、`/conflicts`、`/health`。
- **独立数据面**(`127.0.0.1:8790`)是反过来的形状:除 `GET /health` 外每条路由都要自己的 Bearer token。因此把 `externalApiHost` 指向非回环地址,就等于把它变成一个「明文、只靠 token」的 HTTP 面——这是运营者的显式决定。
- **推论**:如果你把插件暴露到回环之外(反代、绑非回环地址),请设 `apiToken`,**并**核对上面那串开放读是否也能被看到——它们不受 token 保护。
- **这张表怎么保持为真**:每条路由在注册时自己声明 `policy`(`open` / `token` / `token-write`),`createRouteRegistrar` 在路由没声明或复用同一路径时直接抛错。新路由忘了归类会**启动失败**,而不是静默开门。上面两张清单还会被 `dsh-mneme/test/security-doc.test.js` 拿真实装配出来的路由做双向比对——加了路由却不更新这一节,CI 会红,而不是悄悄漂走。这张矩阵来自代码,不来自谁的记忆。
Expand Down
16 changes: 16 additions & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

## [Unreleased]

## 🆕 新增

- **沉淀链接线:把「会自动沉淀工作流」从一句话变成一条能跑的路(`actionNudgeEnabled`,beta,默认关)**:`src/pipeline-nudge.js` 从 v0.9.1 起就完整实现了「多帧 zstd → 重复序列 → 缺席对照 → draft 草稿」,可它在运行时**没有任何调用点**——全仓引用只出现在 `test/pipeline-nudge.test.js` 里,也就是说机器挖得出来、却永远不会被叫去挖。缺的从来不是算法,是**归属**与**触发**:`nudgeFromSessions` 要的是一个项目的会话桶目录,而桶名是路径的**有损**变形(规则不在本仓),唯一可信来源是会话头里的 `cwd`。这一版把三处接上:①`resolveProjectSessionDir` 用 `cwd` 反查桶,查不到就**整跑跳过**(`skipped:true`)而不是降级成全局普查——后者会把 A 项目的动作算到 B 头上,而结果看起来完全正常;②新增 `nudgeForProject` 把「归属 → 挖掘 → 落盘」收成一个入口,调用方没有机会漏掉归属解析;③新增 `src/pipeline-nudge-auto.js` 挂在 `turn/end` 上按项目跑,冷却 10 分钟(**观测新鲜度**参数,不是性能参数)、单次预算 4 秒、延到 `setTimeout(0)` 的宏任务里跑(解压会话日志是同步 I/O,压在回合收尾那一帧上等于让每轮都卡一下)、一次只跑一个项目、抛错只 `warn` 不冒泡(`session/event` 那条链上还挂着摘要与续期)。**它只产提议,不产草稿**:自动跑恒 `materialize:false`,落进 `<memoryDir>/proposals/<项目键>/`;升格仍然要人批,`boundaries` 里那句 `autoActivate:false` 与跳过分支共用同一个常量(`NUDGE_BOUNDARIES`)——两条出口说两句不同的话,读的人就会以为其中一条忘了做。旗标与看图(`actionPipelineEnabled`)、记账(`actionRunEnabled`)**三件互相独立**,开任何一个都不会顺带打开另一个;它进 `LIGHT_MODE_OFF`,是三个 M.O. 键里最重的一个。

## 🎨 面板

- **M.O. 面板多了「现在沉淀一次」:自动沉淀只在 `turn/end` 之后跑、还有冷却,人想立刻看结果时原本没有入口**:按钮走 `POST /api/dsh-mneme/nudge`,与自动跑**同一条** `nudgeForProject`,区别只有 `materialize` 恒关——它写提议、不改仓库。九句回执全部中英双语齐全,且刻意把三种结果说成三句不同的话:`saved`(提出 n 条)、`none`(这一遍没挖到新的重复动作)、`unreadable`(会话日志读不出来,是环境问题)。后两者在界面上长得一样时,人就只能瞎猜是「没东西」还是「坏了」。会话里读不到 `cwd` 时按钮**禁用**(服务端只会回 400,点了必然失败的按钮比没有按钮更糟);禁用态用虚线边而不是降透明度——整块降 `opacity` 会把文字一起压到 AA 以下。顺带把 `lib/client.js` 里最后三处「手法」改词,并把 `actionPipelineEnabled` 的 hint 从「沉淀和触发都还没做,建议暂时保持关闭」改写成三件能力的真实分工。

## 📚 文档

- **`docs/WORKFLOWS.md` 新增 §10「自动沉淀:工作流从哪长出来」**:§1 里「一条新工作流从哪来」此前一直是零代码,这一节补上来源——产物表(提议/草稿/定稿各住哪、谁批准)、归属为什么不能拿全机会话去挖、四种 `reason` 里 `unreadable` 必须与 `no-bucket` 分开(前者是故障、后者是关于这个项目的答案,混起来等于把故障报成空结果,端点层因此一个回 503、一个回 200)、触发控制表(`turn/end` / 10 分钟 / 4 秒 / `setTimeout(0)` / 一次一个,每行都写了为什么是这个值),以及「提议落盘为什么必须单独一个开关」:`nudgeFromSessions` 原本只要算得出项目键就**无条件**写提议,而面板每刷一次就 GET 一次——那是每刷一次往记忆库里塞一批文件。`persistProposals` 因此独立出来,预览路由显式传 `false`,这也是 `/nudge` 的 policy 是 `token-write` 而不是 `open` 的原因。`SECURITY.md` 英中两张表同步登记(`/nudge` 同时进「开放读」与「受门控」两张清单),`docs/CONFIGURATION.md` 补 `actionNudgeEnabled` 键与轻量档清单。

## 🐛 修复

- **术语漂移:整个 M.O. 的「手法」改回「工作流」**:M.O.(Modus Operandi)从 v0.9.0 起在正文里一直写作「手法」,而它与同一个功能在其余位置的叫法(`工作流`、`workflow`、`pipelines`)并不是同一个词——一个功能两个名字,读者得先猜它们是不是一回事。改法覆盖 `src/` 24 处、面板 3 处、文档 11 处,**已发版的 CHANGELOG 条目与测试里作为夹具主题词的「手法」不改**(历史不追溯,夹具词锁的是路线图插入逻辑、不是产品术语)。`git grep 手法 -- src` 现为零命中。

## [0.9.2] - 2026-10-19

## 🆕 新增
Expand Down
Loading
Loading