@sanqianx/project-knowledge 5.0.18 → 5.0.20-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/.agents/plugins/marketplace.json +3 -3
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +349 -349
  7. package/_modules/knowledge-engine/README.md +4 -0
  8. package/_modules/knowledge-engine/src/analysis-contract.js +4 -3
  9. package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
  10. package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
  11. package/_modules/knowledge-engine/src/coordinator.js +8 -0
  12. package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
  13. package/_modules/knowledge-engine/src/memories.js +10 -5
  14. package/_modules/knowledge-engine/src/memory-eligibility.js +74 -0
  15. package/_modules/knowledge-engine/src/retrieval-ranking.js +19 -1
  16. package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +1 -0
  17. package/_modules/knowledge-engine/src/runtime/commit-prompt.js +12 -9
  18. package/_modules/knowledge-engine/src/runtime/conversation-store.js +2 -0
  19. package/_modules/knowledge-engine/src/server.js +1 -1
  20. package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
  21. package/_site/_test/ai-profile-test.js +75 -75
  22. package/_site/_test/ai-vendor-presets-test.js +91 -91
  23. package/_site/_test/automatic-context-test.js +70 -70
  24. package/_site/_test/bin-cli-test.js +230 -230
  25. package/_site/_test/bridge-adapter-test.js +95 -95
  26. package/_site/_test/bridge-consumer-service-test.js +310 -310
  27. package/_site/_test/candidate-bin-content-test.js +35 -0
  28. package/_site/_test/candidate-product-assets-test.js +13 -0
  29. package/_site/_test/candidate-release-contract-test.js +14 -14
  30. package/_site/_test/capture-hook-dedup-test.js +158 -158
  31. package/_site/_test/claude-executable-discovery-test.js +28 -28
  32. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
  33. package/_site/_test/cli-instance-management-test.js +62 -62
  34. package/_site/_test/client-capture-restart-e2e-test.js +189 -189
  35. package/_site/_test/codex-conversation-projection-test.js +60 -60
  36. package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
  37. package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
  38. package/_site/_test/codex-notify-fanout-test.js +134 -134
  39. package/_site/_test/codex-notify-upgrade-test.js +253 -253
  40. package/_site/_test/commit-boundary-freeze-test.js +147 -147
  41. package/_site/_test/commit-conversation-binding-test.js +4 -4
  42. package/_site/_test/configuration-generation-test.js +22 -22
  43. package/_site/_test/context-pack-test.js +200 -200
  44. package/_site/_test/conversation-api-test.js +10 -10
  45. package/_site/_test/conversation-origin-test.js +15 -0
  46. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
  47. package/_site/_test/embedding-config-test.js +32 -32
  48. package/_site/_test/fixtures/fake-browser-exit.js +10 -10
  49. package/_site/_test/fixtures/fake-browser-hang.js +10 -10
  50. package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
  51. package/_site/_test/fixtures/make-git-repos.js +92 -92
  52. package/_site/_test/fixtures/retrieval-gold-queries.json +32 -0
  53. package/_site/_test/fresh-hook-reimport-test.js +53 -53
  54. package/_site/_test/fresh-product-data-test.js +56 -56
  55. package/_site/_test/git-path-compatibility-test.js +80 -80
  56. package/_site/_test/git-validation-test.js +81 -81
  57. package/_site/_test/hook-worktree-test.js +42 -42
  58. package/_site/_test/index-builder-test.js +41 -41
  59. package/_site/_test/index-error-display-contract-test.js +10 -10
  60. package/_site/_test/integration-adapters-test.js +67 -67
  61. package/_site/_test/integration-capture-setup-test.js +118 -118
  62. package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
  63. package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
  64. package/_site/_test/integration-runtime-binding-test.js +473 -473
  65. package/_site/_test/integration-status-scan-test.js +29 -29
  66. package/_site/_test/kb-framework-test.js +57 -57
  67. package/_site/_test/kbpath-follow-test.js +63 -63
  68. package/_site/_test/knowledge-db-test.js +144 -144
  69. package/_site/_test/knowledge-engine-client-test.js +109 -109
  70. package/_site/_test/knowledge-maintenance-test.js +40 -40
  71. package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
  72. package/_site/_test/knowledge-scopes-test.js +37 -37
  73. package/_site/_test/knowledge-storage-location-test.js +28 -28
  74. package/_site/_test/launcher-readiness-test.js +80 -80
  75. package/_site/_test/legacy-claude-session-alias-test.js +106 -106
  76. package/_site/_test/legacy-routes-removed-test.js +47 -47
  77. package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
  78. package/_site/_test/live-gateway-test.js +48 -48
  79. package/_site/_test/local-marketplace-test.js +16 -16
  80. package/_site/_test/log-redaction-test.js +58 -58
  81. package/_site/_test/markdown-maintenance-test.js +11 -11
  82. package/_site/_test/model-context-windows-test.js +89 -89
  83. package/_site/_test/module-agent-start-proxy-test.js +65 -65
  84. package/_site/_test/module-artifact-integrity-test.js +27 -27
  85. package/_site/_test/module-boundary-test.js +30 -30
  86. package/_site/_test/module-bridge-eventbridge-test.js +202 -202
  87. package/_site/_test/module-dynamic-port-test.js +118 -118
  88. package/_site/_test/module-index-proxy-test.js +33 -0
  89. package/_site/_test/module-model-configuration-test.js +44 -44
  90. package/_site/_test/module-process-lifecycle-test.js +35 -35
  91. package/_site/_test/module-stream-proxy-test.js +34 -34
  92. package/_site/_test/module-terminal-restart-test.js +63 -0
  93. package/_site/_test/module-wiring-unbind-race-test.js +59 -59
  94. package/_site/_test/native-context-integrations-test.js +86 -86
  95. package/_site/_test/orphaned-claim-recovery-test.js +80 -80
  96. package/_site/_test/package-boundary-test.js +43 -42
  97. package/_site/_test/package-startup-test.js +56 -56
  98. package/_site/_test/packaged-ui-smoke-test.js +88 -88
  99. package/_site/_test/pr-consumer-contract-test.js +55 -55
  100. package/_site/_test/product-brand-contract-test.js +25 -25
  101. package/_site/_test/product-diagnostics-ui-test.js +93 -93
  102. package/_site/_test/product-import-ui-test.js +104 -104
  103. package/_site/_test/product-progress-status-test.js +12 -1
  104. package/_site/_test/project-remove-running-guard-test.js +79 -79
  105. package/_site/_test/project-store-test.js +90 -90
  106. package/_site/_test/registry-events-test.js +38 -0
  107. package/_site/_test/release-version-sync-test.js +115 -115
  108. package/_site/_test/requirement-binding-test.js +113 -113
  109. package/_site/_test/runtime-endpoint-test.js +64 -64
  110. package/_site/_test/scanner-test.js +83 -83
  111. package/_site/_test/server-security-test.js +96 -96
  112. package/_site/_test/sessions-stream-test.js +93 -93
  113. package/_site/_test/shared-scope-root-binding-test.js +67 -67
  114. package/_site/_test/simple-import-test.js +98 -98
  115. package/_site/_test/startup-analysis-disabled-test.js +6 -6
  116. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
  117. package/_site/_test/team-knowledge-store-test.js +132 -132
  118. package/_site/_test/test-server-cleanup-test.js +35 -35
  119. package/_site/_test/test-server-module-isolation-test.js +37 -37
  120. package/_site/_test/tracking-start-test.js +35 -35
  121. package/_site/_test/workbench-context-receipts-test.js +22 -0
  122. package/_site/_test/workspace-ui-contract-test.js +31 -31
  123. package/_site/_test/zcode-capture-integration-test.js +108 -108
  124. package/_site/lib/ai-vendor-presets.js +83 -83
  125. package/_site/lib/ai-workspace.js +3 -3
  126. package/_site/lib/atomic-file.js +204 -204
  127. package/_site/lib/bridge-adapter.js +260 -260
  128. package/_site/lib/capture-hook-config.js +108 -108
  129. package/_site/lib/claude-cli-runner.js +89 -89
  130. package/_site/lib/cli-instances.js +43 -43
  131. package/_site/lib/codex-context-status.js +17 -17
  132. package/_site/lib/codex-notify-config.js +193 -193
  133. package/_site/lib/commit-conversation-binder.js +13 -12
  134. package/_site/lib/commit-reconciler.js +38 -38
  135. package/_site/lib/context-pack-builder.js +299 -299
  136. package/_site/lib/conversation-exclusions.js +204 -204
  137. package/_site/lib/conversation-query-service.js +17 -17
  138. package/_site/lib/conversation-store.js +2 -0
  139. package/_site/lib/data-dir.js +13 -13
  140. package/_site/lib/embedding-service.js +85 -85
  141. package/_site/lib/engine-connection.js +93 -90
  142. package/_site/lib/engine-knowledge-client.js +72 -72
  143. package/_site/lib/folder-picker-output.js +3 -3
  144. package/_site/lib/git-path.js +14 -14
  145. package/_site/lib/github-team-store.js +3 -3
  146. package/_site/lib/index-builder.js +157 -157
  147. package/_site/lib/integration-status-scan.js +99 -99
  148. package/_site/lib/integration-status-worker.js +14 -14
  149. package/_site/lib/kb-framework.js +15 -15
  150. package/_site/lib/knowledge-db.js +363 -363
  151. package/_site/lib/knowledge-schema.js +121 -121
  152. package/_site/lib/live-gateway.js +78 -76
  153. package/_site/lib/llm-client.js +12 -12
  154. package/_site/lib/model-context-windows.js +82 -82
  155. package/_site/lib/module-bridge.js +874 -865
  156. package/_site/lib/module-port-resolver.js +57 -57
  157. package/_site/lib/post-commit-automation.js +54 -54
  158. package/_site/lib/product-data.js +35 -35
  159. package/_site/lib/project-lifecycle-service.js +14 -14
  160. package/_site/lib/project-registry-store.js +129 -129
  161. package/_site/lib/prompt-registry.js +148 -148
  162. package/_site/lib/registry-events.js +16 -0
  163. package/_site/lib/requirement-adapters.js +10 -10
  164. package/_site/lib/requirement-binder.js +112 -112
  165. package/_site/lib/runtime-control.js +89 -89
  166. package/_site/lib/runtime-endpoint.js +5 -5
  167. package/_site/lib/server-app.js +10 -1
  168. package/_site/lib/zcode-context-integration.js +60 -60
  169. package/_site/scripts/codex-notify-fanout.cjs +95 -95
  170. package/_site/scripts/install-module-candidates.js +69 -69
  171. package/_site/scripts/pack-module-candidates.js +89 -72
  172. package/_site/scripts/sync-product-content.js +28 -28
  173. package/_site/scripts/sync-release-version.js +164 -164
  174. package/_site/scripts/vendor-modules.js +49 -49
  175. package/_site/scripts/verify-module-runtime.js +50 -50
  176. package/_site/scripts/verify-product-runtime.js +36 -36
  177. package/_site/scripts/verify-retrieval-gold.js +83 -0
  178. package/_site/server.js +15 -15
  179. package/_site/start.bat +26 -26
  180. package/_site/stop.bat +11 -11
  181. package/bin/project-knowledge-integrations.js +254 -254
  182. package/bin/project-knowledge-kb.js +67 -67
  183. package/bin/project-knowledge-mcp.js +8 -8
  184. package/bin/project-knowledge.js +170 -170
  185. package/claude-prompts.json +21 -21
  186. package/docs/README.zh-CN.md +205 -205
  187. package/docs/assets/architecture.svg +181 -181
  188. package/docs/assets/logo.svg +49 -49
  189. package/docs/fresh-v5-start.md +42 -42
  190. package/docs/leader-glm-workbench-playbook.md +122 -119
  191. package/docs/project-registry-schema.md +11 -11
  192. package/docs/release-5.0.16-acceptance.md +176 -176
  193. package/docs/release-5.0.17-acceptance.md +117 -117
  194. package/docs/release-5.0.19-acceptance.md +39 -0
  195. package/docs/team-knowledge-mode-a-plan.md +73 -73
  196. package/module-runtime-manifest.json +126 -104
  197. package/package.json +110 -108
  198. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  199. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  200. package/plugins/project-knowledge/.mcp.json +1 -1
  201. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  202. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  203. package/templates/change.md +30 -30
  204. package/templates/project-readme.md +18 -18
  205. package/ui/index.html +1 -1
  206. package/ui/knowledge-panel.html +118 -0
  207. package/ui/product.css +8 -0
  208. package/ui/product.js +175 -8
  209. package/vendor-manifest.json +55 -53
