@twinsearth/fang-dsh-plugin 0.0.0-stage → 3.3.4

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 (144) hide show
  1. package/CLA.md +79 -0
  2. package/LICENSE-COMMERCIAL +78 -0
  3. package/LICENSE-CORE +661 -0
  4. package/LICENSE-SDK +21 -0
  5. package/LICENSES/cordis-MIT.txt +39 -0
  6. package/NOTICE +95 -0
  7. package/README.md +158 -2
  8. package/client.js +7287 -0
  9. package/cordis.patch.yml +163 -0
  10. package/defaults/umbrella-rules.yaml +113 -0
  11. package/docs/CONTRACT-INTEROP.md +77 -0
  12. package/docs/CRASH-SAFETY.md +69 -0
  13. package/docs/DECISION-RELAY.md +46 -0
  14. package/docs/DSH-ARCHITECTURE-MAP.md +73 -0
  15. package/docs/FORK-SEED.md +64 -0
  16. package/docs/MAIN-CITY-GATE.md +58 -0
  17. package/docs/OVERLAY-MASTER-SWITCH.md +35 -0
  18. package/docs/PACKAGE-RENAME-v3.3.3.md +40 -0
  19. package/docs/PM-DECISION-POLICY.md +61 -0
  20. package/docs/WORKFLOW-UI.md +86 -0
  21. package/icon.svg +8 -0
  22. package/locale/en.json +6 -0
  23. package/locale/zh.json +6 -0
  24. package/package.json +71 -4
  25. package/sdk/README.md +65 -0
  26. package/sdk/examples/status-badge/README.md +16 -0
  27. package/sdk/examples/status-badge/client.js +60 -0
  28. package/sdk/examples/status-badge/cordis.patch.yml +7 -0
  29. package/sdk/examples/status-badge/index.js +12 -0
  30. package/sdk/examples/status-badge/package.json +24 -0
  31. package/sdk/templates/plugin/README.md +36 -0
  32. package/sdk/templates/plugin/client.js +35 -0
  33. package/sdk/templates/plugin/cordis.patch.yml +8 -0
  34. package/sdk/templates/plugin/icon.svg +5 -0
  35. package/sdk/templates/plugin/index.js +23 -0
  36. package/sdk/templates/plugin/locale/en.json +6 -0
  37. package/sdk/templates/plugin/package.json +36 -0
  38. package/src/compose.mjs +67 -0
  39. package/src/contract/VENDOR.json +16 -0
  40. package/src/contract/client.js +93 -0
  41. package/src/contract/constants.js +94 -0
  42. package/src/contract/envelope.js +83 -0
  43. package/src/contract/index.js +20 -0
  44. package/src/contract/negotiate.js +106 -0
  45. package/src/contract/relay.js +223 -0
  46. package/src/contract/server.js +77 -0
  47. package/src/contract/transports.js +319 -0
  48. package/src/index.mjs +11 -0
  49. package/src/kernel/core.mjs +100 -0
  50. package/src/kernel/extensions.mjs +61 -0
  51. package/src/kernel/fsx.mjs +349 -0
  52. package/src/kernel/index.mjs +21 -0
  53. package/src/kernel/ledger.mjs +94 -0
  54. package/src/kernel/paths.mjs +182 -0
  55. package/src/kernel/role.mjs +68 -0
  56. package/src/kernel/rounds.mjs +172 -0
  57. package/src/kernel/schema.mjs +5 -0
  58. package/src/kernel/types.mjs +266 -0
  59. package/src/kernel/vendor/cosmokit.mjs +421 -0
  60. package/src/kernel/vendor/schemastery.mjs +638 -0
  61. package/src/lib/adapter-client.mjs +383 -0
  62. package/src/lib/adapter-protocol.mjs +243 -0
  63. package/src/lib/ai-secret-store.mjs +202 -0
  64. package/src/lib/boot-guard.mjs +186 -0
  65. package/src/lib/branch-store.mjs +84 -0
  66. package/src/lib/branch.mjs +74 -0
  67. package/src/lib/bridge-client.mjs +133 -0
  68. package/src/lib/broker-adapter.mjs +67 -0
  69. package/src/lib/broker.mjs +45 -0
  70. package/src/lib/business-producers.mjs +109 -0
  71. package/src/lib/command-cache.mjs +209 -0
  72. package/src/lib/compat.mjs +170 -0
  73. package/src/lib/composer-journal.mjs +128 -0
  74. package/src/lib/container/index.mjs +806 -0
  75. package/src/lib/decision-events.mjs +129 -0
  76. package/src/lib/decision-ingest-client.mjs +41 -0
  77. package/src/lib/decision-outbox.mjs +129 -0
  78. package/src/lib/decision-policy.mjs +52 -0
  79. package/src/lib/default-essence.mjs +61 -0
  80. package/src/lib/desktop-launch.mjs +138 -0
  81. package/src/lib/essence.mjs +1211 -0
  82. package/src/lib/fork-client.mjs +410 -0
  83. package/src/lib/fork-config-lock.mjs +166 -0
  84. package/src/lib/fork-seed.mjs +304 -0
  85. package/src/lib/fork-sessions.mjs +183 -0
  86. package/src/lib/fork-supervision-settings.mjs +58 -0
  87. package/src/lib/fork-supervisor.mjs +122 -0
  88. package/src/lib/fs-guard.mjs +28 -0
  89. package/src/lib/http-auth.mjs +162 -0
  90. package/src/lib/idle-audit.mjs +178 -0
  91. package/src/lib/interaction-poke.mjs +61 -0
  92. package/src/lib/main-gate.mjs +579 -0
  93. package/src/lib/mirror-etag.mjs +92 -0
  94. package/src/lib/mirror-official-source.mjs +93 -0
  95. package/src/lib/mirror.mjs +478 -0
  96. package/src/lib/mother-link.mjs +156 -0
  97. package/src/lib/mutex.mjs +31 -0
  98. package/src/lib/permissions.mjs +115 -0
  99. package/src/lib/pnpm-store.mjs +189 -0
  100. package/src/lib/preferences.mjs +212 -0
  101. package/src/lib/producer-contract.mjs +162 -0
  102. package/src/lib/reflow.mjs +41 -0
  103. package/src/lib/remote.mjs +408 -0
  104. package/src/lib/restart-contract.mjs +752 -0
  105. package/src/lib/roots.mjs +30 -0
  106. package/src/lib/rule-registry.mjs +1127 -0
  107. package/src/lib/runtime-capture.mjs +307 -0
  108. package/src/lib/semaphore.mjs +34 -0
  109. package/src/lib/session-bundle.mjs +138 -0
  110. package/src/lib/session-format.mjs +672 -0
  111. package/src/lib/sse-parser.mjs +102 -0
  112. package/src/lib/ssrf-guard.mjs +170 -0
  113. package/src/lib/status-store.mjs +257 -0
  114. package/src/lib/task-handle.mjs +622 -0
  115. package/src/lib/ui-actions.mjs +55 -0
  116. package/src/lib/umbrella.mjs +1661 -0
  117. package/src/lib/upgrade-coordinator.mjs +107 -0
  118. package/src/lib/upgrade-status.mjs +43 -0
  119. package/src/plugins/adapter.mjs +1468 -0
  120. package/src/plugins/approver.mjs +1210 -0
  121. package/src/plugins/auditor.mjs +330 -0
  122. package/src/plugins/buffer-keeper.mjs +522 -0
  123. package/src/plugins/decision-center.mjs +2581 -0
  124. package/src/plugins/forker.mjs +1610 -0
  125. package/src/plugins/healer.mjs +702 -0
  126. package/src/plugins/hot-reloader.mjs +274 -0
  127. package/src/plugins/imports.mjs +247 -0
  128. package/src/plugins/orchestrator.mjs +3938 -0
  129. package/src/plugins/restart-guard.mjs +1022 -0
  130. package/src/plugins/router.mjs +714 -0
  131. package/src/plugins/scorer.mjs +661 -0
  132. package/src/plugins/session-bridge.mjs +166 -0
  133. package/src/plugins/session-catalog.mjs +771 -0
  134. package/src/plugins/session-router.mjs +125 -0
  135. package/src/plugins/session-sync.mjs +171 -0
  136. package/src/plugins/umbrella.mjs +419 -0
  137. package/tools/boot-guard.mjs +95 -0
  138. package/tools/decision-ingest.mjs +53 -0
  139. package/tools/desktop-launch.mjs +71 -0
  140. package/tools/fangctl.mjs +408 -0
  141. package/tools/gate/main-city.mjs +204 -0
  142. package/tools/mcp/server.mjs +121 -0
  143. package/tools/mcp/tools.mjs +65 -0
  144. package/tools/recover-queue.mjs +133 -0
