Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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