solopup-memory 0.21.0-beta.3 → 0.22.0-beta.3
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/CHANGELOG.md +30 -0
- package/COMPATIBILITY.md +8 -5
- package/DETECTION.md +139 -0
- package/DREAMING-DESIGN.md +167 -0
- package/README.en.md +33 -6
- package/README.md +38 -7
- package/RELEASE.md +26 -0
- package/SECURITY.md +7 -0
- package/context-history.mjs +20 -3
- package/detect.mjs +441 -0
- package/eval/bench-detect.mjs +107 -0
- package/eval/semantic/cases.json +387 -0
- package/eval/semantic-suite.mjs +156 -0
- package/index.mjs +82 -10
- package/managed-policy.mjs +3 -1
- package/package.json +90 -69
- package/scripts/demo-source-recall.mjs +123 -0
- package/scripts/detect.mjs +69 -0
- package/scripts/dream.mjs +205 -0
- package/scripts/probe-history.mjs +92 -0
- package/scripts/probe-load.mjs +1 -1
- package/scripts/probe-package.mjs +6 -2
- package/scripts/security-check.mjs +173 -0
- package/scripts/soak.mjs +219 -0
- package/scripts/telemetry.mjs +187 -0
- package/scripts/verify.mjs +3 -1
- package/{test-adversarial.mjs → test/test-adversarial.mjs} +5 -5
- package/{test-belief-revision.mjs → test/test-belief-revision.mjs} +7 -7
- package/{test-candidates.mjs → test/test-candidates.mjs} +4 -4
- package/{test-capture-diagnostics.mjs → test/test-capture-diagnostics.mjs} +6 -6
- package/{test-capture-field-lineage.mjs → test/test-capture-field-lineage.mjs} +4 -4
- package/{test-capture-inputs.mjs → test/test-capture-inputs.mjs} +8 -8
- package/{test-capture-jobs.mjs → test/test-capture-jobs.mjs} +5 -5
- package/{test-capture-projections.mjs → test/test-capture-projections.mjs} +12 -12
- package/{test-capture-revision.mjs → test/test-capture-revision.mjs} +6 -6
- package/{test-capture-submission.mjs → test/test-capture-submission.mjs} +3 -3
- package/{test-capture-validation-flow.mjs → test/test-capture-validation-flow.mjs} +5 -5
- package/{test-capture-validation-job.mjs → test/test-capture-validation-job.mjs} +5 -5
- package/{test-capture-validation-lineage.mjs → test/test-capture-validation-lineage.mjs} +2 -2
- package/{test-capture-validation.mjs → test/test-capture-validation.mjs} +2 -2
- package/{test-capture.mjs → test/test-capture.mjs} +20 -20
- package/{test-compaction-compat.mjs → test/test-compaction-compat.mjs} +5 -5
- package/{test-compat.mjs → test/test-compat.mjs} +3 -3
- package/{test-concurrency.mjs → test/test-concurrency.mjs} +3 -3
- package/{test-conflict.mjs → test/test-conflict.mjs} +2 -2
- package/{test-context-history.mjs → test/test-context-history.mjs} +1 -1
- package/{test-control-schema.mjs → test/test-control-schema.mjs} +1 -1
- package/test/test-detect.mjs +125 -0
- package/test/test-dream-plan.mjs +72 -0
- package/test/test-dsh-registration.mjs +111 -0
- package/{test-episodes.mjs → test/test-episodes.mjs} +5 -5
- package/{test-explicit-memory.mjs → test/test-explicit-memory.mjs} +4 -4
- package/{test-field-lineage.mjs → test/test-field-lineage.mjs} +4 -4
- package/{test-history.mjs → test/test-history.mjs} +4 -4
- package/{test-inline-summary.mjs → test/test-inline-summary.mjs} +5 -5
- package/{test-legacy-source-identity.mjs → test/test-legacy-source-identity.mjs} +2 -2
- package/{test-links.mjs → test/test-links.mjs} +4 -4
- package/{test-managed-access.mjs → test/test-managed-access.mjs} +29 -5
- package/{test-managed-activation.mjs → test/test-managed-activation.mjs} +5 -5
- package/{test-meaning.mjs → test/test-meaning.mjs} +6 -6
- package/{test-native-service-lifecycle.mjs → test/test-native-service-lifecycle.mjs} +1 -1
- package/{test-native-write-validation.mjs → test/test-native-write-validation.mjs} +4 -4
- package/{test-packaging.mjs → test/test-packaging.mjs} +32 -5
- package/{test-paired-analysis.mjs → test/test-paired-analysis.mjs} +1 -1
- package/{test-patch.mjs → test/test-patch.mjs} +11 -11
- package/{test-plugin-contract.mjs → test/test-plugin-contract.mjs} +4 -4
- package/{test-privacy.mjs → test/test-privacy.mjs} +3 -3
- package/{test-project-stores.mjs → test/test-project-stores.mjs} +5 -5
- package/{test-publication.mjs → test/test-publication.mjs} +4 -4
- package/{test-restore.mjs → test/test-restore.mjs} +11 -11
- package/{test-retrieval.mjs → test/test-retrieval.mjs} +2 -2
- package/{test-revision-admission.mjs → test/test-revision-admission.mjs} +3 -3
- package/{test-rule-admission.mjs → test/test-rule-admission.mjs} +8 -8
- package/{test-runtime-startup.mjs → test/test-runtime-startup.mjs} +2 -2
- package/{test-schema.mjs → test/test-schema.mjs} +2 -2
- package/{test-security.mjs → test/test-security.mjs} +6 -6
- package/test/test-semantic-suite.mjs +20 -0
- package/{test-service-readiness.mjs → test/test-service-readiness.mjs} +1 -1
- package/{test-smell.mjs → test/test-smell.mjs} +4 -4
- package/{test-snapshots.mjs → test/test-snapshots.mjs} +3 -3
- package/{test-source-completeness.mjs → test/test-source-completeness.mjs} +6 -6
- package/{test-source-key.mjs → test/test-source-key.mjs} +2 -2
- package/{test-source-retirement.mjs → test/test-source-retirement.mjs} +4 -4
- package/{test-status-both.mjs → test/test-status-both.mjs} +12 -4
- package/{test-supersede.mjs → test/test-supersede.mjs} +3 -3
- package/test/test-telemetry.mjs +60 -0
- package/{test-text-admission.mjs → test/test-text-admission.mjs} +6 -6
- package/{test-tool-inputs.mjs → test/test-tool-inputs.mjs} +1 -1
- package/{test-tool-observations.mjs → test/test-tool-observations.mjs} +6 -6
- package/{test-tool-write-guard.mjs → test/test-tool-write-guard.mjs} +3 -3
- package/{test-tool-write-lifecycle.mjs → test/test-tool-write-lifecycle.mjs} +3 -3
- package/{test-usability.mjs → test/test-usability.mjs} +7 -7
- package/{test-validation-source-refs.mjs → test/test-validation-source-refs.mjs} +2 -2
- package/{test-validation-transition-context.mjs → test/test-validation-transition-context.mjs} +1 -1
- package/{test-withdrawal-archives.mjs → test/test-withdrawal-archives.mjs} +6 -6
- package/{test-withdrawal.mjs → test/test-withdrawal.mjs} +2 -2
- package/{test.mjs → test/test.mjs} +4 -4
- package/version.mjs +1 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 做梦 P0:只读的 op 计划生成器(影子报告)。
|
|
4
|
+
*
|
|
5
|
+
* 它读 L0 检测 findings 与 L3 遥测,把“该整理什么”写成可审计的 op 列表,
|
|
6
|
+
* **不写任何文件、不调用模型、不 apply**。真正的应用在 P1 之后,必须人工确认。
|
|
7
|
+
*
|
|
8
|
+
* node scripts/dream.mjs # 人类可读
|
|
9
|
+
* node scripts/dream.mjs --json # 机器可读
|
|
10
|
+
* node scripts/dream.mjs --check # 有 high 风险 op 时退出码 1
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'
|
|
16
|
+
import { join, resolve } from 'node:path'
|
|
17
|
+
|
|
18
|
+
import { detectRoot, detectStore } from '../detect.mjs'
|
|
19
|
+
import { pruneStore } from '../index.mjs'
|
|
20
|
+
import { collect as collectTelemetry } from './telemetry.mjs'
|
|
21
|
+
|
|
22
|
+
const KIND_BY_FINDING = {
|
|
23
|
+
'I3-record-missing-from-index': { kind: 'align-projection', risk: 'auto', why: '账本有效记录没有投影到索引' },
|
|
24
|
+
'I3-detail-topic-missing': { kind: 'align-projection', risk: 'auto', why: '记录正文文件缺失' },
|
|
25
|
+
'I3-detail-mismatch': { kind: 'align-projection', risk: 'auto', why: '正文与账本不一致' },
|
|
26
|
+
'I2-missing-topic-file': { kind: 'repair-reference', risk: 'auto', why: '索引指向不存在的主题文件' },
|
|
27
|
+
'I2-missing-anchor': { kind: 'repair-reference', risk: 'auto', why: '索引锚点在主题文件里找不到' },
|
|
28
|
+
'I5-withdrawal-residue': { kind: 'republish-withdrawal', risk: 'auto', why: '撤回没有传播到索引' },
|
|
29
|
+
'I6-duplicate-line': { kind: 'merge-duplicate', risk: 'human', why: '完全重复的索引行' },
|
|
30
|
+
'I6-near-duplicate': { kind: 'merge-duplicate', risk: 'human', why: '相似度 ≥0.9 的近重复' },
|
|
31
|
+
'I7-conflict': { kind: 'arbitrate-conflict', risk: 'human', why: '同一属性并存多个取值' },
|
|
32
|
+
'I8-budget-lines': { kind: 'compact-index', risk: 'auto', why: '索引行数超限' },
|
|
33
|
+
'I8-budget-bytes': { kind: 'compact-index', risk: 'auto', why: '索引字节超限' },
|
|
34
|
+
'I8-budget-store': { kind: 'compact-index', risk: 'auto', why: '库占用超限' },
|
|
35
|
+
'I10-version-mention': { kind: 'review-stale', risk: 'human', why: '记忆提到与当前版本不同的版本号' },
|
|
36
|
+
'I14-truncated-paragraph': { kind: 'repair-topic-truncation', risk: 'human', why: '主题段落像被截断' },
|
|
37
|
+
'I15-heading-without-index': { kind: 'link-heading', risk: 'auto', why: '标题没有索引指向' },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function opId(store, kind, evidence) {
|
|
41
|
+
return createHash('sha256').update(`${store}|${kind}|${evidence.join(',')}`).digest('hex').slice(0, 12)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function planDream(root) {
|
|
45
|
+
const detection = detectRoot(root)
|
|
46
|
+
const telemetry = collectTelemetry(root)
|
|
47
|
+
const ops = []
|
|
48
|
+
for (const store of detection.stores) {
|
|
49
|
+
const storeName = store.dir === root ? 'global' : store.dir
|
|
50
|
+
for (const finding of store.findings) {
|
|
51
|
+
const mapping = KIND_BY_FINDING[finding.id]
|
|
52
|
+
if (mapping === undefined) continue
|
|
53
|
+
const evidence = [`${store.scope}:${finding.id}`, ...(finding.refs ?? []).map((ref) => `${ref.file}${ref.line === undefined ? '' : ':' + ref.line}`)]
|
|
54
|
+
ops.push({
|
|
55
|
+
opId: opId(storeName, mapping.kind, evidence),
|
|
56
|
+
kind: mapping.kind,
|
|
57
|
+
risk: mapping.risk,
|
|
58
|
+
store: storeName,
|
|
59
|
+
why: mapping.why,
|
|
60
|
+
evidence,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const finding of detection.crossFindings) {
|
|
65
|
+
if (finding.id !== 'X2-duplicate-store') continue
|
|
66
|
+
ops.push({
|
|
67
|
+
opId: opId('root', 'dedupe-store', [finding.message]),
|
|
68
|
+
kind: 'dedupe-store',
|
|
69
|
+
risk: 'human',
|
|
70
|
+
store: 'root',
|
|
71
|
+
why: '内容完全相同的库(遗留副本)',
|
|
72
|
+
evidence: [finding.id, finding.message],
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
for (const warning of telemetry.warnings) {
|
|
76
|
+
if (!['T1-low-capture-success', 'T2-zero-output', 'T3-short-sessions'].includes(warning.id)) continue
|
|
77
|
+
ops.push({
|
|
78
|
+
opId: opId('root', 'inspect-capture', [warning.id]),
|
|
79
|
+
kind: 'inspect-capture',
|
|
80
|
+
risk: 'human',
|
|
81
|
+
store: 'root',
|
|
82
|
+
why: warning.message,
|
|
83
|
+
evidence: [warning.id],
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
const summary = {
|
|
87
|
+
total: ops.length,
|
|
88
|
+
auto: ops.filter((op) => op.risk === 'auto').length,
|
|
89
|
+
human: ops.filter((op) => op.risk === 'human').length,
|
|
90
|
+
byKind: Object.fromEntries([...new Set(ops.map((op) => op.kind))].sort().map((kind) => [kind, ops.filter((op) => op.kind === kind).length])),
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
kind: 'dream-plan',
|
|
94
|
+
phase: 'P0-shadow',
|
|
95
|
+
readOnly: true,
|
|
96
|
+
applySupported: false,
|
|
97
|
+
modelCalls: 0,
|
|
98
|
+
generatedAt: new Date().toISOString(),
|
|
99
|
+
root,
|
|
100
|
+
summary,
|
|
101
|
+
ops,
|
|
102
|
+
detectionSummary: detection.summary,
|
|
103
|
+
telemetrySummary: { capture: telemetry.capture, output: telemetry.output, warnings: telemetry.warnings.length },
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** P1 只自动执行确定性与可回滚的整理;其余 op 明确留给人工。 */
|
|
108
|
+
const AUTO_KINDS = new Set(['compact-index'])
|
|
109
|
+
|
|
110
|
+
export function planHash(plan) {
|
|
111
|
+
return createHash('sha256')
|
|
112
|
+
.update(JSON.stringify({ root: plan.root, ops: plan.ops.map((op) => ({ opId: op.opId, kind: op.kind, store: op.store, evidence: op.evidence })) }))
|
|
113
|
+
.digest('hex')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function storeDir(root, store) {
|
|
117
|
+
return store === 'global' ? root : store
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function backupStore(root, dir, stamp) {
|
|
121
|
+
const target = join(root, '.dream', 'backups', stamp, dir === root ? 'global' : dir.slice(dir.lastIndexOf('/') + 1))
|
|
122
|
+
mkdirSync(target, { recursive: true })
|
|
123
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
124
|
+
if (!entry.isFile() || entry.name.startsWith('.')) continue
|
|
125
|
+
copyFileSync(join(dir, entry.name), join(target, entry.name))
|
|
126
|
+
}
|
|
127
|
+
return target
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function applyDream(plan, reviewed) {
|
|
131
|
+
if (reviewed !== planHash(plan)) throw new Error('dream-review-hash-mismatch')
|
|
132
|
+
const applyOps = plan.ops.filter((op) => AUTO_KINDS.has(op.kind))
|
|
133
|
+
const skipped = plan.ops.filter((op) => !AUTO_KINDS.has(op.kind)).map((op) => ({ opId: op.opId, kind: op.kind, reason: 'needs-human' }))
|
|
134
|
+
if (applyOps.length === 0) return { applied: 0, skipped, results: [], backup: null }
|
|
135
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
136
|
+
const dirs = [...new Set(applyOps.map((op) => storeDir(plan.root, op.store)))]
|
|
137
|
+
const before = dirs.map((dir) => detectStore(dir, { scope: dir === plan.root ? 'global' : 'project' }))
|
|
138
|
+
const backup = {}
|
|
139
|
+
for (const dir of dirs) backup[dir] = backupStore(plan.root, dir, stamp)
|
|
140
|
+
const results = []
|
|
141
|
+
for (const dir of dirs) {
|
|
142
|
+
const result = pruneStore(dir, { apply: true })
|
|
143
|
+
results.push({ dir, before: result.before, after: result.after, removed: result.removed, archived: result.archived })
|
|
144
|
+
}
|
|
145
|
+
const verification = dirs.map((dir, index) => {
|
|
146
|
+
const after = detectStore(dir, { scope: dir === plan.root ? 'global' : 'project' })
|
|
147
|
+
return { dir, errorsBefore: before[index].summary.errors, errorsAfter: after.summary.errors, ok: after.summary.errors <= before[index].summary.errors }
|
|
148
|
+
})
|
|
149
|
+
const applied = results.reduce((sum, row) => sum + (row.removed.expired + row.removed.duplicate + row.removed.overCap > 0 ? 1 : 0), 0)
|
|
150
|
+
return { applied: results.length, skipped, results, backup, verification }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`) {
|
|
154
|
+
const argv = process.argv.slice(2)
|
|
155
|
+
const option = (name) => {
|
|
156
|
+
const at = argv.indexOf(name)
|
|
157
|
+
return at >= 0 ? argv[at + 1] : undefined
|
|
158
|
+
}
|
|
159
|
+
const root = resolve(option('--root') ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'memory'))
|
|
160
|
+
if (!existsSync(root)) {
|
|
161
|
+
console.error(`[dream] 记忆根不存在:${root}`)
|
|
162
|
+
process.exit(2)
|
|
163
|
+
}
|
|
164
|
+
const plan = planDream(root)
|
|
165
|
+
if (argv.includes('--apply')) {
|
|
166
|
+
const reviewed = option('--reviewed')
|
|
167
|
+
if (reviewed === undefined) {
|
|
168
|
+
console.error('[dream] --apply 需要 --reviewed <planHash>;先看 `--json` 输出的完整计划')
|
|
169
|
+
process.exit(2)
|
|
170
|
+
}
|
|
171
|
+
let result
|
|
172
|
+
try {
|
|
173
|
+
result = applyDream(plan, reviewed)
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(`[dream] 应用失败:${error.message}`)
|
|
176
|
+
process.exit(2)
|
|
177
|
+
}
|
|
178
|
+
if (argv.includes('--json')) process.stdout.write(JSON.stringify({ planHash: planHash(plan), ...result }, null, 2) + '\n')
|
|
179
|
+
else {
|
|
180
|
+
const lines = [
|
|
181
|
+
`做梦 P1 应用 · ${new Date().toISOString()}`,
|
|
182
|
+
`planHash:${planHash(plan)}`,
|
|
183
|
+
`执行 ${result.applied} 个自动 op;跳过 ${result.skipped.length} 个(需人工)。`,
|
|
184
|
+
]
|
|
185
|
+
for (const row of result.results) lines.push(` ${row.dir}:${row.before.lines} → ${row.after.lines} 行,remove expired ${row.removed.expired} / duplicate ${row.removed.duplicate} / overCap ${row.removed.overCap}`)
|
|
186
|
+
for (const row of result.verification ?? []) lines.push(` 验证 ${row.dir}:error ${row.errorsBefore} → ${row.errorsAfter} ${row.ok ? 'OK' : '恶化,请用备份回滚'}`)
|
|
187
|
+
if (result.backup !== null) lines.push(` 备份:${Object.values(result.backup).join('、')}`)
|
|
188
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
189
|
+
}
|
|
190
|
+
process.exit((result.verification ?? []).every((row) => row.ok) ? 0 : 1)
|
|
191
|
+
}
|
|
192
|
+
if (argv.includes('--json')) process.stdout.write(JSON.stringify(plan, null, 2) + '\n')
|
|
193
|
+
else {
|
|
194
|
+
const lines = [
|
|
195
|
+
`做梦 P0(影子报告,只读)· ${plan.generatedAt}`,
|
|
196
|
+
`根目录:${plan.root}`,
|
|
197
|
+
`op 总数 ${plan.summary.total}(自动可执行候选 ${plan.summary.auto}、需人工确认 ${plan.summary.human})`,
|
|
198
|
+
`分类:${Object.entries(plan.summary.byKind).map(([kind, count]) => `${kind} ${count}`).join('、') || '无'}`,
|
|
199
|
+
'边界:不写文件、不调用模型;apply 未实现。',
|
|
200
|
+
]
|
|
201
|
+
for (const op of plan.ops) lines.push(` [${op.risk}] ${op.kind} · ${op.why}${op.evidence.length ? `(${op.evidence.slice(0, 3).join(';')})` : ''}`)
|
|
202
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
203
|
+
}
|
|
204
|
+
if (argv.includes('--check') && plan.summary.human > 0) process.exit(1)
|
|
205
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Real DSH CLI/Loader, Session and Projection API check. Synthetic input, no model calls. */
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, realpathSync } from 'node:fs'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
|
+
import { probeEnvironment } from './probe-env.mjs'
|
|
9
|
+
|
|
10
|
+
const CLI = process.env.DSH_CLI ?? '/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh'
|
|
11
|
+
if (!existsSync(CLI)) { console.error('[FAIL] DSH_CLI must identify an installed official host'); process.exit(2) }
|
|
12
|
+
const source = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
13
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-history-probe-')))
|
|
14
|
+
const env = probeEnvironment(root), profile = join(env.HOME, 'profiles', 'probe')
|
|
15
|
+
mkdirSync(profile, { recursive: true }); mkdirSync(env.TMPDIR)
|
|
16
|
+
const reportFile = join(root, 'result.json'), driver = join(root, 'driver.mjs')
|
|
17
|
+
writeFileSync(driver, `
|
|
18
|
+
import assert from 'node:assert/strict'
|
|
19
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
20
|
+
import * as memory from ${JSON.stringify(pathToFileURL(join(source, 'index.mjs')).href)}
|
|
21
|
+
import { memoryContextProjection, reconcileMemoryContext } from ${JSON.stringify(pathToFileURL(join(source, 'context-history.mjs')).href)}
|
|
22
|
+
export const name = 'solopup-history-probe'
|
|
23
|
+
export const inject = ['sessions', 'sessionProjections', 'systemPrompt', 'workingDirectory', 'tools']
|
|
24
|
+
export async function apply(ctx) {
|
|
25
|
+
try {
|
|
26
|
+
const owner = await ctx.plugin(memory, { autoCapture: { enabled: false } })
|
|
27
|
+
if (typeof owner?.await === 'function') await owner.await()
|
|
28
|
+
const stamp = JSON.parse(readFileSync(${JSON.stringify(join(env.HOME, 'memory', '.loaded.json'))}, 'utf8'))
|
|
29
|
+
assert.equal(stamp.memoryHistory.available, true)
|
|
30
|
+
assert.equal(stamp.tools.length, 6)
|
|
31
|
+
const key = 'solopup:synthetic-projection-probe'
|
|
32
|
+
const definition = { ...memoryContextProjection(), key }
|
|
33
|
+
const register = async () => {
|
|
34
|
+
const value = await ctx.sessionProjections.register(definition)
|
|
35
|
+
return typeof value?.stateOf === 'function' ? { ...value, retained: true } : { stateOf: session => ctx.sessionProjections.stateOf(session, key), dispose: value, retained: false }
|
|
36
|
+
}
|
|
37
|
+
const handle = await register()
|
|
38
|
+
const session = ctx.sessions.create('solopup-synthetic-history')
|
|
39
|
+
const ordinary = { id: 'ordinary', role: 'user', source: { kind: 'user' }, content: [{ type: 'text', text: 'ordinary synthetic quotation remains' }] }
|
|
40
|
+
session.append('user/message', ordinary, { surfaceOp: 'append' })
|
|
41
|
+
const sections = [{ name: 'memory:index', text: 'obsolete synthetic memory' }, { name: 'other:context', text: 'keep other provider' }]
|
|
42
|
+
const old = { id: 'old-context', role: 'user', source: { kind: 'runtime-context', form: 'snapshot', sections }, content: [{ type: 'text', text: 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\\n\\n' + sections.map(s => s.text).join('\\n\\n') }] }
|
|
43
|
+
const event = session.append('user/message', old, { surfaceOp: 'append' })
|
|
44
|
+
assert.deepEqual(handle.stateOf(session).nodes, [{ id: 'old-context', seq: event.seq }])
|
|
45
|
+
assert.equal(reconcileMemoryContext(session, handle.stateOf(session), 'current synthetic memory').replaced, 1)
|
|
46
|
+
assert.deepEqual(session.deriveMessages()[0], ordinary)
|
|
47
|
+
assert.doesNotMatch(JSON.stringify(session.deriveMessages()), /obsolete synthetic memory/)
|
|
48
|
+
assert.match(JSON.stringify(session.deriveMessages()), /keep other provider/)
|
|
49
|
+
assert.deepEqual(handle.stateOf(session).nodes, [])
|
|
50
|
+
await handle.dispose()
|
|
51
|
+
// A registration acquired after events exist must reconstruct from the real Session.
|
|
52
|
+
const replay = await register()
|
|
53
|
+
assert.deepEqual(replay.stateOf(session).nodes, [])
|
|
54
|
+
const replayed = ctx.sessions.create('solopup-synthetic-replay', { seed: [{ ...event, seq: 0 }] })
|
|
55
|
+
assert.equal(replay.stateOf(replayed).nodes.length, 1)
|
|
56
|
+
await replay.dispose()
|
|
57
|
+
if (replay.retained) assert.equal(replay.stateOf(replayed).nodes.length, 1, 'retained readers remain usable after owner disposal')
|
|
58
|
+
await assert.rejects(async () => await ctx.sessionProjections.register({ ...definition, stateVersion: -1 }), /non-negative/)
|
|
59
|
+
await owner.dispose()
|
|
60
|
+
writeFileSync(${JSON.stringify(reportFile)}, JSON.stringify({ passed: true, realDshHost: true, realModel: false, cleanHome: true, syntheticInput: true, tools: stamp.tools.length, projectionApi: handle.retained ? 'retained-handle' : 'legacy-registry', historyAvailable: stamp.memoryHistory.available, liveState: true, rewritePreservesOrdinaryAndOtherProvider: true, coldReplay: true, registrationRejected: true, ownerDisposed: true }) + '\\n')
|
|
61
|
+
} catch (error) {
|
|
62
|
+
writeFileSync(${JSON.stringify(reportFile)}, JSON.stringify({ passed: false, error: error.stack }) + '\\n')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`)
|
|
66
|
+
writeFileSync(join(profile, 'package.json'), JSON.stringify({ name: 'solopup-history-probe', private: true, dependencies: {}, dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'] } } }))
|
|
67
|
+
writeFileSync(join(profile, 'cordis.yml'), '[]\n')
|
|
68
|
+
writeFileSync(join(profile, 'cordis.patch.yml'), `- insert:\n - id: history-probe\n name: ${JSON.stringify(pathToFileURL(driver).href)}\n`)
|
|
69
|
+
let output = '', child
|
|
70
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
71
|
+
try {
|
|
72
|
+
child = spawn(CLI, ['--profile', 'probe', '--port', '0', '--no-open'], { env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
73
|
+
child.stdout.on('data', chunk => { output += chunk }); child.stderr.on('data', chunk => { output += chunk })
|
|
74
|
+
let childError
|
|
75
|
+
child.once('error', error => { childError = error.code })
|
|
76
|
+
const deadline = Date.now() + 40000
|
|
77
|
+
while (!existsSync(reportFile) && !childError && child.exitCode === null && Date.now() < deadline) await sleep(100)
|
|
78
|
+
if (!existsSync(reportFile)) throw Error(`history-probe-host-failed:${childError ?? child.exitCode ?? 'timeout'}\n${output.slice(-6000)}`)
|
|
79
|
+
const report = JSON.parse(readFileSync(reportFile, 'utf8'))
|
|
80
|
+
if (!report.passed) throw Error(report.error + '\n' + output.slice(-6000))
|
|
81
|
+
console.log('[PASS] Real DSH history registration, live state, rewrite, cold replay, rejection and disposal')
|
|
82
|
+
console.log(JSON.stringify(report))
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('[FAIL] ' + error.message); process.exitCode = 1
|
|
85
|
+
} finally {
|
|
86
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
87
|
+
const stopped = new Promise(resolve => child.once('exit', resolve))
|
|
88
|
+
child.kill('SIGTERM'); await Promise.race([stopped, sleep(1500)])
|
|
89
|
+
if (child.exitCode === null && child.signalCode === null) { child.kill('SIGKILL'); await Promise.race([stopped, sleep(1500)]) }
|
|
90
|
+
}
|
|
91
|
+
rmSync(root, { recursive: true, force: true })
|
|
92
|
+
}
|
package/scripts/probe-load.mjs
CHANGED
|
@@ -84,5 +84,5 @@ if (missing.length > 0) {
|
|
|
84
84
|
console.error(`[FAIL] 少注册了工具:${missing.join('、')}`)
|
|
85
85
|
process.exit(1)
|
|
86
86
|
}
|
|
87
|
-
if (stamp.sourceControl?.state !== 'managed' || stamp.degraded?.length) { console.error('[FAIL] 工具存在但来源控制未就绪'); process.exit(1) }
|
|
87
|
+
if (stamp.sourceControl?.state !== 'managed' || stamp.degraded?.length || stamp.memoryHistory?.available !== true) { console.error('[FAIL] 工具存在但来源控制未就绪'); process.exit(1) }
|
|
88
88
|
console.log(`[PASS] 隔离实例加载成功:v${stamp.version},${stamp.tools.length} 个工具,pid ${stamp.pid}`)
|
|
@@ -49,6 +49,10 @@ try {
|
|
|
49
49
|
report.lockContention = testOutput.split('\n').filter((line) => /accepted=40; lock-busy retries=/.test(line))
|
|
50
50
|
if (!Number.isInteger(report.packagedTests) || report.packagedTests < 1) throw new Error('package-probe-test-count-missing')
|
|
51
51
|
|
|
52
|
+
const historyOutput = run(process.execPath, ['scripts/probe-history.mjs'], unpacked, { env: { ...cleanEnv, DSH_CLI: CLI } })
|
|
53
|
+
report.historyContract = JSON.parse(historyOutput.trim().split('\n').at(-1))
|
|
54
|
+
if (!report.historyContract.passed) throw new Error('package-probe-history-contract-failed')
|
|
55
|
+
|
|
52
56
|
// Entire profile is authored here; no copy of the user's desktop configuration.
|
|
53
57
|
const home = join(root, 'home'); const profile = join(home, 'profiles', 'clean')
|
|
54
58
|
mkdirSync(profile, { recursive: true })
|
|
@@ -71,8 +75,8 @@ try {
|
|
|
71
75
|
}
|
|
72
76
|
if (!stamp) throw new Error(`package-probe-host-load-failed:${hostError ?? host.exitCode ?? 'timeout'}`)
|
|
73
77
|
const expected = ['memory_write', 'memory_forget', 'memory_links', 'memory_search', 'memory_prune', 'memory_status']
|
|
74
|
-
if (stamp.version !== pkg.version || expected.some((tool) => !stamp.tools.includes(tool)) || stamp.degraded.length || stamp.sourceControl?.state !== 'managed') throw new Error('package-probe-host-registration-failed')
|
|
75
|
-
report.hostLoad = { version: stamp.version, node: stamp.node, tools: stamp.tools, cleanHome: true, profileCopied: false, sourceControl: stamp.sourceControl,
|
|
78
|
+
if (stamp.version !== pkg.version || expected.some((tool) => !stamp.tools.includes(tool)) || stamp.degraded.length || stamp.sourceControl?.state !== 'managed' || stamp.memoryHistory?.available !== true) throw new Error('package-probe-host-registration-failed')
|
|
79
|
+
report.hostLoad = { version: stamp.version, node: stamp.node, tools: stamp.tools, cleanHome: true, profileCopied: false, sourceControl: stamp.sourceControl, memoryHistory: stamp.memoryHistory,
|
|
76
80
|
environmentAllowlist: Object.keys(cleanEnv).sort(), parentEnvironmentInherited: false }
|
|
77
81
|
const retained = join(home, 'memory', 'MEMORY.md')
|
|
78
82
|
const syntheticMemory = '- [user] 合成验收偏好只用于打包检查 — user.md\n'
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 发版前安全自查(只读)。检查依赖面、运行时执行/网络面、密钥与绝对路径、
|
|
4
|
+
* CI 工作流权限与 pin、打包内容、许可与引擎声明。
|
|
5
|
+
*
|
|
6
|
+
* node scripts/security-check.mjs
|
|
7
|
+
* node scripts/security-check.mjs --json
|
|
8
|
+
*
|
|
9
|
+
* 这不是渗透测试,也不能替代独立审计;它检查的是“不该进发行包/不该在运行时出现”的机械项。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
13
|
+
import { dirname, join, relative } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const ROOT = join(HERE, '..')
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
|
|
19
|
+
const checks = []
|
|
20
|
+
const check = (id, run) => {
|
|
21
|
+
try {
|
|
22
|
+
const detail = run()
|
|
23
|
+
checks.push({ id, passed: true, detail: detail ?? null })
|
|
24
|
+
} catch (error) {
|
|
25
|
+
checks.push({ id, passed: false, error: error.message })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const assert = (condition, message) => {
|
|
29
|
+
if (!condition) throw new Error(message)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function walk(dir, out = []) {
|
|
33
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
if (['node_modules', 'research', '.git', '.github', 'ci'].includes(entry.name)) continue
|
|
35
|
+
const full = join(dir, entry.name)
|
|
36
|
+
if (entry.isDirectory()) walk(full, out)
|
|
37
|
+
else if (entry.isFile()) out.push(full)
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const files = walk(ROOT)
|
|
43
|
+
const runtimeFiles = files.filter((file) => file.endsWith('.mjs') && !/\/test-[^/]+\.mjs$/.test(file) && !file.includes('/scripts/') && !file.includes('/eval/'))
|
|
44
|
+
const text = (file) => readFileSync(file, 'utf8')
|
|
45
|
+
|
|
46
|
+
/** 发行包实际包含的文件(目录条目展开)。绝对路径与密钥检查只看这些。 */
|
|
47
|
+
function shippedFiles() {
|
|
48
|
+
const out = new Set()
|
|
49
|
+
for (const entry of pkg.files ?? []) {
|
|
50
|
+
if (entry.endsWith('/')) {
|
|
51
|
+
const prefix = join(ROOT, entry)
|
|
52
|
+
if (existsSync(prefix)) for (const file of walk(prefix)) out.add(file)
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
const full = join(ROOT, entry)
|
|
56
|
+
if (existsSync(full) && statSync(full).isFile()) out.add(full)
|
|
57
|
+
}
|
|
58
|
+
return [...out]
|
|
59
|
+
}
|
|
60
|
+
/** 注入测试语料与安全测试里故意放的反例,不是真实凭据。 */
|
|
61
|
+
const fixtureAllowlist = (file) => /\/corpus\/|\/test-[^/]+\.mjs$|\/scripts\/security-check\.mjs$/.test(file)
|
|
62
|
+
|
|
63
|
+
check('dependencies', () => {
|
|
64
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.optionalDependencies ?? {}), ...(pkg.peerDependencies ?? {}) }
|
|
65
|
+
assert(Object.keys(deps).length === 0, `存在运行时依赖:${Object.keys(deps).join(',')}`)
|
|
66
|
+
return 'zero runtime dependencies'
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
check('install-hooks', () => {
|
|
70
|
+
const hooks = ['preinstall', 'install', 'postinstall', 'prepare', 'prepublishOnly'].filter((name) => pkg.scripts?.[name] !== undefined)
|
|
71
|
+
assert(hooks.length === 0, `存在安装/发布钩子:${hooks.join(',')}`)
|
|
72
|
+
return 'no install hooks'
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
check('runtime-network', () => {
|
|
76
|
+
const hits = []
|
|
77
|
+
for (const file of runtimeFiles) {
|
|
78
|
+
const body = text(file)
|
|
79
|
+
if (/\bfetch\s*\(/.test(body) || /node:(http|https|net|tls)/.test(body) || /\.listen\s*\(/.test(body) || /new\s+WebSocket/.test(body)) hits.push(relative(ROOT, file))
|
|
80
|
+
}
|
|
81
|
+
assert(hits.length === 0, `运行时出现网络/监听面:${hits.join(',')}`)
|
|
82
|
+
return `${runtimeFiles.length} 个运行时文件无网络调用`
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
check('runtime-exec', () => {
|
|
86
|
+
const allowed = new Set(['restore.mjs'])
|
|
87
|
+
const hits = []
|
|
88
|
+
for (const file of runtimeFiles) {
|
|
89
|
+
if (!/child_process|\bexecSync\b|\bspawnSync\b/.test(text(file))) continue
|
|
90
|
+
if (!allowed.has(relative(ROOT, file))) hits.push(relative(ROOT, file))
|
|
91
|
+
}
|
|
92
|
+
assert(hits.length === 0, `未登记的运行时子进程:${hits.join(',')}`)
|
|
93
|
+
const restore = text(join(ROOT, 'restore.mjs'))
|
|
94
|
+
assert(/spawnSync\('tar',\s*\[/.test(restore), 'restore.mjs 的子进程不是固定的 tar 参数调用')
|
|
95
|
+
assert(!/shell:\s*true/.test(restore), 'restore.mjs 启用了 shell')
|
|
96
|
+
return 'restore.mjs 仅以固定参数调用 tar(无 shell)'
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
check('secrets-shipped', () => {
|
|
100
|
+
const patterns = [/gho_[A-Za-z0-9]{20,}/, /npm_[A-Za-z0-9]{20,}/, /sk-[A-Za-z0-9]{20,}/, /-----BEGIN [A-Z ]*PRIVATE KEY-----/]
|
|
101
|
+
const hits = []
|
|
102
|
+
const shipped = shippedFiles().filter((file) => !fixtureAllowlist(file))
|
|
103
|
+
for (const file of [...shipped, join(ROOT, '.github', 'workflows', 'publish.yml')]) {
|
|
104
|
+
if (!existsSync(file) || !/\.(mjs|json|md|yml|yaml|js|txt)$/.test(file)) continue
|
|
105
|
+
const body = text(file)
|
|
106
|
+
for (const pattern of patterns) if (pattern.test(body)) hits.push(`${relative(ROOT, file)}:${pattern}`)
|
|
107
|
+
}
|
|
108
|
+
assert(hits.length === 0, `发行文件里发现密钥:${hits.slice(0, 5).join(';')}`)
|
|
109
|
+
return `${shipped.length} 个发行文件无密钥(测试语料已列允许项)`
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
check('absolute-paths-shipped', () => {
|
|
113
|
+
const pattern = /\/Users\/[a-z][^/\s"']+/
|
|
114
|
+
const hits = shippedFiles().filter((file) => /\.(mjs|json|md|yml|yaml|js|txt)$/.test(file) && pattern.test(text(file)))
|
|
115
|
+
assert(hits.length === 0, `发行文件含机器绝对路径:${hits.map((file) => relative(ROOT, file)).slice(0, 5).join(',')}`)
|
|
116
|
+
return `${shippedFiles().length} 个发行文件无 /Users 绝对路径`
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
check('workflow-pins', () => {
|
|
120
|
+
const file = join(ROOT, '.github', 'workflows', 'publish.yml')
|
|
121
|
+
if (!existsSync(file)) return '未包含发布工作流'
|
|
122
|
+
const body = text(file)
|
|
123
|
+
const uses = [...body.matchAll(/^\s*(?:-\s*)?uses:\s*(\S+)/gm)].map((match) => match[1])
|
|
124
|
+
const unpinned = uses.filter((value) => !value.startsWith('./.github/workflows/') && !/@[0-9a-f]{40}$/.test(value))
|
|
125
|
+
assert(unpinned.length === 0, `未按 commit pin 的 action:${unpinned.join(',')}`)
|
|
126
|
+
assert(/permissions:\s*\n\s*contents:\s*read/.test(body), '工作流权限不是 contents: read')
|
|
127
|
+
assert(!/secrets\.[A-Za-z0-9_]+/.test(body), '工作流引用了 secrets(优先 OIDC/可信发布)')
|
|
128
|
+
assert(!/pull_request_target/.test(body), '工作流使用了 pull_request_target')
|
|
129
|
+
return `${uses.length} 个 action 全部 pin 到 commit;contents: read;无 secrets`
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
check('package-contents', () => {
|
|
133
|
+
const bad = (pkg.files ?? []).filter((entry) => /research|last-run|\.tgz|node_modules|\.env/.test(entry))
|
|
134
|
+
assert(bad.length === 0, `发行包包含不该发布的条目:${bad.join(',')}`)
|
|
135
|
+
assert(pkg.files.includes('LICENSE') || pkg.license === 'MIT', '发行包未声明 LICENSE')
|
|
136
|
+
return `${pkg.files.length} 个发布条目,无研究目录/日志/tgz`
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
check('license', () => {
|
|
140
|
+
assert(pkg.license === 'MIT', `license=${pkg.license}`)
|
|
141
|
+
const body = text(join(ROOT, 'LICENSE'))
|
|
142
|
+
assert(/MIT License/.test(body), 'LICENSE 不是 MIT 正文')
|
|
143
|
+
return 'MIT'
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
check('engines', () => {
|
|
147
|
+
assert(pkg.engines?.node, '缺少 node engines 声明')
|
|
148
|
+
assert(pkg.engines?.dsh, '缺少 dsh engines 声明')
|
|
149
|
+
return `node ${pkg.engines.node};dsh ${pkg.engines.dsh}`
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
check('gitignore', () => {
|
|
153
|
+
const body = existsSync(join(ROOT, '.gitignore')) ? text(join(ROOT, '.gitignore')) : ''
|
|
154
|
+
const need = ['node_modules/', '*.tgz']
|
|
155
|
+
const missing = need.filter((line) => !body.includes(line))
|
|
156
|
+
assert(missing.length === 0, `.gitignore 缺:${missing.join(',')}`)
|
|
157
|
+
return '.gitignore 覆盖 node_modules 与 tgz'
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
check('store-permissions-code', () => {
|
|
161
|
+
const security = text(join(ROOT, 'security.mjs'))
|
|
162
|
+
assert(/0o700/.test(security) && /0o600/.test(security), 'security.mjs 未显式设置 0700/0600')
|
|
163
|
+
return 'store 目录 0700、文件 0600(由 soak 实测再次验证)'
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const passed = checks.every((row) => row.passed)
|
|
167
|
+
const report = { passed, scope: 'mechanical pre-release checks; not an independent audit', version: pkg.version, checks }
|
|
168
|
+
if (process.argv.includes('--json')) process.stdout.write(JSON.stringify(report, null, 2) + '\n')
|
|
169
|
+
else {
|
|
170
|
+
for (const row of checks) console.log(`[${row.passed ? 'PASS' : 'FAIL'}] ${row.id}${row.passed ? ` · ${row.detail}` : ` · ${row.error}`}`)
|
|
171
|
+
console.log(passed ? `安全自查通过(${checks.length} 项;机械项,不是独立审计)` : '安全自查未通过')
|
|
172
|
+
}
|
|
173
|
+
if (!passed) process.exitCode = 1
|