@@ -0,0 +1,330 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Fang contributors — see LICENSE-CORE.
3
+ // dsh-Fang 2.8 — fang-auditor: the append-only chained ledger of the whole system.
4
+ //
5
+ // Every cross-plugin fact (fork started, promote committed, restart resolved, ...) is
6
+ // appended through this service, so the chain can be verified later:
7
+ // hash_i = sha256( recursive-canonical(record_without_hash) + prevHash )
8
+ // A single edited/deleted/swapped line makes verify() fail at that index.
9
+ //
10
+ // bootCheck() is the startup admission gate (CONTRACTS.md §5.2): it looks at the tail
11
+ // left by the PREVIOUS run and reports
12
+ // * unclean-boot — the previous run's last record is not shutdown-graceful
13
+ // * interrupted-in-guard— a restart was opened but never resolved
14
+ // This instance marks its own records with `inst`, so records written by this run can
15
+ // never be mistaken for the previous run's tail (and no timestamp-window heuristics are
16
+ // needed — those race when two runs happen within the same millisecond).
17
+ //
18
+ // v2.0.0 (CONTRACTS-V2.md §1/§5.7): the plugin is role-gated (matrix allows mother AND
19
+ // fork) and exposes `scope`. The mother does not share the fork's process/bus, so it
20
+ // pulls each fork's ledger over HTTP and merges it here:
21
+ // aggregate({limit, sources}) -> {ok,total,records,sources}
22
+ // local records get taskId:null, each fork's records get that source's taskId,
23
+ // merged ascending by at (tie-break seq) and the newest `limit` are returned.
24
+ // bootAudit() — alias of bootCheck(), kept for the v2 vocabulary.
25
+ // cloudSink() — commercial backup/cloud hook placeholder ({ok:false,reason:'not-configured'}).
26
+ import path from 'node:path'
27
+ import {
28
+ AUDIT,
29
+ SERVICE,
30
+ appendChained,
31
+ ensureDir,
32
+ iso,
33
+ newId,
34
+ nowMs,
35
+ readTextSafe,
36
+ resolveFangLayout,
37
+ roleGate,
38
+ verifyChain,
39
+ z,
40
+ } from '../kernel/index.mjs'
41
+
42
+ export const name = 'fang-auditor'
43
+ export const inject = []
44
+
45
+ const DEFAULTS = {
46
+ dataRoot: '',
47
+ bootSelfCheck: true,
48
+ ledgerFile: '',
49
+ role: 'mother',
50
+ }
51
+
52
+ export const Config = z
53
+ .object({
54
+ dataRoot: z.string().default('').description('审计数据目录(空 = 默认 $DSH_HOME/fang/<profile>)'),
55
+ bootSelfCheck: z.boolean().default(true).description('apply 时做一次只读启动自检(不写盘)'),
56
+ ledgerFile: z.string().default('').description('显式账本路径(空 = dataDir/audit.jsonl)'),
57
+ role: z
58
+ .union([z.const('mother'), z.const('fork')])
59
+ .default('mother')
60
+ .description('部署角色(v2 §1);DSH_FANG_ROLE 环境变量优先级更高(forker 启动分身时注入)'),
61
+ })
62
+ .description('fang-auditor 配置')
63
+
64
+ const msgOf = (e) => String(e && e.message ? e.message : e)
65
+
66
+ /** A restart was opened by the guard (requested or already committed). */
67
+ function isRestartOpen(rec) {
68
+ if (!rec || typeof rec !== 'object') return false
69
+ if (rec.kind === AUDIT.RESTART_REQUESTED || rec.kind === 'restart-committed') return true
70
+ return rec.action === 'restart' && (rec.phase === 'requested' || rec.phase === 'committed')
71
+ }
72
+
73
+ /** A restart was closed (approved/denied/timeout all resolve to restart-resolved). */
74
+ function isRestartResolved(rec) {
75
+ if (!rec || typeof rec !== 'object') return false
76
+ return rec.kind === AUDIT.RESTART_RESOLVED || rec.action === 'restart-resolved'
77
+ }
78
+
79
+ export function apply(ctx, config) {
80
+ // v2 §1:角色门禁必须是 apply 的第一件事;未部署到该角色时不注册服务/不自检。
81
+ const gate = roleGate('auditor', {
82
+ cfgRole: config && typeof config === 'object' ? config.role : '',
83
+ logger: ctx.logger,
84
+ })
85
+ if (!gate.active) return null
86
+ const scope = gate.role
87
+ const cfg = { ...DEFAULTS, ...(config && typeof config === 'object' ? config : {}) }
88
+ let layout
89
+ try {
90
+ layout = resolveFangLayout({ dataRoot: cfg.dataRoot })
91
+ } catch {
92
+ layout = { dataDir: '', ledgerFile: path.resolve('audit.jsonl') }
93
+ }
94
+ const ledgerFile = cfg.ledgerFile ? path.resolve(cfg.ledgerFile) : layout.ledgerFile
95
+ // Identity of THIS plugin instance: records carrying it are this run's own writes.
96
+ const instanceId = newId('inst')
97
+ let lastError = null
98
+ let lastBoot = null
99
+ // Fail-closed after a chain check failed: appending on top of a broken chain would
100
+ // launder the tampering (the kernel appendChained only refuses an unparsable TAIL).
101
+ let integrityBroken = false
102
+
103
+ /** Tolerant JSONL read: bad lines are skipped here and reported by verify(). */
104
+ const readAll = () => {
105
+ const raw = readTextSafe(ledgerFile, '')
106
+ const out = []
107
+ for (const line of raw.split('\n')) {
108
+ if (!line.trim()) continue
109
+ try {
110
+ const rec = JSON.parse(line)
111
+ if (rec && typeof rec === 'object') out.push(rec)
112
+ } catch {
113
+ /* verify() reports the index; reading must not throw */
114
+ }
115
+ }
116
+ return out
117
+ }
118
+
119
+ /**
120
+ * Append one chained record. `at` and `seq` are filled automatically; `data` wins on
121
+ * collision (so callers can record a fork's own pid via data.pid).
122
+ * @returns {object|null} the appended record, or null when the ledger tail is unusable.
123
+ */
124
+ const append = (kind, data) => {
125
+ if (typeof kind !== 'string' || !kind.trim()) return null
126
+ if (integrityBroken) {
127
+ lastError = 'ledger-integrity-broken: refusing to append on top of a broken chain'
128
+ return null
129
+ }
130
+ try {
131
+ ensureDir(path.dirname(ledgerFile))
132
+ const body = {
133
+ kind: kind.trim(),
134
+ pid: process.pid,
135
+ at: iso(nowMs()),
136
+ ...(data ?? {}),
137
+ inst: (data && data.inst) || instanceId,
138
+ }
139
+ const chain = appendChained(ledgerFile, body)
140
+ lastError = null
141
+ return { ...body, ...chain }
142
+ } catch (e) {
143
+ // An unparsable tail is deliberately NOT overwritten (that would hide tampering).
144
+ lastError = msgOf(e)
145
+ return null
146
+ }
147
+ }
148
+
149
+ /** @returns {object[]} the newest `limit` records (default 100, 0 = none). */
150
+ const read = (opts = {}) => {
151
+ const recs = readAll()
152
+ const rawLimit = opts == null ? 100 : opts.limit
153
+ if (rawLimit === undefined || rawLimit === null) return recs.slice(-100)
154
+ const limit = Math.max(0, Math.floor(Number(rawLimit) || 0))
155
+ return limit === 0 ? [] : recs.slice(-limit)
156
+ }
157
+
158
+ /** Whole-chain verification; names the first bad record by its seq. */
159
+ const verify = () => {
160
+ const result = verifyChain(ledgerFile)
161
+ integrityBroken = !result.ok
162
+ const recs = readAll()
163
+ const badIndex = Number.isInteger(result.badIndex) ? result.badIndex : null
164
+ const badRec = badIndex !== null ? recs[badIndex] : null
165
+ return {
166
+ ok: !!result.ok,
167
+ records: Number.isFinite(result.count) ? result.count : recs.length,
168
+ firstBadSeq: result.ok ? null : (badRec && Number.isFinite(badRec.seq) ? badRec.seq : badIndex),
169
+ badIndex,
170
+ reason: result.reason ?? null,
171
+ expected: result.expected,
172
+ actual: result.actual,
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Startup admission gate — read-only (CONTRACTS.md §5.2).
178
+ * Only records not written by this instance AND carrying a parsable timestamp are
179
+ * treated as "the previous run"; kernel boot-verify rows have no `at` and are skipped.
180
+ * @returns {{ok:boolean, findings:{id:string, level:'warn'|'error', message:string, detail?:object}[], records:number}}
181
+ */
182
+ const bootCheck = () => {
183
+ // Integrity is a precondition for appending; cache the verdict so a corrupt ledger
184
+ // cannot be extended later in this run (bootCheck itself stays read-only).
185
+ try {
186
+ const chain = verifyChain(ledgerFile)
187
+ integrityBroken = !chain.ok
188
+ } catch {
189
+ /* a read-only integrity probe must never throw out of bootCheck */
190
+ }
191
+ const recs = readAll()
192
+ const previous = []
193
+ for (const rec of recs) {
194
+ if (!rec || typeof rec !== 'object') continue
195
+ if (rec.inst !== undefined && rec.inst === instanceId) continue // this run's own writes
196
+ if (!Number.isFinite(Date.parse(String(rec.at ?? '')))) continue // unstructured row (e.g. boot-verify)
197
+ previous.push(rec)
198
+ }
199
+ const findings = []
200
+
201
+ const last = previous.length ? previous[previous.length - 1] : null
202
+ if (last) {
203
+ const lastKind = last.kind ?? last.action ?? null
204
+ if (lastKind !== AUDIT.SHUTDOWN_GRACEFUL) {
205
+ findings.push({
206
+ id: 'unclean-boot',
207
+ level: 'warn',
208
+ message: `上次运行未干净收尾:最后一条记录是 ${lastKind ?? 'unknown'}(缺少 shutdown-graceful)`,
209
+ detail: { lastKind, seq: last.seq ?? null, at: last.at ?? null, pid: last.pid ?? null },
210
+ })
211
+ }
212
+ }
213
+
214
+ let lastOpen = -1
215
+ for (let i = 0; i < previous.length; i++) if (isRestartOpen(previous[i])) lastOpen = i
216
+ if (lastOpen >= 0 && !previous.slice(lastOpen + 1).some(isRestartResolved)) {
217
+ const open = previous[lastOpen]
218
+ findings.push({
219
+ id: 'interrupted-in-guard',
220
+ level: 'warn',
221
+ message: '上次运行死在守卫中途:restart 已发起但始终没有 restart-resolved',
222
+ detail: {
223
+ requestId: open.requestId ?? null,
224
+ kind: open.kind ?? null,
225
+ at: open.at ?? null,
226
+ seq: open.seq ?? null,
227
+ },
228
+ })
229
+ }
230
+
231
+ return { ok: findings.length === 0, findings, records: recs.length, previous: previous.length, pid: process.pid }
232
+ }
233
+
234
+ // ───────── v2 §5.7 汇总 / 商用钩子 ─────────
235
+
236
+ const atMsOf = (rec) => {
237
+ const ms = Date.parse(String(rec?.at ?? ''))
238
+ return Number.isFinite(ms) ? ms : null
239
+ }
240
+ const seqOf = (rec) => {
241
+ const n = Number(rec?.seq)
242
+ return Number.isFinite(n) ? n : null
243
+ }
244
+ /** 合并多进程时间线:优先 at(不同进程的 seq 各自从 0 起,不可跨源比较),同刻退回 seq。 */
245
+ const compareRecords = (a, b) => {
246
+ const aAt = atMsOf(a)
247
+ const bAt = atMsOf(b)
248
+ if (aAt !== null && bAt !== null && aAt !== bAt) return aAt - bAt
249
+ const aSeq = seqOf(a)
250
+ const bSeq = seqOf(b)
251
+ if (aSeq !== null && bSeq !== null && aSeq !== bSeq) return aSeq - bSeq
252
+ return 0
253
+ }
254
+ const sourceTaskId = (source) => {
255
+ const raw = source && typeof source === 'object' ? source.taskId : null
256
+ return raw === undefined || raw === null ? null : String(raw)
257
+ }
258
+
259
+ /**
260
+ * v2 §5.7:纯汇总(只读本进程账本 + 调用方拉来的分身记录;不写盘、不改链)。
261
+ * 本地记录 taskId=null;每个 source 的记录强制打上该 source 的 taskId;
262
+ * 按 at/seq 升序合并后返回最新 limit 条(与 read() 的"最新 N 条"语义一致)。
263
+ */
264
+ const aggregate = (options = {}) => {
265
+ const rawLimit = options?.limit === undefined || options?.limit === null ? 200 : Number(options.limit)
266
+ const limit = Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : 200
267
+ const sources = Array.isArray(options?.sources) ? options.sources : []
268
+ const merged = []
269
+ for (const rec of readAll()) {
270
+ if (rec && typeof rec === 'object') merged.push({ ...rec, taskId: null })
271
+ }
272
+ for (const source of sources) {
273
+ const taskId = sourceTaskId(source)
274
+ const records = Array.isArray(source?.records) ? source.records : []
275
+ for (const rec of records) {
276
+ if (rec && typeof rec === 'object') merged.push({ ...rec, taskId })
277
+ }
278
+ }
279
+ merged.sort(compareRecords)
280
+ const total = merged.length
281
+ return {
282
+ ok: true,
283
+ scope,
284
+ total,
285
+ records: limit === 0 ? [] : merged.slice(-limit),
286
+ sources: sources.map(sourceTaskId),
287
+ }
288
+ }
289
+
290
+ /** 商用备份/云上传钩子占位(CONTRACTS-V2 §6:本期只留接入点,不实现传输)。 */
291
+ const cloudSink = () => ({ ok: false, reason: 'not-configured' })
292
+
293
+ const service = {
294
+ scope,
295
+ append,
296
+ read,
297
+ verify,
298
+ bootCheck,
299
+ /** v2 别名:bootCheck 的等价物(保留 v1 名称)。 */
300
+ bootAudit: bootCheck,
301
+ aggregate,
302
+ cloudSink,
303
+ get ledgerFile() {
304
+ return ledgerFile
305
+ },
306
+ get instanceId() {
307
+ return instanceId
308
+ },
309
+ get lastError() {
310
+ return lastError
311
+ },
312
+ get lastBoot() {
313
+ return lastBoot
314
+ },
315
+ }
316
+
317
+ ctx.effect(() => ctx.provide(SERVICE.auditor, service))
318
+ if (cfg.bootSelfCheck) {
319
+ // Read-only: apply never writes the ledger. Findings stay on the service for the
320
+ // orchestrator/UI to pick up; recording them is an explicit append() by the caller.
321
+ try {
322
+ lastBoot = bootCheck()
323
+ } catch (e) {
324
+ lastBoot = { ok: false, findings: [], records: 0, error: msgOf(e) }
325
+ }
326
+ }
327
+ return service
328
+ }
329
+
330
+ export const _internals = { isRestartOpen, isRestartResolved, DEFAULTS }