@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.
- package/.agents/plugins/marketplace.json +3 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +14 -0
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/README.md +4 -0
- package/_modules/knowledge-engine/src/analysis-contract.js +4 -3
- package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
- package/_modules/knowledge-engine/src/coordinator.js +8 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- package/_modules/knowledge-engine/src/memories.js +10 -5
- package/_modules/knowledge-engine/src/memory-eligibility.js +74 -0
- package/_modules/knowledge-engine/src/retrieval-ranking.js +19 -1
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +1 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +12 -9
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +2 -0
- package/_modules/knowledge-engine/src/server.js +1 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -0
- package/_site/_test/candidate-product-assets-test.js +13 -0
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/claude-executable-discovery-test.js +28 -28
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +134 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +4 -4
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +10 -10
- package/_site/_test/conversation-origin-test.js +15 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -0
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/integration-adapters-test.js +67 -67
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +109 -109
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -0
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -0
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -42
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-import-ui-test.js +104 -104
- package/_site/_test/product-progress-status-test.js +12 -1
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/registry-events-test.js +38 -0
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-scope-root-binding-test.js +67 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +6 -6
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +35 -35
- package/_site/_test/workbench-context-receipts-test.js +22 -0
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/_site/_test/zcode-capture-integration-test.js +108 -108
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +3 -3
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +89 -89
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +13 -12
- package/_site/lib/commit-reconciler.js +38 -38
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +17 -17
- package/_site/lib/conversation-store.js +2 -0
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -90
- package/_site/lib/engine-knowledge-client.js +72 -72
- package/_site/lib/folder-picker-output.js +3 -3
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/github-team-store.js +3 -3
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +15 -15
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/live-gateway.js +78 -76
- package/_site/lib/llm-client.js +12 -12
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -865
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +54 -54
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +14 -14
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/registry-events.js +16 -0
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +5 -5
- package/_site/lib/server-app.js +10 -1
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -72
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -0
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +8 -8
- package/bin/project-knowledge.js +170 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +205 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +42 -42
- package/docs/leader-glm-workbench-playbook.md +122 -119
- package/docs/project-registry-schema.md +11 -11
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/module-runtime-manifest.json +126 -104
- package/package.json +110 -108
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/change.md +30 -30
- package/templates/project-readme.md +18 -18
- package/ui/index.html +1 -1
- package/ui/knowledge-panel.html +118 -0
- package/ui/product.css +8 -0
- package/ui/product.js +175 -8
- 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:
|
|
18
|
-
Shape: [{"text":"the
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
119
|
-
const scopeId = scopeType === 'global' ? brainId :
|
|
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
|
|
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
|
-
|
|
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 || []);
|
|
@@ -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
|
-
|
|
39
|
-
`- turnId: ${turn.turnId}`,
|
|
40
|
-
` eventId: ${event.eventId}`,
|
|
41
|
-
`
|
|
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('<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('<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; });
|