@@ -114,6 +114,10 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
114
114
 
115
115
  ## 验证与尚未验收项
116
116
 
117
+ 默认维护模型额度或余额不足时,不会成功生成知识;提交任务仍先持久化。SDK 明确报告 billing 错误时记录 `MODEL_QUOTA_EXHAUSTED`,请求限流单独记录 `MODEL_RATE_LIMITED`,不把任意 HTTP 429 当作余额耗尽。同项目后续提交保持排队,失败与排队记录跨重启保留;恢复额度或更换默认维护模型后需显式重试前一条失败提交,不会自动消耗额度反复重试。
118
+
119
+ `/api/snapshot` 的兼容扩展字段 `maintenance` 包含执行启用状态、事件消费启用状态及按当前项目过滤的 journal 故障。建单前的消费故障不会伪造提交运行;外壳展示“提交事件消费受阻”,不能将零条运行当作没有提交。故障变化和恢复通过现有 progress 流通知,稳定故障不产生重复刷新事件。未启动服务时,Hook 已捕获的事件保留于 journal,服务消费后才生成分析任务。
120
+
117
121
  `npm test` 覆盖真实 Git/Hook、真实分项目 journal、真实 vector-hub 文件索引、晋升、重复抑制、FIFO/并发、记忆、旧格式拒绝、新版恢复及进程隔离。自动测试中的模型和 embedding 是可控替身。
118
122
 
119
123
  `npm run test:web` 使用兄弟源码仓库启动隔离的五服务网页环境,只有模型与 embedding 是替身。单一 1920×1080 视口验证双目录 UI 导入、真实 Hook 到自动刷新、原生消息流与中止、快速项目切换/草稿/历史、迟到检索及文件响应、领域检索、记忆落盘、四 iframe 身份与主题、唯一进度连接、诊断关闭和保留数据解绑;不会在用户代码仓库提交。该脚本是开发验收工具,不属于引擎生产运行依赖。
@@ -14,10 +14,11 @@ function frameFrozenEvidence(prompt) {
14
14
  }
15
15
 
16
16
  function memoryCandidateContract(entryId) {
17
- return `\nMEMORY CANDIDATE CONTRACT: When the frozen owner explicitly states a durable preference, quote it exactly in a written knowledge document and propose memoryCandidates in manifest.json. With no explicit owner preference, omit candidates; never infer preferences from code style, test fixtures or assistant claims alone.
18
- Shape: [{"text":"the summarized preference","entryId":${JSON.stringify(entryId)},"quote":"an exact excerpt from that written document","scopeType":"project"}].
17
+ return `\nMEMORY CANDIDATE CONTRACT: An automatic long-term memory may ONLY be the owner's own explicit, durable, cross-project statement: a global convention or a domain norm the owner stated in a frozen user event whose inputOrigin is "human". Assistant replies, generated knowledge documents, code, test fixtures and agent-delegation prompts are never owner statements, even when they contain preference-like wording. Project architecture, version facts, one-off fixes and temporary task discipline belong in project knowledge only: do not propose them as candidates. With no such owner statement, omit candidates entirely.
18
+ Shape: [{"text":"the owner's verbatim durable statement or a contiguous excerpt of quote; never a paraphrase","entryId":${JSON.stringify(entryId)},"quote":"the owner's exact original sentence, copied verbatim into that written document","sourceEventId":"the eventId of that frozen human user event","sourceHash":"that event's contentHash","scopeType":"domain" or "global","eligibility":{"durable":true,"crossProject":true,"reason":"why this statement is long-term rather than one-off, grounded in the owner's wording","scopeReason":"why it applies across the whole domain or all projects"}}].
19
+ The engine independently verifies every field against the frozen conversation snapshot of this claim: sourceEventId must be one of its user events with inputOrigin "human"; sourceHash must equal that event's contentHash; quote must appear verbatim both in that event's content and in the entryId document; eligibility must be fully attested with non-empty reasons; scopeType must be "domain" or "global" — automatic candidates have no project scope. A failed check only rejects the candidate, never the knowledge promotion, so attest honestly: self-reported labels cannot substitute for the frozen source, and one-time instructions, project-local conventions or delegated task orders must not be proposed.
19
20
  entryId MUST equal an operations[].path for a create/replace in THIS manifest. It is the final knowledge-relative .md document path, NOT a candidate identifier, NOT an absolute path, NOT files/<path>. For example, when the quoted text is in files/${entryId}, entryId is ${JSON.stringify(entryId)}. If another written document contains the quote, use that document's exact operations[].path instead.
20
- quote must appear verbatim in that document and have at least 8 characters; text is 1–2000 characters. Check the document and entryId before submitting the manifest. scopeType is project (default), domain or global. Recommend domain/global only when the quoted owner requirement explicitly applies across that domain/all projects; ambiguous or local requirements stay project-scoped. Do not invent scope target IDs. Candidates remain unconfirmed until the user reviews content and scope.`;
21
+ quote must appear verbatim in that document and have at least 8 characters; text is 1–2000 characters. Check the document, the entryId and the frozen event before submitting the manifest. Prefer "domain" when the owner's wording explicitly spans the current domain and "global" when it spans all projects; do not invent scope target IDs. Candidates remain unconfirmed until the user reviews content and scope.`;
21
22
  }
22
23
 
23
24
  function requireAnalysisOutput(manifestPath) {
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const failures = {
4
+ MODEL_QUOTA_EXHAUSTED: '默认维护模型的额度或余额不足。提交证据与失败记录已保留;恢复额度或更换默认模型后,请重试此提交。',
5
+ MODEL_RATE_LIMITED: '默认维护模型请求受限。提交证据与失败记录已保留,请稍后重试。',
6
+ MODEL_AUTH_FAILED: '默认维护模型认证失败。提交证据与失败记录已保留,请检查凭据后重试。',
7
+ AI_RUNTIME_REQUIRED: '后台分析运行时不可用,请修复安装后重试。',
8
+ ANALYSIS_OUTPUT_MISSING: '后台分析未生成有效文件,提交证据与失败记录已保留,请重试。',
9
+ MODEL_FAILED: '后台 Agent 分析失败。提交证据与失败记录已保留,请检查默认维护模型后重试。',
10
+ };
11
+
12
+ // Provider text can echo credentials. Classify structured codes only; neither
13
+ // raw messages nor unknown codes may cross the worker IPC boundary.
14
+ function classifyAnalysisFailure(input) {
15
+ const codes = new Set(), statuses = new Set(), seen = new Set();
16
+ function collect(value, depth = 0) {
17
+ if (typeof value === 'string') { codes.add(value.toLowerCase()); return; }
18
+ if (!value || typeof value !== 'object' || depth > 3 || seen.has(value)) return;
19
+ seen.add(value);
20
+ for (const key of ['code', 'type']) if (typeof value[key] === 'string') codes.add(value[key].toLowerCase());
21
+ if (value.status !== undefined) statuses.add(Number(value.status));
22
+ collect(value.error, depth + 1); collect(value.cause, depth + 1);
23
+ }
24
+ collect(input);
25
+ const has = (...values) => values.some(value => codes.has(value));
26
+ if (has('billing_error', 'insufficient_quota', 'quota_exceeded', 'insufficient_balance', 'model_quota_exhausted')) return 'MODEL_QUOTA_EXHAUSTED';
27
+ if (has('authentication_failed', 'authentication_error', 'model_auth_failed') || statuses.has(401)) return 'MODEL_AUTH_FAILED';
28
+ if (has('rate_limit', 'rate_limit_error', 'model_rate_limited') || statuses.has(429)) return 'MODEL_RATE_LIMITED';
29
+ if (has('module_not_found', 'ai_runtime_required')) return 'AI_RUNTIME_REQUIRED';
30
+ if (has('analysis_output_missing')) return 'ANALYSIS_OUTPUT_MISSING';
31
+ return 'MODEL_FAILED';
32
+ }
33
+
34
+ function analysisFailure(code) {
35
+ code = Object.hasOwn(failures, code) ? code : 'MODEL_FAILED';
36
+ return Object.assign(new Error(failures[code]), { code, retryable: true });
37
+ }
38
+
39
+ module.exports = { classifyAnalysisFailure, analysisFailure };
@@ -4,6 +4,7 @@ const { pathToFileURL } = require('node:url');
4
4
  const { evaluateAutomationToolUse } = require('./runtime/automation-config');
5
5
  const { RuntimeLayout } = require('./runtime/layout');
6
6
  const { ANALYSIS_SYSTEM_INSTRUCTIONS, requireAnalysisOutput } = require('./analysis-contract');
7
+ const { classifyAnalysisFailure, analysisFailure } = require('./analysis-failures');
7
8
 
8
9
  const abortController = new AbortController();
9
10
  process.once('SIGTERM', () => abortController.abort());
@@ -11,6 +12,7 @@ process.once('disconnect', () => abortController.abort());
11
12
  process.on('message', message => { if (message?.type === 'cancel') abortController.abort(); });
12
13
 
13
14
  process.once('message', async job => {
15
+ let failureCode = 'MODEL_FAILED';
14
16
  try {
15
17
  const { query } = await import(pathToFileURL(require.resolve('@anthropic-ai/claude-agent-sdk')).href);
16
18
  const layout = new RuntimeLayout(job.stagingPath);
@@ -40,15 +42,17 @@ process.once('message', async job => {
40
42
  }] }] },
41
43
  } });
42
44
  for await (const message of turn) {
45
+ if (message.type === 'assistant' && message.error) failureCode = classifyAnalysisFailure(message.error);
43
46
  if (message.type === 'result') completed = message.subtype === 'success' && !message.is_error;
44
47
  }
45
- if (!completed) throw new Error('MODEL_FAILED');
48
+ if (!completed) throw analysisFailure(failureCode);
46
49
  requireAnalysisOutput(job.manifestPath);
47
50
  process.send({ type: 'complete' }, () => process.exit(0));
48
51
  } catch (error) {
49
52
  // SDK/provider messages can echo credentials. Only controlled codes leave
50
53
  // this isolated process; raw output is never written to state or logs.
51
- const code = error.code === 'MODULE_NOT_FOUND' ? 'AI_RUNTIME_REQUIRED' : error.code === 'ANALYSIS_OUTPUT_MISSING' ? error.code : 'MODEL_FAILED';
54
+ const classified = classifyAnalysisFailure(error);
55
+ const code = classified === 'MODEL_FAILED' ? failureCode : classified;
52
56
  process.send({ type: 'failure', code }, () => process.exit(1));
53
57
  }
54
58
  });
@@ -41,6 +41,7 @@ class EngineCoordinator extends EventEmitter {
41
41
  this.sharedIndexDirty = new Set();
42
42
  this.sharedIndexQueue = Promise.resolve();
43
43
  this.memories = new MemoryService(store, {
44
+ readSnapshot: (projectId, commitSha) => this.conversations.readSnapshot(projectId, commitSha),
44
45
  getIndexStatus: scope => scope && this.memoryIndexUpdates.has(scope.id) && this.sharedIndexJobs.has(scope.id) ? 'pending' : null,
45
46
  onMaterialized: scope => { this.memoryIndexUpdates.add(scope.id); this.indexSharedScope(scope).catch(() => {}); },
46
47
  onSyncChange: (scope, code) => {
@@ -136,6 +137,7 @@ class EngineCoordinator extends EventEmitter {
136
137
  return job;
137
138
  }
138
139
  async drainJournals() {
140
+ const prior = JSON.stringify([...this.journalFailures.values()]);
139
141
  this.draining = true;
140
142
  try {
141
143
  await this.bridge.registerConsumer('knowledge-engine', { journals: ['*'] });
@@ -156,8 +158,14 @@ class EngineCoordinator extends EventEmitter {
156
158
  finally {
157
159
  this.journalError = this.journalFailures.size ? '事件消费异常,未消费证据与游标已保留,请查看诊断日志。' : null;
158
160
  this.draining = false;
161
+ if (JSON.stringify([...this.journalFailures.values()]) !== prior) this.emit('progress', this.store.addEvent({ type: 'knowledge.journal', summary: this.journalError ? '提交事件消费受阻,请查看维护状态' : '提交事件消费已恢复' }));
159
162
  }
160
163
  }
164
+ maintenanceStatus(projectId) {
165
+ const project = projectId ? this.store.getProject(projectId) : null;
166
+ const failures = [...this.journalFailures.values()].filter(failure => !projectId || failure.journalId === 'connection' || (failure.journalId === 'global' && !failure.projectId) || failure.projectId === projectId || failure.journalId === project?.journalId);
167
+ return { projectId: projectId || null, executionEnabled: this.executionEnabled && (!projectId || !!project && (!this.allowedProjects || this.allowedProjects.has(projectId))), consuming: this.consume, journal: { error: failures.length ? this.journalError : null, failures } };
168
+ }
161
169
  async recordJournalFailure(journalId, error) {
162
170
  const context = error.journalContext || {};
163
171
  const failure = { journalId, sequence: context.sequence ?? null, projectId: context.projectId || null, code: error.code || 'JOURNAL_FAILED', message: this.safeMessage(error.message) };
@@ -4,6 +4,7 @@ const path = require('node:path');
4
4
  const { frameFrozenEvidence, memoryCandidateContract } = require('./analysis-contract');
5
5
  const { finalizeAnalysisOutput } = require('./analysis-output');
6
6
  const { freezeKnowledgeTargets } = require('./knowledge-targets');
7
+ const { analysisFailure } = require('./analysis-failures');
7
8
 
8
9
  class HeadlessAnalyzer {
9
10
  get sourceReadMode() { return 'isolated-frozen-evidence'; }
@@ -42,7 +43,7 @@ class HeadlessAnalyzer {
42
43
  const timer = setTimeout(() => stop(Object.assign(new Error('知识分析超时。'), { code: 'MODEL_TIMEOUT', retryable: true })), this.timeoutMs);
43
44
  child.on('message', message => {
44
45
  if (message?.type === 'complete') stop();
45
- else if (message?.type === 'failure') stop(Object.assign(new Error('后台 Agent 分析失败,请检查模型配置。'), { code: ['AI_RUNTIME_REQUIRED', 'ANALYSIS_OUTPUT_MISSING'].includes(message.code) ? message.code : 'MODEL_FAILED', retryable: true }));
46
+ else if (message?.type === 'failure') stop(analysisFailure(message.code));
46
47
  });
47
48
  child.on('error', () => stop(Object.assign(new Error('无法启动后台分析进程。'), { code: 'WORKER_START_FAILED', retryable: true })));
48
49
  // A completion message is not an exit: retain the directory lock until
@@ -5,6 +5,7 @@ const crypto = require('node:crypto');
5
5
  const AtomicFile = require('./runtime/atomic-file');
6
6
  const { RuntimeLayout } = require('./runtime/layout');
7
7
  const { validateProjectId, DomainError } = require('./runtime/contracts');
8
+ const { MemoryEligibility } = require('./memory-eligibility');
8
9
 
9
10
  const TYPES = ['global', 'domain', 'project'];
10
11
  const normalized = text => String(text).toLowerCase().replace(/\s+/g, ' ').trim();
@@ -15,6 +16,7 @@ class MemoryService {
15
16
  this.store = store; this.layout = new RuntimeLayout(store.dataDir); this.onMaterialized = options.onMaterialized;
16
17
  this.syncErrors = new Map(); this.closed = false; this.onSyncChange = options.onSyncChange; this.writes = new Set();
17
18
  this.getIndexStatus = options.getIndexStatus;
19
+ this.eligibility = new MemoryEligibility({ readSnapshot: options.readSnapshot || null });
18
20
  }
19
21
  lock() { return this.layout.getRuntimePath('locks', 'memories.lock'); }
20
22
  scopeOf(memory) { return { type: memory.scopeType || 'global', id: memory.scopeId || memory.brainId || 'brain-default' }; }
@@ -101,6 +103,7 @@ class MemoryService {
101
103
  const project = this.store.getProject(run.projectId);
102
104
  if (!project || !run.promotedAt) return report;
103
105
  const candidates = Array.isArray(run.staging?.memoryCandidates) ? run.staging.memoryCandidates : [];
106
+ const snapshot = this.eligibility.resolve(run);
104
107
  const before = this.store.state.memories, next = [...before];
105
108
  for (const [index, raw] of candidates.slice(0, 20).entries()) {
106
109
  const reject = code => report.rejected.push({ index, code });
@@ -114,12 +117,14 @@ class MemoryService {
114
117
  if (!this.layout.isPathInside(project.knowledgePath, file, { realpath: true })) { reject('PATH_OUTSIDE_ROOT'); continue; }
115
118
  try { if (!fs.readFileSync(file, 'utf8').includes(quote)) { reject('QUOTE_MISMATCH'); continue; } }
116
119
  catch { reject('EVIDENCE_UNREADABLE'); continue; }
120
+ const provenance = this.eligibility.verify(raw, snapshot, project);
121
+ if (!provenance.ok) { reject(provenance.code); continue; }
117
122
  const brainId = project.brainId || 'brain-default';
118
- const scopeType = raw.scopeType === 'global' ? 'global' : raw.scopeType === 'domain' && project.domainId ? 'domain' : 'project';
119
- const scopeId = scopeType === 'global' ? brainId : scopeType === 'domain' ? project.domainId : project.projectId;
123
+ const scopeType = raw.scopeType;
124
+ const scopeId = scopeType === 'global' ? brainId : project.domainId;
120
125
  const key = fingerprint(brainId, scopeType, scopeId, text);
121
- if (next.some(m => m.captureFingerprint === key || fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key || !m.scopeType && m.evidence.projectId === project.projectId && normalized(m.originalText || m.text) === normalized(text))) { report.deduplicated++; continue; }
122
- next.push({ id: `memory-${crypto.randomUUID()}`, fingerprint: key, captureFingerprint: key, brainId, domainId: project.domainId || null, scopeType, scopeId, text, originalText: text, status: 'candidate', evidence: { projectId: project.projectId, commitSha: run.commitSha, runId: run.runId, entryId, quote }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
126
+ if (next.some(m => m.captureFingerprint === key || fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key || !m.scopeType && m.evidence?.projectId === project.projectId && normalized(m.originalText || m.text) === normalized(text))) { report.deduplicated++; continue; }
127
+ next.push({ id: `memory-${crypto.randomUUID()}`, fingerprint: key, captureFingerprint: key, brainId, domainId: project.domainId || null, scopeType, scopeId, text, originalText: text, status: 'candidate', sourceEventId: provenance.sourceEventId, sourceHash: provenance.sourceHash, eligibility: provenance.eligibility, evidence: { projectId: project.projectId, commitSha: run.commitSha, runId: run.runId, entryId, quote }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
123
128
  report.accepted++;
124
129
  }
125
130
  if (report.accepted) {
@@ -183,7 +188,7 @@ class MemoryService {
183
188
  // Preserve legacy global bytes until the user actually edits that record.
184
189
  const scopeText = m.scopeType ? `适用范围:${{ global: '全局', domain: '领域', project: '项目' }[scope.type]} · ${scope.id}\n\n` : '';
185
190
  const text = m.scopeType ? m.text.replace(/^\s*(#{1,6})\s/gm, '\\$1 ') : m.text;
186
- const source = m.sourceType === 'manual' ? '手动添加(用户确认)' : `${m.evidence.projectId} · ${m.evidence.commitSha} · ${m.evidence.entryId}`;
191
+ const source = m.sourceType === 'manual' ? '手动添加(用户确认)' : m.evidence ? `${m.evidence.projectId} · ${m.evidence.commitSha} · ${m.evidence.entryId}` : '历史记录(来源未记录)';
187
192
  return `## ${m.id}\n\n${scopeText}${text}\n\n来源:${source}\n`;
188
193
  }).join('\n');
189
194
  }
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ // Hard provenance gate for automatic long-term memory admission. The
4
+ // maintenance model owns the semantic judgment (its prompt contract demands
5
+ // durable, cross-project owner preferences); this gate never re-classifies
6
+ // text and never trusts model self-reports over the frozen snapshot of the
7
+ // claim being captured.
8
+ const SHARED_SCOPES = new Set(['domain', 'global']);
9
+
10
+ function indexUserEvents(snapshot) {
11
+ const events = new Map();
12
+ for (const turn of snapshot?.turns || []) {
13
+ for (const event of turn.userEvents || []) {
14
+ if (event?.eventId) events.set(String(event.eventId), event);
15
+ }
16
+ }
17
+ return events;
18
+ }
19
+
20
+ function conversationReferences(run) {
21
+ const references = new Set();
22
+ for (const operation of run?.staging?.operations || []) {
23
+ for (const reference of operation.evidenceReferences || []) {
24
+ if (typeof reference === 'string' && reference.startsWith('conversation:')) references.add(reference.slice('conversation:'.length));
25
+ }
26
+ }
27
+ return references;
28
+ }
29
+
30
+ class MemoryEligibility {
31
+ constructor(options = {}) {
32
+ this.readSnapshot = typeof options.readSnapshot === 'function' ? options.readSnapshot : null;
33
+ }
34
+
35
+ // The snapshot must exist AND be bound to this run's promoted operations
36
+ // through a `conversation:<snapshotHash>` evidence reference, so a foreign
37
+ // or later-frozen conversation can never vouch for this claim.
38
+ resolve(run) {
39
+ if (!this.readSnapshot) return null;
40
+ const references = conversationReferences(run);
41
+ if (!references.size) return null;
42
+ let snapshot = null;
43
+ try { snapshot = this.readSnapshot(run.projectId, run.commitSha); } catch { return null; }
44
+ return snapshot && references.has(snapshot.snapshotHash) ? snapshot : null;
45
+ }
46
+
47
+ verify(candidate, snapshot, project) {
48
+ if (!snapshot) return { ok: false, code: 'SNAPSHOT_UNAVAILABLE' };
49
+ const sourceEventId = String(candidate.sourceEventId || '');
50
+ if (!sourceEventId) return { ok: false, code: 'SOURCE_EVENT_MISSING' };
51
+ const event = indexUserEvents(snapshot).get(sourceEventId);
52
+ if (!event) return { ok: false, code: 'SOURCE_EVENT_NOT_FOUND' };
53
+ if ((event.inputOrigin || 'unknown') !== 'human') return { ok: false, code: 'SOURCE_NOT_HUMAN' };
54
+ if (String(candidate.sourceHash || '') !== String(event.contentHash || '')) return { ok: false, code: 'SOURCE_HASH_MISMATCH' };
55
+ if (!String(event.content || '').includes(String(candidate.quote || '').trim())) return { ok: false, code: 'QUOTE_NOT_IN_SOURCE' };
56
+ if (!String(candidate.quote || '').includes(String(candidate.text || '').trim())) return { ok: false, code: 'TEXT_NOT_IN_QUOTE' };
57
+ const eligibility = candidate.eligibility;
58
+ if (!eligibility || typeof eligibility !== 'object' || Array.isArray(eligibility)) return { ok: false, code: 'ELIGIBILITY_MISSING' };
59
+ if (eligibility.durable !== true) return { ok: false, code: 'ELIGIBILITY_NOT_DURABLE' };
60
+ if (eligibility.crossProject !== true) return { ok: false, code: 'ELIGIBILITY_NOT_CROSS_PROJECT' };
61
+ if (!String(eligibility.reason || '').trim()) return { ok: false, code: 'ELIGIBILITY_REASON_MISSING' };
62
+ if (!String(eligibility.scopeReason || '').trim()) return { ok: false, code: 'ELIGIBILITY_SCOPE_REASON_MISSING' };
63
+ if (!SHARED_SCOPES.has(candidate.scopeType)) return { ok: false, code: 'SCOPE_NOT_SHARED' };
64
+ if (candidate.scopeType === 'domain' && !project.domainId) return { ok: false, code: 'SCOPE_DOMAIN_UNBOUND' };
65
+ return {
66
+ ok: true,
67
+ sourceEventId,
68
+ sourceHash: String(event.contentHash),
69
+ eligibility: { durable: true, crossProject: true, reason: String(eligibility.reason).trim(), scopeReason: String(eligibility.scopeReason).trim() },
70
+ };
71
+ }
72
+ }
73
+
74
+ module.exports = { MemoryEligibility };
@@ -47,7 +47,8 @@ function intent(query) {
47
47
  const text = String(query || '').trim();
48
48
  if (/^(?:hello|hi|hey|你好|您好|早上好|晚上好|在吗)[\s!!。??]*$/i.test(text)) return 'greeting';
49
49
  if (/^(?:为什么|怎么回事|这是什么意思|继续|然后呢|展开说说|why|continue|tell me more)[\s!!。??]*$/i.test(text)) return 'followup';
50
- if (/(你|本轮|本次|刚才|上一条|这段).*(知识|记忆|上下文|context|memory).*(来源|哪里|什么地方|收到|读取|获取|from|received)/i.test(text)) return 'followup';
50
+ const concreteTask = /(修复|实现|新增|设计|为什么|为何|如何|怎样|避免|并发|草稿|implement|fix|design|why|how)/i.test(text);
51
+ if (!concreteTask && /(你|本轮|本次|刚才|上一条|这段).*(知识|记忆|上下文|context|memory).*(来源|哪里|什么地方|收到|读取|获取|from|received)/i.test(text)) return 'followup';
51
52
  if (/(最近|最新|上次|上一[次回]|recent|latest|last)/i.test(text) && /(进度|开发|提交|修改|更新|改动|progress|commit|develop|change|update)/i.test(text)) return 'recent';
52
53
  return 'search';
53
54
  }
@@ -80,6 +81,23 @@ function lexical(candidates, query, ignoredTerms = []) {
80
81
  }
81
82
 
82
83
  function rank(candidates, query, scoreIdentity, options = {}) {
84
+ // Named technologies select evidence, not merely a similar subject. A
85
+ // calibrated embedding can place an unrelated document nearby; it cannot
86
+ // establish that PostgreSQL/OpenTelemetry exists in the current knowledge.
87
+ // Filenames are excluded because new files legitimately seek analogies.
88
+ const topicQuery = String(query).replace(/[A-Za-z0-9_./\\-]+\.[A-Za-z0-9]{1,8}\b/g, '');
89
+ const named = [...new Set((topicQuery.match(/\b[A-Z][A-Za-z0-9]*[a-z][A-Z][A-Za-z0-9]*\b/g) || []).map(name => name.toLowerCase()))];
90
+ if (named.length) {
91
+ const documented = new Map();
92
+ for (const c of candidates) {
93
+ const key = `${c.spaceId}:${c.entryId}`;
94
+ const text = `${c.title}\n${c.chunkText}`.toLowerCase();
95
+ const found = documented.get(key) || new Set();
96
+ for (const name of named) if (text.includes(name)) found.add(name);
97
+ documented.set(key, found);
98
+ }
99
+ candidates = candidates.filter(c => named.every(name => documented.get(`${c.spaceId}:${c.entryId}`)?.has(name)));
100
+ }
83
101
  // A resolved commit already constrains provenance. Repeated SHA text in a
84
102
  // heading is not evidence for the topic asked about within that commit.
85
103
  lexical(candidates, query, options.commitPrefixes || []);
@@ -9,6 +9,7 @@ function projectEvent(event) {
9
9
  sequence: event.sequence,
10
10
  content: event.content,
11
11
  contentHash: event.contentHash,
12
+ inputOrigin: event.inputOrigin || 'unknown',
12
13
  capturedAt: event.capturedAt,
13
14
  };
14
15
  }
@@ -33,18 +33,21 @@ function formatRequirements(records) {
33
33
  ].join('\n')).join('\n');
34
34
  }
35
35
 
36
- function formatConversationUserTruth(snapshot) {
37
- if (!snapshot || !Array.isArray(snapshot.turns) || snapshot.turns.length === 0) return REQUIREMENT_UNAVAILABLE;
38
- return snapshot.turns.flatMap(turn => turn.userEvents.map(event => [
39
- `- turnId: ${turn.turnId}`,
40
- ` eventId: ${event.eventId}`,
41
- ` source/session: ${turn.source}/${turn.sessionId || '(unavailable)'}`,
36
+ function formatConversationUserTruth(snapshot) {
37
+ if (!snapshot || !Array.isArray(snapshot.turns) || snapshot.turns.length === 0) return REQUIREMENT_UNAVAILABLE;
38
+ const rendered = snapshot.turns.flatMap(turn => turn.userEvents.map(event => [
39
+ `- turnId: ${turn.turnId}`,
40
+ ` eventId: ${event.eventId}`,
41
+ ` inputOrigin: ${event.inputOrigin || 'unknown'}`,
42
+ ` contentHash: ${event.contentHash}`,
43
+ ` source/session: ${turn.source}/${turn.sessionId || '(unavailable)'}`,
42
44
  ` binding: ${turn.bindingKind}`,
43
45
  ` capturedAt: ${event.capturedAt}`,
44
46
  ' userPrompt: |',
45
- ...String(event.content || '').split(/\r?\n/).map(line => ` ${line}`),
46
- ].join('\n'))).join('\n');
47
- }
47
+ ...String(event.content || '').split(/\r?\n/).map(line => ` ${line}`),
48
+ ].join('\n'))).join('\n');
49
+ return `inputOrigin marks who typed each frozen user event: only "human" is the owner speaking; "agent-delegation" is a delegated task order from another agent; "unknown" cannot be verified. Memory candidates may rely on "human" events only.\n${rendered}`;
50
+ }
48
51
 
49
52
  function formatAssistantEvidence(snapshot) {
50
53
  if (!snapshot || !Array.isArray(snapshot.turns)) return '(no assistant conversation evidence was frozen)';
@@ -46,6 +46,7 @@ function normalizeConversationEvent(projectId, input = {}) {
46
46
  source: String(input.source || ''),
47
47
  eventType: String(input.eventType || ''),
48
48
  role,
49
+ inputOrigin: ['human', 'agent-delegation'].includes(input.inputOrigin) ? input.inputOrigin : 'unknown',
49
50
  content,
50
51
  contentHash: sha256(content),
51
52
  sessionId: optionalString(input.sessionId),
@@ -235,6 +236,7 @@ class ConversationStore {
235
236
  source: bridgeEvent.source,
236
237
  eventType: bridgeEvent.eventType,
237
238
  role: bridgeEvent.role,
239
+ inputOrigin: bridgeEvent.inputOrigin || bridgeEvent.meta?.inputOrigin || /:origin=(human|agent-delegation|unknown)$/.exec(String(bridgeEvent.eventKey || ''))?.[1],
238
240
  content: bridgeEvent.content == null ? '' : bridgeEvent.content,
239
241
  sessionId: bridgeEvent.sessionId,
240
242
  turnId: bridgeEvent.turnId,
@@ -143,7 +143,7 @@ async function api(method, pathname, search, body, store, coordinator, ctx) {
143
143
  if (method === 'GET' && pathname === '/api/events') return { events: store.listEvents(search.get('limit')).filter(e => !search.get('projectId') || e.projectId === search.get('projectId')) };
144
144
  if (method === 'POST' && pathname === '/api/events' && ctx.debug) return { event: store.addEvent(body) };
145
145
  if (method === 'POST' && pathname === '/api/bridge/notify') { if (coordinator.consume) coordinator.drain(); return { accepted: true }; }
146
- if (method === 'GET' && pathname === '/api/snapshot') return { projects: store.listProjects(), ...(search.get('summary') === '1' ? store.listRunPage({ projectId: search.get('projectId'), limit: search.get('limit') || 50, before: search.get('before') }) : { runs: store.listRuns(search.get('projectId')) }), events: store.listEvents(50), memories: coordinator.memories.list(), sharedScopes: store.state.sharedScopes };
146
+ if (method === 'GET' && pathname === '/api/snapshot') return { projects: store.listProjects(), ...(search.get('summary') === '1' ? store.listRunPage({ projectId: search.get('projectId'), limit: search.get('limit') || 50, before: search.get('before') }) : { runs: store.listRuns(search.get('projectId')) }), events: store.listEvents(50), memories: coordinator.memories.list(), sharedScopes: store.state.sharedScopes, maintenance: coordinator.maintenanceStatus(search.get('projectId')) };
147
147
  throw httpError(404, 'NOT_FOUND', '接口不存在。');
148
148
  }
149
149
 
@@ -1,67 +1,67 @@
1
- 'use strict';
2
- const assert = require('node:assert/strict');
3
- const fs = require('node:fs');
4
- const os = require('node:os');
5
- const path = require('node:path');
6
- const http = require('node:http');
7
- const { createRequire } = require('node:module');
8
- const coreRoot = path.resolve(process.env.KB_CAPTURE_TEST_CORE_ROOT || path.join(__dirname, '../..'));
9
- const fromCore = createRequire(path.join(coreRoot, 'package.json'));
10
- const { createRuntime, initializeRuntime, createRequestHandler } = fromCore('./_site/lib/server-app');
11
- const { BridgeAdapter } = fromCore('./_site/lib/bridge-adapter');
12
- const { ModuleBridge } = fromCore('./_site/lib/module-bridge');
13
- const { findChrome } = require('./helpers/find-chrome');
14
- const { launchCdpBrowser, waitFor } = require('./helpers/cdp-browser');
15
-
16
- const HOSTILE_DETAIL = '<img src=x onerror="window.__kbXssProbe=1"> INJECT-STATUS-MARKER sk-abcdef123456';
17
-
18
- function clientFixture(client, label) {
19
- const mcpCapable = client === 'claude' || client === 'codex';
20
- return {
21
- client, label, available: false, configFound: false,
22
- knowledgeIntegration: { ok: true, state: 'installed' },
23
- automaticContext: { supported: client !== 'opencode', configured: false, state: 'check-failed', detail: HOSTILE_DETAIL },
24
- developmentCapture: { ok: true, state: 'installed', installed: true, detail: HOSTILE_DETAIL },
25
- mcpStartup: mcpCapable ? { state: 'failed', checkedAt: new Date().toISOString(), scope: 'client:' + client, code: 'PROBE_TIMEOUT', expectedVersion: '9.9.9-test', detail: HOSTILE_DETAIL } : undefined,
26
- };
27
- }
28
-
29
- async function main() {
30
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-mcp-ui-'));
31
- const dataDir = path.join(temp, 'data');
32
- const homeDir = path.join(temp, 'home');
33
- const eventsHome = path.join(dataDir, 'events');
34
- const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: 'offline stub' } })); });
35
- await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
36
- const offlineUrl = 'http://127.0.0.1:' + offline.address().port;
37
- const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
38
- await initializeRuntime(runtime);
39
- const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
40
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
41
- const base = 'http://127.0.0.1:' + server.address().port;
42
- const payload = JSON.stringify({ bridge: { ok: true }, scan: { state: 'ready' }, clients: [clientFixture('claude', 'Claude Code'), clientFixture('codex', 'Codex'), clientFixture('zcode', 'ZCode'), clientFixture('opencode', 'OpenCode')] });
43
- let browser;
44
- try {
45
- browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1440, height: 900 });
46
- const run = expression => browser.evaluate(expression);
47
- const click = selector => run('document.querySelector(' + JSON.stringify(selector) + ').click()');
48
- await run('(() => { const original = window.fetch; window.__originalFetch = original; window.fetch = (input, init) => { const url = String(input && input.url || input); if (url.includes(\'/api/integrations/status\')) { return Promise.resolve({ ok: true, json: async () => (' + payload + ') }); } return original(input, init); }; })()');
49
- await click('#openSystem');
50
- await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'capture rows render');
51
- assert(await run("document.getElementById('agentCaptureList').textContent.includes('PROBE_TIMEOUT')"), 'the MCP startup failure code must be rendered for MCP-capable clients');
52
- const claudeRowHtml = await run("document.querySelector('#agentCaptureList .agent-capture-row').innerHTML");
53
- assert(claudeRowHtml.includes('&lt;img'), 'hostile mcpStartup detail must be HTML-escaped, got: ' + claudeRowHtml.slice(0, 300));
54
- assert(!claudeRowHtml.includes('<img'), 'no raw hostile markup may reach the DOM');
55
- assert.equal(await run('window.__kbXssProbe'), undefined, 'the hostile onerror must never execute');
56
- const smalls = await run("[...document.querySelectorAll('#agentCaptureList small')].map(el => el.textContent).join('|')");
57
- assert(smalls.split('|').some(text => text.startsWith('MCP')), 'the MCP diagnostic is its own line item, got: ' + smalls);
58
- assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
59
- console.log('agent-capture-mcp-ui-test PASS (MCP line rendered separately; hostile detail escaped; no XSS execution)');
60
- } finally {
61
- await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
62
- offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
63
- assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-mcp-ui-'));
64
- fs.rmSync(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
65
- }
66
- }
67
- main().catch(error => { console.error(error.stack); process.exitCode = 1; });
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const http = require('node:http');
7
+ const { createRequire } = require('node:module');
8
+ const coreRoot = path.resolve(process.env.KB_CAPTURE_TEST_CORE_ROOT || path.join(__dirname, '../..'));
9
+ const fromCore = createRequire(path.join(coreRoot, 'package.json'));
10
+ const { createRuntime, initializeRuntime, createRequestHandler } = fromCore('./_site/lib/server-app');
11
+ const { BridgeAdapter } = fromCore('./_site/lib/bridge-adapter');
12
+ const { ModuleBridge } = fromCore('./_site/lib/module-bridge');
13
+ const { findChrome } = require('./helpers/find-chrome');
14
+ const { launchCdpBrowser, waitFor } = require('./helpers/cdp-browser');
15
+
16
+ const HOSTILE_DETAIL = '<img src=x onerror="window.__kbXssProbe=1"> INJECT-STATUS-MARKER sk-abcdef123456';
17
+
18
+ function clientFixture(client, label) {
19
+ const mcpCapable = client === 'claude' || client === 'codex';
20
+ return {
21
+ client, label, available: false, configFound: false,
22
+ knowledgeIntegration: { ok: true, state: 'installed' },
23
+ automaticContext: { supported: client !== 'opencode', configured: false, state: 'check-failed', detail: HOSTILE_DETAIL },
24
+ developmentCapture: { ok: true, state: 'installed', installed: true, detail: HOSTILE_DETAIL },
25
+ mcpStartup: mcpCapable ? { state: 'failed', checkedAt: new Date().toISOString(), scope: 'client:' + client, code: 'PROBE_TIMEOUT', expectedVersion: '9.9.9-test', detail: HOSTILE_DETAIL } : undefined,
26
+ };
27
+ }
28
+
29
+ async function main() {
30
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-mcp-ui-'));
31
+ const dataDir = path.join(temp, 'data');
32
+ const homeDir = path.join(temp, 'home');
33
+ const eventsHome = path.join(dataDir, 'events');
34
+ const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: 'offline stub' } })); });
35
+ await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
36
+ const offlineUrl = 'http://127.0.0.1:' + offline.address().port;
37
+ const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
38
+ await initializeRuntime(runtime);
39
+ const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
40
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
41
+ const base = 'http://127.0.0.1:' + server.address().port;
42
+ const payload = JSON.stringify({ bridge: { ok: true }, scan: { state: 'ready' }, clients: [clientFixture('claude', 'Claude Code'), clientFixture('codex', 'Codex'), clientFixture('zcode', 'ZCode'), clientFixture('opencode', 'OpenCode')] });
43
+ let browser;
44
+ try {
45
+ browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1440, height: 900 });
46
+ const run = expression => browser.evaluate(expression);
47
+ const click = selector => run('document.querySelector(' + JSON.stringify(selector) + ').click()');
48
+ await run('(() => { const original = window.fetch; window.__originalFetch = original; window.fetch = (input, init) => { const url = String(input && input.url || input); if (url.includes(\'/api/integrations/status\')) { return Promise.resolve({ ok: true, json: async () => (' + payload + ') }); } return original(input, init); }; })()');
49
+ await click('#openSystem');
50
+ await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'capture rows render');
51
+ assert(await run("document.getElementById('agentCaptureList').textContent.includes('PROBE_TIMEOUT')"), 'the MCP startup failure code must be rendered for MCP-capable clients');
52
+ const claudeRowHtml = await run("document.querySelector('#agentCaptureList .agent-capture-row').innerHTML");
53
+ assert(claudeRowHtml.includes('&lt;img'), 'hostile mcpStartup detail must be HTML-escaped, got: ' + claudeRowHtml.slice(0, 300));
54
+ assert(!claudeRowHtml.includes('<img'), 'no raw hostile markup may reach the DOM');
55
+ assert.equal(await run('window.__kbXssProbe'), undefined, 'the hostile onerror must never execute');
56
+ const smalls = await run("[...document.querySelectorAll('#agentCaptureList small')].map(el => el.textContent).join('|')");
57
+ assert(smalls.split('|').some(text => text.startsWith('MCP')), 'the MCP diagnostic is its own line item, got: ' + smalls);
58
+ assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
59
+ console.log('agent-capture-mcp-ui-test PASS (MCP line rendered separately; hostile detail escaped; no XSS execution)');
60
+ } finally {
61
+ await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
62
+ offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
63
+ assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-mcp-ui-'));
64
+ fs.rmSync(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
65
+ }
66
+ }
67
+ main().catch(error => { console.error(error.stack); process.exitCode = 1; });