@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.
- package/CLA.md +79 -0
- package/LICENSE-COMMERCIAL +78 -0
- package/LICENSE-CORE +661 -0
- package/LICENSE-SDK +21 -0
- package/LICENSES/cordis-MIT.txt +39 -0
- package/NOTICE +95 -0
- package/README.md +158 -2
- package/client.js +7287 -0
- package/cordis.patch.yml +163 -0
- package/defaults/umbrella-rules.yaml +113 -0
- package/docs/CONTRACT-INTEROP.md +77 -0
- package/docs/CRASH-SAFETY.md +69 -0
- package/docs/DECISION-RELAY.md +46 -0
- package/docs/DSH-ARCHITECTURE-MAP.md +73 -0
- package/docs/FORK-SEED.md +64 -0
- package/docs/MAIN-CITY-GATE.md +58 -0
- package/docs/OVERLAY-MASTER-SWITCH.md +35 -0
- package/docs/PACKAGE-RENAME-v3.3.3.md +40 -0
- package/docs/PM-DECISION-POLICY.md +61 -0
- package/docs/WORKFLOW-UI.md +86 -0
- package/icon.svg +8 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +71 -4
- package/sdk/README.md +65 -0
- package/sdk/examples/status-badge/README.md +16 -0
- package/sdk/examples/status-badge/client.js +60 -0
- package/sdk/examples/status-badge/cordis.patch.yml +7 -0
- package/sdk/examples/status-badge/index.js +12 -0
- package/sdk/examples/status-badge/package.json +24 -0
- package/sdk/templates/plugin/README.md +36 -0
- package/sdk/templates/plugin/client.js +35 -0
- package/sdk/templates/plugin/cordis.patch.yml +8 -0
- package/sdk/templates/plugin/icon.svg +5 -0
- package/sdk/templates/plugin/index.js +23 -0
- package/sdk/templates/plugin/locale/en.json +6 -0
- package/sdk/templates/plugin/package.json +36 -0
- package/src/compose.mjs +67 -0
- package/src/contract/VENDOR.json +16 -0
- package/src/contract/client.js +93 -0
- package/src/contract/constants.js +94 -0
- package/src/contract/envelope.js +83 -0
- package/src/contract/index.js +20 -0
- package/src/contract/negotiate.js +106 -0
- package/src/contract/relay.js +223 -0
- package/src/contract/server.js +77 -0
- package/src/contract/transports.js +319 -0
- package/src/index.mjs +11 -0
- package/src/kernel/core.mjs +100 -0
- package/src/kernel/extensions.mjs +61 -0
- package/src/kernel/fsx.mjs +349 -0
- package/src/kernel/index.mjs +21 -0
- package/src/kernel/ledger.mjs +94 -0
- package/src/kernel/paths.mjs +182 -0
- package/src/kernel/role.mjs +68 -0
- package/src/kernel/rounds.mjs +172 -0
- package/src/kernel/schema.mjs +5 -0
- package/src/kernel/types.mjs +266 -0
- package/src/kernel/vendor/cosmokit.mjs +421 -0
- package/src/kernel/vendor/schemastery.mjs +638 -0
- package/src/lib/adapter-client.mjs +383 -0
- package/src/lib/adapter-protocol.mjs +243 -0
- package/src/lib/ai-secret-store.mjs +202 -0
- package/src/lib/boot-guard.mjs +186 -0
- package/src/lib/branch-store.mjs +84 -0
- package/src/lib/branch.mjs +74 -0
- package/src/lib/bridge-client.mjs +133 -0
- package/src/lib/broker-adapter.mjs +67 -0
- package/src/lib/broker.mjs +45 -0
- package/src/lib/business-producers.mjs +109 -0
- package/src/lib/command-cache.mjs +209 -0
- package/src/lib/compat.mjs +170 -0
- package/src/lib/composer-journal.mjs +128 -0
- package/src/lib/container/index.mjs +806 -0
- package/src/lib/decision-events.mjs +129 -0
- package/src/lib/decision-ingest-client.mjs +41 -0
- package/src/lib/decision-outbox.mjs +129 -0
- package/src/lib/decision-policy.mjs +52 -0
- package/src/lib/default-essence.mjs +61 -0
- package/src/lib/desktop-launch.mjs +138 -0
- package/src/lib/essence.mjs +1211 -0
- package/src/lib/fork-client.mjs +410 -0
- package/src/lib/fork-config-lock.mjs +166 -0
- package/src/lib/fork-seed.mjs +304 -0
- package/src/lib/fork-sessions.mjs +183 -0
- package/src/lib/fork-supervision-settings.mjs +58 -0
- package/src/lib/fork-supervisor.mjs +122 -0
- package/src/lib/fs-guard.mjs +28 -0
- package/src/lib/http-auth.mjs +162 -0
- package/src/lib/idle-audit.mjs +178 -0
- package/src/lib/interaction-poke.mjs +61 -0
- package/src/lib/main-gate.mjs +579 -0
- package/src/lib/mirror-etag.mjs +92 -0
- package/src/lib/mirror-official-source.mjs +93 -0
- package/src/lib/mirror.mjs +478 -0
- package/src/lib/mother-link.mjs +156 -0
- package/src/lib/mutex.mjs +31 -0
- package/src/lib/permissions.mjs +115 -0
- package/src/lib/pnpm-store.mjs +189 -0
- package/src/lib/preferences.mjs +212 -0
- package/src/lib/producer-contract.mjs +162 -0
- package/src/lib/reflow.mjs +41 -0
- package/src/lib/remote.mjs +408 -0
- package/src/lib/restart-contract.mjs +752 -0
- package/src/lib/roots.mjs +30 -0
- package/src/lib/rule-registry.mjs +1127 -0
- package/src/lib/runtime-capture.mjs +307 -0
- package/src/lib/semaphore.mjs +34 -0
- package/src/lib/session-bundle.mjs +138 -0
- package/src/lib/session-format.mjs +672 -0
- package/src/lib/sse-parser.mjs +102 -0
- package/src/lib/ssrf-guard.mjs +170 -0
- package/src/lib/status-store.mjs +257 -0
- package/src/lib/task-handle.mjs +622 -0
- package/src/lib/ui-actions.mjs +55 -0
- package/src/lib/umbrella.mjs +1661 -0
- package/src/lib/upgrade-coordinator.mjs +107 -0
- package/src/lib/upgrade-status.mjs +43 -0
- package/src/plugins/adapter.mjs +1468 -0
- package/src/plugins/approver.mjs +1210 -0
- package/src/plugins/auditor.mjs +330 -0
- package/src/plugins/buffer-keeper.mjs +522 -0
- package/src/plugins/decision-center.mjs +2581 -0
- package/src/plugins/forker.mjs +1610 -0
- package/src/plugins/healer.mjs +702 -0
- package/src/plugins/hot-reloader.mjs +274 -0
- package/src/plugins/imports.mjs +247 -0
- package/src/plugins/orchestrator.mjs +3938 -0
- package/src/plugins/restart-guard.mjs +1022 -0
- package/src/plugins/router.mjs +714 -0
- package/src/plugins/scorer.mjs +661 -0
- package/src/plugins/session-bridge.mjs +166 -0
- package/src/plugins/session-catalog.mjs +771 -0
- package/src/plugins/session-router.mjs +125 -0
- package/src/plugins/session-sync.mjs +171 -0
- package/src/plugins/umbrella.mjs +419 -0
- package/tools/boot-guard.mjs +95 -0
- package/tools/decision-ingest.mjs +53 -0
- package/tools/desktop-launch.mjs +71 -0
- package/tools/fangctl.mjs +408 -0
- package/tools/gate/main-city.mjs +204 -0
- package/tools/mcp/server.mjs +121 -0
- package/tools/mcp/tools.mjs +65 -0
- package/tools/recover-queue.mjs +133 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang contributors — see LICENSE-CORE.
|
|
3
|
+
// dsh-Fang v2 Router — 母体初判(decide)+ 分身复核(confirm)+ 容器兜底执行入口(runInContainer)。
|
|
4
|
+
//
|
|
5
|
+
// 决策来源(v1 契约 5.6 保留,v2 契约 5.3 增量):
|
|
6
|
+
// 1. assessment.veto === true(dsh.hotReload === false 的 E 级否决)→ deny(终局,不可被复核推翻)
|
|
7
|
+
// 2. 不可信来源:
|
|
8
|
+
// - 需要子进程(needsChildProcess)→ deny(容器内没有真实进程执行体)
|
|
9
|
+
// - 容器可用 → container
|
|
10
|
+
// - 容器不可用 → deny + 原因(fail-closed)
|
|
11
|
+
// 3. 可信来源:
|
|
12
|
+
// - 有裸写残留(未申报/申报无效的写)→ container(容器可用);否则 deny + 原因
|
|
13
|
+
// - 已申报写 或 无写 → sandbox(真实开发环境,不升红)
|
|
14
|
+
// * 评分本身只作 advisory 透传(veto 除外):A 级不可信 ⚠ 仍然进容器,E 级可信仍然 sandbox。
|
|
15
|
+
//
|
|
16
|
+
// v2 增量:
|
|
17
|
+
// * apply 首行 roleGate('router'):router 在 mother 与 fork 都激活,但 basis.role / basis.environment 如实标注。
|
|
18
|
+
// * decide() = 母体初判:静态 quickScan(从 assessment.signals 取能力证据;调用方也可以直接给 code 现场扫)
|
|
19
|
+
// + 来源可信度 + 能力需求 → RouteDecision(basis 带 role/environment/grade/source)。
|
|
20
|
+
// * confirm() = 分身复核:用分身的 assessment 与分身的容器可用性重算一次建议,再与母体 decision 取
|
|
21
|
+
// max(rank);rank = sandbox(1) < container(2) < deny(3),**只升不降**;母体 deny 原样返回(不可推翻)。
|
|
22
|
+
// 分身评分是**单向升级**信号:fork grade E → deny;fork grade D 或比母体 basis.grade 更差 → 至少 container。
|
|
23
|
+
// 复核只能把路由抬高,任何输入都不会把 container 降回 sandbox。
|
|
24
|
+
// * containerAvailable() 返回 {ok, state:'disabled'|'unavailable'|'ready', reason}(v1 只有 {ok,reason})。
|
|
25
|
+
// Config.container.enabled 默认 false → state='disabled';显式断言 containerAvailable=true 时 state='ready'/reason='configured'。
|
|
26
|
+
// * runInContainer() 懒加载 `../lib/container/index.mjs`(T8 提供)。模块不存在 / enabled=false / 宿主不可用
|
|
27
|
+
// 一律返回 {ok:false, state, reason, stdout:'', stderr:'', exitCode:null, writes:[]} 且**绝不抛出**。
|
|
28
|
+
//
|
|
29
|
+
// 降级点(如实声明,见 docs/t6-report.md):
|
|
30
|
+
// * containerAvailable() 是同步方法(契约签名),ESM 懒加载只能是异步的:探测结果缓存在 probeCache,
|
|
31
|
+
// apply 时异步预热(container.enabled=true 才预热),真正执行/探针走 async probeContainer()。
|
|
32
|
+
// 在预热完成前,enabled 的实例返回 state='unavailable', reason='not-probed'(fail-closed,不假装 ready)。
|
|
33
|
+
// * v1 的 Config.containerAvailable=true 是“操作者断言”,保留为路由可用(state='ready'),但真正
|
|
34
|
+
// runInContainer 仍要求 ../lib/container 模块存在,否则如实返回 unavailable。
|
|
35
|
+
import { AUDIT, CONTAINER, ROLE, ROUTE, SERVICE, z, roleGate } from '../kernel/index.mjs'
|
|
36
|
+
import { scanSource, importsToSignals } from './imports.mjs'
|
|
37
|
+
|
|
38
|
+
export const name = 'fang-router'
|
|
39
|
+
export const inject = []
|
|
40
|
+
|
|
41
|
+
/** 路由秩:只允许 confirm 往上抬。 */
|
|
42
|
+
const RANK = { [ROUTE.SANDBOX]: 1, [ROUTE.CONTAINER]: 2, [ROUTE.DENY]: 3 }
|
|
43
|
+
const GRADE_RANK = { A: 1, B: 2, C: 3, D: 4, E: 5 }
|
|
44
|
+
const CONTAINER_STATES = new Set([CONTAINER.DISABLED, CONTAINER.UNAVAILABLE, CONTAINER.READY])
|
|
45
|
+
|
|
46
|
+
export const Config = z.object({
|
|
47
|
+
containerAvailable: z.boolean().default(false)
|
|
48
|
+
.description('操作者断言容器执行体可用(v1 兼容;state=ready/reason=configured)'),
|
|
49
|
+
trustedPlugins: z.array(z.string()).default([])
|
|
50
|
+
.description('受信插件名单(名字或包名;命中即视为可信来源)'),
|
|
51
|
+
container: z.object({
|
|
52
|
+
enabled: z.boolean().default(false)
|
|
53
|
+
.description('P5 容器分支:true 才懒加载 ../lib/container/index.mjs 并探测 AppContainer 执行体'),
|
|
54
|
+
powershellPath: z.string().default('powershell.exe'),
|
|
55
|
+
timeoutMs: z.number().default(30000),
|
|
56
|
+
}).default({}),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const TRUSTED_TOKENS = new Set(['trusted', 'first-party', 'local', 'official', 'mother'])
|
|
60
|
+
const UNTRUSTED_TOKENS = new Set(['untrusted', 'third-party', 'external'])
|
|
61
|
+
|
|
62
|
+
/** 角色/环境名归一化。 */
|
|
63
|
+
export function normalizeEnvironment(value, fallback = ROLE.MOTHER) {
|
|
64
|
+
const v = String(value ?? '').toLowerCase()
|
|
65
|
+
return v === ROLE.MOTHER || v === ROLE.FORK ? v : fallback
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 来源描述归一化:字符串 | 对象 → {trusted, declaredWrites, bareWrites, name, via}。 */
|
|
69
|
+
export function normalizeSource(source, trustedPlugins = []) {
|
|
70
|
+
const list = Array.isArray(trustedPlugins) ? trustedPlugins.map(String) : []
|
|
71
|
+
|
|
72
|
+
if (typeof source === 'string') {
|
|
73
|
+
const raw = source.trim()
|
|
74
|
+
const low = raw.toLowerCase()
|
|
75
|
+
const listed = list.some(n => n === raw || n.toLowerCase() === low)
|
|
76
|
+
if (listed) return { name: raw, trusted: true, declaredWrites: false, bareWrites: false, via: 'trustedPlugins' }
|
|
77
|
+
if (TRUSTED_TOKENS.has(low)) return { name: raw, trusted: true, declaredWrites: false, bareWrites: false, via: 'trust-token' }
|
|
78
|
+
return { name: raw, trusted: false, declaredWrites: false, bareWrites: false, via: UNTRUSTED_TOKENS.has(low) ? 'untrusted-token' : 'unlisted-untrusted' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!source || typeof source !== 'object') {
|
|
82
|
+
return { name: null, trusted: false, declaredWrites: false, bareWrites: false, via: 'missing-untrusted' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rawName = source.name ?? source.plugin ?? source.id
|
|
86
|
+
const name = typeof rawName === 'string' && rawName.trim() !== '' ? rawName : null
|
|
87
|
+
const token = String(source.trust ?? source.trustLevel ?? source.origin ?? source.kind ?? '').toLowerCase()
|
|
88
|
+
const trusted = source.trusted === true
|
|
89
|
+
|| TRUSTED_TOKENS.has(token)
|
|
90
|
+
|| (name !== null && list.includes(name))
|
|
91
|
+
|| (typeof source.id === 'string' && list.includes(source.id))
|
|
92
|
+
const via = source.trusted === true
|
|
93
|
+
? (typeof source.via === 'string' && source.via !== '' ? source.via : 'explicit')
|
|
94
|
+
: TRUSTED_TOKENS.has(token) ? 'trust-token'
|
|
95
|
+
: (name !== null && list.includes(name)) ? 'trustedPlugins'
|
|
96
|
+
: 'untrusted'
|
|
97
|
+
|
|
98
|
+
// 申报制(见《申报制落地-未申报不许装载.md》):path + 非空 reason 才算申报。
|
|
99
|
+
const declaredRaw = source.declaredWrites ?? source.writesDeclared ?? source.declared
|
|
100
|
+
let declaredWrites = false
|
|
101
|
+
let invalidDeclaration = false
|
|
102
|
+
if (Array.isArray(declaredRaw)) {
|
|
103
|
+
declaredWrites = declaredRaw.some(d => d && typeof d.path === 'string' && String(d.reason ?? '').trim() !== '')
|
|
104
|
+
invalidDeclaration = declaredRaw.length > 0 && !declaredWrites
|
|
105
|
+
} else {
|
|
106
|
+
declaredWrites = declaredRaw === true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const bareRaw = source.bareWrites ?? source.bareWrite ?? source.hasBareWrites ?? source.undeclaredWrites
|
|
110
|
+
let bareWrites
|
|
111
|
+
if (Array.isArray(bareRaw)) bareWrites = bareRaw.length > 0
|
|
112
|
+
else if (typeof bareRaw === 'number') bareWrites = bareRaw > 0
|
|
113
|
+
else bareWrites = bareRaw === true
|
|
114
|
+
// 申报无效(有写但 reason 全空)视为未申报残留 → 走容器(fail-closed)。
|
|
115
|
+
if (invalidDeclaration) bareWrites = true
|
|
116
|
+
|
|
117
|
+
return { name, trusted, declaredWrites, bareWrites, via }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* decide() 的来源视图:`source` 是可信度权威(字符串/对象),`plugin` 只补充身份与申报/能力证据。
|
|
122
|
+
* 两者都给时 source 决定 trusted,plugin 补充 name/declaredWrites/bareWrites。
|
|
123
|
+
*/
|
|
124
|
+
export function mergeSourceInput(plugin, source, trustedPlugins = []) {
|
|
125
|
+
const hasExplicit = source !== undefined && source !== null
|
|
126
|
+
const explicit = hasExplicit ? normalizeSource(source, trustedPlugins) : null
|
|
127
|
+
const fromPlugin = plugin !== undefined && plugin !== null
|
|
128
|
+
? normalizeSource(plugin, trustedPlugins)
|
|
129
|
+
: null
|
|
130
|
+
if (explicit && fromPlugin) {
|
|
131
|
+
return {
|
|
132
|
+
// plugin 是身份(name),source 是可信度权威(trusted/via);申报/裸写证据取并集。
|
|
133
|
+
name: fromPlugin.name ?? explicit.name,
|
|
134
|
+
trusted: explicit.trusted,
|
|
135
|
+
via: explicit.via,
|
|
136
|
+
declaredWrites: explicit.declaredWrites || fromPlugin.declaredWrites,
|
|
137
|
+
bareWrites: explicit.bareWrites || fromPlugin.bareWrites,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return explicit ?? fromPlugin ?? normalizeSource(null, trustedPlugins)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 容器可用性归一化:单次输入 > 探针。 */
|
|
144
|
+
export function resolveContainer(input = {}, probe) {
|
|
145
|
+
const explicit = input?.containerAvailable
|
|
146
|
+
if (typeof explicit === 'boolean') {
|
|
147
|
+
return { ok: explicit, reason: explicit ? 'caller-asserted' : 'caller-denied' }
|
|
148
|
+
}
|
|
149
|
+
if (explicit && typeof explicit === 'object' && typeof explicit.ok === 'boolean') {
|
|
150
|
+
const out = { ok: explicit.ok, reason: explicit.reason ?? (explicit.ok ? 'caller-asserted' : 'caller-denied') }
|
|
151
|
+
if (CONTAINER_STATES.has(explicit.state)) out.state = explicit.state
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
const probed = typeof probe === 'function' ? probe() : { ok: false, state: CONTAINER.UNAVAILABLE, reason: 'no-probe' }
|
|
155
|
+
return probed && typeof probed === 'object' ? probed : { ok: false, state: CONTAINER.UNAVAILABLE, reason: 'no-probe' }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** assessment.signals 里的 S1 族 → 需要子进程执行体。 */
|
|
159
|
+
function assessmentNeedsChild(assessment) {
|
|
160
|
+
const signals = Array.isArray(assessment?.signals) ? assessment.signals : []
|
|
161
|
+
return signals.some(s => String(s?.id ?? '').split('-')[0] === 'S1')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 静态 quickScan(母体初判的“静态部分”,可独立测试):
|
|
166
|
+
* * capabilities 来自 assessment.signals 的 S1–S4 族,以及 plugin.capabilities/plugin.needs 的申报;
|
|
167
|
+
* * 调用方给了 code(源码文本)时现场跑 import 层扫描器,不依赖 scorer 已评分。
|
|
168
|
+
* 不做调用层分析(与 scorer 的 import-only 局限一致)。
|
|
169
|
+
*/
|
|
170
|
+
export function quickScan({ plugin = null, assessment = null, source = null, code = null } = {}) {
|
|
171
|
+
const signals = []
|
|
172
|
+
const warnings = []
|
|
173
|
+
let scanned = false
|
|
174
|
+
|
|
175
|
+
if (typeof code === 'string' && code.trim() !== '') {
|
|
176
|
+
try {
|
|
177
|
+
const scan = scanSource(code)
|
|
178
|
+
const mapped = importsToSignals(scan, {})
|
|
179
|
+
signals.push(...mapped.signals)
|
|
180
|
+
warnings.push(...scan.warnings.map(w => w.reason ?? 'warning'))
|
|
181
|
+
scanned = true
|
|
182
|
+
} catch (e) {
|
|
183
|
+
warnings.push(`scan-error:${e?.message ?? e}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const assessmentSignals = Array.isArray(assessment?.signals) ? assessment.signals : []
|
|
187
|
+
if (assessmentSignals.length > 0) {
|
|
188
|
+
signals.push(...assessmentSignals)
|
|
189
|
+
scanned = true
|
|
190
|
+
}
|
|
191
|
+
const declared = [
|
|
192
|
+
...(Array.isArray(plugin?.capabilities) ? plugin.capabilities : []),
|
|
193
|
+
...(Array.isArray(plugin?.needs) ? plugin.needs : []),
|
|
194
|
+
...(Array.isArray(source?.capabilities) ? source.capabilities : []),
|
|
195
|
+
].map(v => String(v ?? '').toLowerCase())
|
|
196
|
+
|
|
197
|
+
const caps = new Set()
|
|
198
|
+
for (const sig of signals) {
|
|
199
|
+
const fam = String(sig?.id ?? '').split('-')[0]
|
|
200
|
+
if (fam === 'S1') caps.add('child-process')
|
|
201
|
+
else if (fam === 'S2') caps.add('network')
|
|
202
|
+
else if (fam === 'S3') caps.add('fs')
|
|
203
|
+
else if (fam === 'S4') caps.add('vm')
|
|
204
|
+
}
|
|
205
|
+
const declaresChild = declared.some(c => c === 'child-process' || c === 'childprocess' || c === 'child_process')
|
|
206
|
+
|| plugin?.needsChildProcess === true
|
|
207
|
+
if (declaresChild) caps.add('child-process')
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
grade: typeof assessment?.grade === 'string' ? assessment.grade : null,
|
|
211
|
+
veto: assessment?.veto === true,
|
|
212
|
+
signals: signals.length,
|
|
213
|
+
capabilities: [...caps].sort(),
|
|
214
|
+
needsChildProcess: caps.has('child-process'),
|
|
215
|
+
coverage: scanned ? 'import-only' : 'none',
|
|
216
|
+
warnings,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 纯决策函数(decide/confirm 的数学部分,可独立测试)。
|
|
222
|
+
* @returns {{route:'sandbox'|'container'|'deny', reason:string, needsApproval:boolean,
|
|
223
|
+
* grade:string|null, veto:boolean, source:object, container:object, basis:object}}
|
|
224
|
+
*/
|
|
225
|
+
export function decidePure(input = {}, { config = {}, probe, role = ROLE.MOTHER, environment = null } = {}) {
|
|
226
|
+
const safeInput = input && typeof input === 'object' ? input : {}
|
|
227
|
+
const assessment = safeInput.assessment && typeof safeInput.assessment === 'object' ? safeInput.assessment : null
|
|
228
|
+
const src = normalizeSource(safeInput.source, config?.trustedPlugins ?? [])
|
|
229
|
+
const needsChild = safeInput.needsChildProcess === true || assessmentNeedsChild(assessment)
|
|
230
|
+
const container = resolveContainer(safeInput, probe)
|
|
231
|
+
const veto = assessment?.veto === true
|
|
232
|
+
const roleName = normalizeEnvironment(role)
|
|
233
|
+
const envName = normalizeEnvironment(environment ?? roleName)
|
|
234
|
+
const grade = typeof assessment?.grade === 'string' ? assessment.grade : null
|
|
235
|
+
|
|
236
|
+
const sourceSummary = {
|
|
237
|
+
trusted: src.trusted,
|
|
238
|
+
name: src.name,
|
|
239
|
+
declaredWrites: src.declaredWrites,
|
|
240
|
+
bareWrites: src.bareWrites,
|
|
241
|
+
via: src.via,
|
|
242
|
+
needsChildProcess: needsChild,
|
|
243
|
+
}
|
|
244
|
+
const containerView = {
|
|
245
|
+
ok: container?.ok === true,
|
|
246
|
+
reason: String(container?.reason ?? (container?.ok === true ? 'ok' : 'unavailable')),
|
|
247
|
+
}
|
|
248
|
+
if (CONTAINER_STATES.has(container?.state)) containerView.state = container.state
|
|
249
|
+
|
|
250
|
+
const base = {
|
|
251
|
+
grade,
|
|
252
|
+
veto,
|
|
253
|
+
source: sourceSummary,
|
|
254
|
+
container: containerView,
|
|
255
|
+
basis: { role: roleName, environment: envName, grade, source: sourceSummary },
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 1) 静态硬否决:dsh.hotReload === false。终局,任何路线都不放行。
|
|
259
|
+
if (veto) {
|
|
260
|
+
const why = Array.isArray(assessment?.reasons) && assessment.reasons.length > 0 ? assessment.reasons.join(',') : 'hotReload-false'
|
|
261
|
+
return { route: ROUTE.DENY, reason: `veto:${why}`, needsApproval: false, ...base }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 2) 不可信来源
|
|
265
|
+
if (!src.trusted) {
|
|
266
|
+
if (needsChild) {
|
|
267
|
+
return { route: ROUTE.DENY, reason: 'untrusted-needs-child-process', needsApproval: false, ...base }
|
|
268
|
+
}
|
|
269
|
+
if (containerView.ok) {
|
|
270
|
+
return { route: ROUTE.CONTAINER, reason: 'untrusted-source', needsApproval: true, ...base }
|
|
271
|
+
}
|
|
272
|
+
return { route: ROUTE.DENY, reason: `container-unavailable:${containerView.reason}`, needsApproval: false, ...base }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3) 可信来源
|
|
276
|
+
if (src.bareWrites) {
|
|
277
|
+
if (containerView.ok) {
|
|
278
|
+
return { route: ROUTE.CONTAINER, reason: 'trusted-bare-writes', needsApproval: true, ...base }
|
|
279
|
+
}
|
|
280
|
+
return { route: ROUTE.DENY, reason: `container-unavailable:${containerView.reason}`, needsApproval: false, ...base }
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
route: ROUTE.SANDBOX,
|
|
284
|
+
reason: src.declaredWrites ? 'trusted-declared-writes' : 'trusted-clean',
|
|
285
|
+
needsApproval: false,
|
|
286
|
+
...base,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** confirm 的证据来源:母体 decision 里透传的来源视图(v2 basis.source,v1 退化到 decision.source)。 */
|
|
291
|
+
export function evidenceFrom(decision) {
|
|
292
|
+
const summary = decision?.basis?.source ?? decision?.source ?? null
|
|
293
|
+
if (!summary || typeof summary !== 'object') return null
|
|
294
|
+
const name = typeof summary.name === 'string' && summary.name.trim() !== '' ? summary.name : null
|
|
295
|
+
return {
|
|
296
|
+
name,
|
|
297
|
+
trusted: summary.trusted === true,
|
|
298
|
+
declaredWrites: summary.declaredWrites === true,
|
|
299
|
+
bareWrites: summary.bareWrites === true,
|
|
300
|
+
needsChildProcess: summary.needsChildProcess === true,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 容器错误归类(永不抛)。 */
|
|
305
|
+
function classifyContainerError(err) {
|
|
306
|
+
const code = String(err?.code ?? '')
|
|
307
|
+
const message = String(err?.message ?? err ?? '')
|
|
308
|
+
if (code === 'CONTAINER_MODULE_INVALID') return { state: CONTAINER.UNAVAILABLE, reason: 'container-module-invalid' }
|
|
309
|
+
if (code === 'ERR_MODULE_NOT_FOUND' || /cannot find (module|package)/i.test(message)) {
|
|
310
|
+
return { state: CONTAINER.UNAVAILABLE, reason: 'container-module-missing' }
|
|
311
|
+
}
|
|
312
|
+
return { state: CONTAINER.UNAVAILABLE, reason: `container-module-load-failed:${message}` }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function stateOf(avail, host = null) {
|
|
316
|
+
const raw = avail?.state ?? host?.state
|
|
317
|
+
if (CONTAINER_STATES.has(raw)) return raw
|
|
318
|
+
return avail?.ok === true ? CONTAINER.READY : CONTAINER.UNAVAILABLE
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function readAvailable(host) {
|
|
322
|
+
try {
|
|
323
|
+
const avail = typeof host?.available === 'function' ? host.available() : null
|
|
324
|
+
if (avail && typeof avail === 'object') return avail
|
|
325
|
+
return { ok: host?.state === CONTAINER.READY, state: host?.state, reason: host?.state ?? 'no-probe' }
|
|
326
|
+
} catch (e) {
|
|
327
|
+
return { ok: false, state: CONTAINER.UNAVAILABLE, reason: `container-probe-failed:${e?.message ?? e}` }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─────────────────────── 容器模块懒加载(进程级缓存的只有“模块”本身) ───────────────────────
|
|
332
|
+
let containerModuleLoader = () => import('../lib/container/index.mjs')
|
|
333
|
+
let containerModulePromise = null
|
|
334
|
+
|
|
335
|
+
async function loadContainerModule() {
|
|
336
|
+
if (!containerModulePromise) {
|
|
337
|
+
containerModulePromise = Promise.resolve()
|
|
338
|
+
.then(() => containerModuleLoader())
|
|
339
|
+
.catch((e) => {
|
|
340
|
+
containerModulePromise = null // 下次重试(T8 模块可能在运行时才落地)
|
|
341
|
+
throw e
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
return containerModulePromise
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ─────────────────────────────── plugin ───────────────────────────────
|
|
348
|
+
|
|
349
|
+
export function apply(ctx, config) {
|
|
350
|
+
const gate = roleGate('router', { cfgRole: config?.role, logger: ctx.logger })
|
|
351
|
+
// v2 部署矩阵:router 在 mother/fork 都激活;这里的 return 只是守住“未部署角色不得注册服务”的契约。
|
|
352
|
+
if (!gate.active) return
|
|
353
|
+
|
|
354
|
+
const cfg = Config(config ?? {})
|
|
355
|
+
const normalized = {
|
|
356
|
+
containerAvailable: cfg.containerAvailable === true,
|
|
357
|
+
trustedPlugins: Array.isArray(cfg.trustedPlugins) ? cfg.trustedPlugins.map(String) : [],
|
|
358
|
+
container: {
|
|
359
|
+
enabled: cfg.container?.enabled === true,
|
|
360
|
+
powershellPath: typeof cfg.container?.powershellPath === 'string' ? cfg.container.powershellPath : 'powershell.exe',
|
|
361
|
+
timeoutMs: Number(cfg.container?.timeoutMs ?? 30000),
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
const role = gate.role
|
|
365
|
+
|
|
366
|
+
const safeGet = (key) => {
|
|
367
|
+
try { return ctx?.get?.(key) } catch { return undefined }
|
|
368
|
+
}
|
|
369
|
+
const audit = (data) => {
|
|
370
|
+
try { safeGet(SERVICE.auditor)?.append?.(AUDIT.ROUTE, { component: name, ...data }) } catch { /* 审计缺失不得影响路由 */ }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── 容器执行体:懒加载 + 可用性缓存(同步 API 只能读缓存,异步探测在 probeContainer/runInContainer 里刷新)──
|
|
374
|
+
let containerHost = null
|
|
375
|
+
let containerHostPromise = null
|
|
376
|
+
let disposed = false
|
|
377
|
+
let probeCache = { ok: false, state: CONTAINER.UNAVAILABLE, reason: 'not-probed' }
|
|
378
|
+
|
|
379
|
+
async function createHostOnce() {
|
|
380
|
+
const mod = await loadContainerModule()
|
|
381
|
+
if (typeof mod?.createContainerHost !== 'function') {
|
|
382
|
+
const err = new Error('container module does not export createContainerHost')
|
|
383
|
+
err.code = 'CONTAINER_MODULE_INVALID'
|
|
384
|
+
throw err
|
|
385
|
+
}
|
|
386
|
+
if (disposed) {
|
|
387
|
+
const err = new Error('router disposed')
|
|
388
|
+
err.code = 'ROUTER_DISPOSED'
|
|
389
|
+
throw err
|
|
390
|
+
}
|
|
391
|
+
containerHost = mod.createContainerHost({
|
|
392
|
+
powershellPath: normalized.container.powershellPath,
|
|
393
|
+
timeoutMs: normalized.container.timeoutMs,
|
|
394
|
+
})
|
|
395
|
+
return containerHost
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function ensureContainerHost() {
|
|
399
|
+
if (containerHost) return containerHost
|
|
400
|
+
if (!containerHostPromise) {
|
|
401
|
+
containerHostPromise = createHostOnce().catch((e) => {
|
|
402
|
+
containerHostPromise = null // 失败后允许下一次重试
|
|
403
|
+
throw e
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
return containerHostPromise
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function disposeContainerHost() {
|
|
410
|
+
disposed = true
|
|
411
|
+
containerHostPromise = null
|
|
412
|
+
try { containerHost?.dispose?.() } catch { /* 容器宿主清理失败不得影响卸载 */ }
|
|
413
|
+
containerHost = null
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** 同步可用性(契约 5.3):disabled / unavailable / ready。 */
|
|
417
|
+
function containerAvailable() {
|
|
418
|
+
if (normalized.containerAvailable) {
|
|
419
|
+
return { ok: true, state: CONTAINER.READY, reason: 'configured' }
|
|
420
|
+
}
|
|
421
|
+
if (!normalized.container.enabled) {
|
|
422
|
+
return { ok: false, state: CONTAINER.DISABLED, reason: 'container-disabled' }
|
|
423
|
+
}
|
|
424
|
+
if (probeCache.ok && probeCache.state === CONTAINER.READY) {
|
|
425
|
+
return { ok: true, state: CONTAINER.READY, reason: probeCache.reason }
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
state: probeCache.state === CONTAINER.DISABLED ? CONTAINER.DISABLED : CONTAINER.UNAVAILABLE,
|
|
430
|
+
reason: probeCache.reason ?? 'not-probed',
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** 异步刷新容器可用性(apply 预热 / probeContainer / runInContainer 共用)。永不抛出。 */
|
|
435
|
+
async function refreshContainer() {
|
|
436
|
+
if (normalized.containerAvailable) {
|
|
437
|
+
probeCache = { ok: true, state: CONTAINER.READY, reason: 'configured' }
|
|
438
|
+
return containerAvailable()
|
|
439
|
+
}
|
|
440
|
+
if (!normalized.container.enabled) {
|
|
441
|
+
probeCache = { ok: false, state: CONTAINER.DISABLED, reason: 'container-disabled' }
|
|
442
|
+
return containerAvailable()
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const host = await ensureContainerHost()
|
|
446
|
+
const avail = readAvailable(host)
|
|
447
|
+
const state = stateOf(avail, host)
|
|
448
|
+
probeCache = {
|
|
449
|
+
ok: avail?.ok === true && state === CONTAINER.READY,
|
|
450
|
+
state,
|
|
451
|
+
reason: String(avail?.reason ?? (state === CONTAINER.READY ? 'ready' : 'unavailable')),
|
|
452
|
+
}
|
|
453
|
+
} catch (e) {
|
|
454
|
+
probeCache = { ok: false, ...classifyContainerError(e) }
|
|
455
|
+
}
|
|
456
|
+
return containerAvailable()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** 容器内执行(契约 5.3):{ok, stdout, stderr, exitCode, writes},任何失败路径都不抛出。 */
|
|
460
|
+
async function runInContainer(input = {}) {
|
|
461
|
+
const req = input && typeof input === 'object' ? input : {}
|
|
462
|
+
const code = typeof req.code === 'string' ? req.code : ''
|
|
463
|
+
const env = req.env && typeof req.env === 'object' ? req.env : {}
|
|
464
|
+
const declaredWrites = Array.isArray(req.declaredWrites) ? req.declaredWrites : []
|
|
465
|
+
const empty = { stdout: '', stderr: '', exitCode: null, writes: [] }
|
|
466
|
+
|
|
467
|
+
const enabled = normalized.container.enabled || normalized.containerAvailable
|
|
468
|
+
if (!enabled) return { ok: false, state: CONTAINER.DISABLED, reason: 'container-disabled', ...empty }
|
|
469
|
+
if (code.trim() === '') {
|
|
470
|
+
return { ok: false, state: containerAvailable().state, reason: 'code-required', ...empty }
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 先确保宿主存在(懒加载;模块缺失在这里被归类,不抛)。
|
|
474
|
+
try {
|
|
475
|
+
await ensureContainerHost()
|
|
476
|
+
} catch (e) {
|
|
477
|
+
const classified = classifyContainerError(e)
|
|
478
|
+
probeCache = { ok: false, ...classified }
|
|
479
|
+
return { ok: false, ...classified, ...empty }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let avail = readAvailable(containerHost)
|
|
483
|
+
let state = stateOf(avail, containerHost)
|
|
484
|
+
if (!(avail?.ok === true && state === CONTAINER.READY) && normalized.container.enabled) {
|
|
485
|
+
avail = await refreshContainer()
|
|
486
|
+
state = avail.state
|
|
487
|
+
}
|
|
488
|
+
if (!containerHost || !(avail?.ok === true)) {
|
|
489
|
+
const why = String(avail?.reason ?? 'container-unavailable')
|
|
490
|
+
return { ok: false, state: state ?? CONTAINER.UNAVAILABLE, reason: why, ...empty }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const out = await containerHost.exec({
|
|
495
|
+
code,
|
|
496
|
+
env,
|
|
497
|
+
declaredWrites,
|
|
498
|
+
timeoutMs: normalized.container.timeoutMs,
|
|
499
|
+
})
|
|
500
|
+
probeCache = { ok: true, state: CONTAINER.READY, reason: probeCache.reason === 'not-probed' ? 'exec-ok' : probeCache.reason }
|
|
501
|
+
return {
|
|
502
|
+
ok: out?.ok === true,
|
|
503
|
+
state: CONTAINER.READY,
|
|
504
|
+
stdout: String(out?.stdout ?? ''),
|
|
505
|
+
stderr: String(out?.stderr ?? ''),
|
|
506
|
+
exitCode: typeof out?.exitCode === 'number' ? out.exitCode : (out?.exitCode ?? null),
|
|
507
|
+
writes: Array.isArray(out?.writes) ? out.writes : [],
|
|
508
|
+
...(out?.ok === true ? {} : { reason: String(out?.reason ?? out?.error ?? 'exec-failed') }),
|
|
509
|
+
}
|
|
510
|
+
} catch (e) {
|
|
511
|
+
return {
|
|
512
|
+
ok: false,
|
|
513
|
+
state: containerAvailable().state,
|
|
514
|
+
reason: `container-exec-error:${e?.message ?? e}`,
|
|
515
|
+
...empty,
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── 母体初判 ──────────────────────────────────────────────────────────
|
|
521
|
+
function decide(input = {}) {
|
|
522
|
+
const raw = input && typeof input === 'object' ? input : {}
|
|
523
|
+
const qs = quickScan({ plugin: raw.plugin, assessment: raw.assessment, source: raw.source, code: raw.code })
|
|
524
|
+
const sourceView = mergeSourceInput(raw.plugin, raw.source, normalized.trustedPlugins)
|
|
525
|
+
const out = decidePure(
|
|
526
|
+
{ ...raw, source: sourceView, needsChildProcess: raw.needsChildProcess === true || qs.needsChildProcess },
|
|
527
|
+
{ config: normalized, probe: containerAvailable, role, environment: normalizeEnvironment(raw.environment, role) },
|
|
528
|
+
)
|
|
529
|
+
out.quickScan = qs
|
|
530
|
+
audit({
|
|
531
|
+
phase: 'decide',
|
|
532
|
+
route: out.route,
|
|
533
|
+
reason: out.reason,
|
|
534
|
+
needsApproval: out.needsApproval,
|
|
535
|
+
grade: out.grade,
|
|
536
|
+
role,
|
|
537
|
+
environment: out.basis.environment,
|
|
538
|
+
sourceTrusted: out.source.trusted,
|
|
539
|
+
sourceName: out.source.name,
|
|
540
|
+
capabilities: qs.capabilities,
|
|
541
|
+
container: out.container,
|
|
542
|
+
})
|
|
543
|
+
return out
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── 分身复核 ──────────────────────────────────────────────────────────
|
|
547
|
+
function confirm(input = {}) {
|
|
548
|
+
const raw = input && typeof input === 'object' ? input : {}
|
|
549
|
+
const decision = raw.decision && typeof raw.decision === 'object' ? raw.decision : null
|
|
550
|
+
const environment = normalizeEnvironment(raw.environment, role)
|
|
551
|
+
|
|
552
|
+
if (!decision || !RANK[decision.route]) {
|
|
553
|
+
const out = {
|
|
554
|
+
route: ROUTE.DENY,
|
|
555
|
+
reason: `confirm:${decision ? 'invalid-decision-route' : 'missing-decision'}`,
|
|
556
|
+
needsApproval: false,
|
|
557
|
+
escalated: false,
|
|
558
|
+
grade: null,
|
|
559
|
+
basis: { role, environment, grade: null, source: null, escalated: false },
|
|
560
|
+
}
|
|
561
|
+
audit({ phase: 'confirm', route: out.route, reason: out.reason, escalated: false, role, environment })
|
|
562
|
+
return out
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 母体 deny 是终局:原样返回 route/reason/needsApproval,只补复核环境的 basis 与 escalated:false。
|
|
566
|
+
if (decision.route === ROUTE.DENY) {
|
|
567
|
+
const out = {
|
|
568
|
+
...decision,
|
|
569
|
+
escalated: false,
|
|
570
|
+
basis: {
|
|
571
|
+
...(decision.basis ?? {}),
|
|
572
|
+
role,
|
|
573
|
+
environment,
|
|
574
|
+
grade: decision.basis?.grade ?? decision.grade ?? null,
|
|
575
|
+
escalated: false,
|
|
576
|
+
},
|
|
577
|
+
}
|
|
578
|
+
audit({ phase: 'confirm', route: out.route, reason: out.reason, escalated: false, role, environment, from: decision.route })
|
|
579
|
+
return out
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const evidence = evidenceFrom(decision) ?? mergeSourceInput(raw.plugin, raw.source, normalized.trustedPlugins)
|
|
583
|
+
const qs = quickScan({ plugin: raw.plugin, assessment: raw.assessment, source: evidence, code: raw.code })
|
|
584
|
+
const forkAdvice = decidePure(
|
|
585
|
+
{
|
|
586
|
+
assessment: raw.assessment ?? null,
|
|
587
|
+
source: evidence,
|
|
588
|
+
needsChildProcess: evidence?.needsChildProcess === true || qs.needsChildProcess,
|
|
589
|
+
},
|
|
590
|
+
{ config: normalized, probe: containerAvailable, role, environment },
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
// max(母体 decision, 分身建议):只升不降。
|
|
594
|
+
let route = decision.route
|
|
595
|
+
const upgrades = []
|
|
596
|
+
if ((RANK[forkAdvice.route] ?? 0) > (RANK[route] ?? 0)) {
|
|
597
|
+
route = forkAdvice.route
|
|
598
|
+
upgrades.push(forkAdvice.reason)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 评分第二意见只做单向升级:E → deny;D 或“比母体更差” → 至少 container。
|
|
602
|
+
const forkGrade = typeof raw.assessment?.grade === 'string' ? raw.assessment.grade : (forkAdvice.grade ?? null)
|
|
603
|
+
const motherGrade = decision.basis?.grade ?? decision.grade ?? null
|
|
604
|
+
if (route !== ROUTE.DENY && forkGrade === 'E') {
|
|
605
|
+
route = ROUTE.DENY
|
|
606
|
+
upgrades.push('fork-grade-E')
|
|
607
|
+
} else if (route === ROUTE.SANDBOX && forkGrade) {
|
|
608
|
+
const worseThanMother = motherGrade !== null
|
|
609
|
+
&& typeof GRADE_RANK[forkGrade] === 'number'
|
|
610
|
+
&& GRADE_RANK[forkGrade] > (GRADE_RANK[motherGrade] ?? 0)
|
|
611
|
+
if (forkGrade === 'D' || worseThanMother) {
|
|
612
|
+
route = ROUTE.CONTAINER
|
|
613
|
+
upgrades.push(`fork-grade-worse:${motherGrade ?? 'unknown'}->${forkGrade}`)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const escalated = route !== decision.route
|
|
618
|
+
let needsApproval
|
|
619
|
+
if (route === ROUTE.CONTAINER) needsApproval = true
|
|
620
|
+
else if (route === ROUTE.DENY) needsApproval = false
|
|
621
|
+
else needsApproval = decision.needsApproval === true
|
|
622
|
+
if (escalated && route === ROUTE.DENY) needsApproval = false
|
|
623
|
+
|
|
624
|
+
const grade = forkGrade ?? decision.basis?.grade ?? decision.grade ?? null
|
|
625
|
+
const basis = {
|
|
626
|
+
role,
|
|
627
|
+
environment,
|
|
628
|
+
grade,
|
|
629
|
+
source: forkAdvice.basis?.source ?? decision.basis?.source ?? null,
|
|
630
|
+
escalated,
|
|
631
|
+
...(decision.basis?.role ? { decidedBy: decision.basis.role } : {}),
|
|
632
|
+
}
|
|
633
|
+
const out = {
|
|
634
|
+
route,
|
|
635
|
+
reason: escalated ? `escalated:${decision.route}->${route}:${upgrades.join('|') || forkAdvice.reason}` : decision.reason,
|
|
636
|
+
needsApproval,
|
|
637
|
+
escalated,
|
|
638
|
+
grade,
|
|
639
|
+
veto: decision.veto === true || forkAdvice.veto === true,
|
|
640
|
+
source: basis.source,
|
|
641
|
+
container: forkAdvice.container,
|
|
642
|
+
basis,
|
|
643
|
+
}
|
|
644
|
+
audit({
|
|
645
|
+
phase: 'confirm',
|
|
646
|
+
route: out.route,
|
|
647
|
+
reason: out.reason,
|
|
648
|
+
escalated,
|
|
649
|
+
from: decision.route,
|
|
650
|
+
role,
|
|
651
|
+
environment,
|
|
652
|
+
grade,
|
|
653
|
+
container: out.container,
|
|
654
|
+
})
|
|
655
|
+
return out
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const service = {
|
|
659
|
+
role,
|
|
660
|
+
policy: {
|
|
661
|
+
role,
|
|
662
|
+
environment: role,
|
|
663
|
+
platform: process.platform,
|
|
664
|
+
containerAvailable: normalized.containerAvailable,
|
|
665
|
+
container: { ...normalized.container },
|
|
666
|
+
trustedPlugins: normalized.trustedPlugins,
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
/** 容器执行体可用性(契约 5.3):{ok, state, reason}。 */
|
|
670
|
+
containerAvailable,
|
|
671
|
+
|
|
672
|
+
/** 异步刷新容器可用性(补充方法,供 Orchestrator/Adapter 在需要确定性状态时调用)。 */
|
|
673
|
+
async probeContainer() {
|
|
674
|
+
return refreshContainer()
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
/** 母体初判(契约 5.3):静态 quickScan + 来源可信度 + 能力需求。 */
|
|
678
|
+
decide,
|
|
679
|
+
|
|
680
|
+
/** 分身复核(契约 5.3):只升不降;母体 deny 原样返回。 */
|
|
681
|
+
confirm,
|
|
682
|
+
|
|
683
|
+
/** 容器内执行(契约 5.3):懒加载 T8 容器执行器,缺模块/禁用时降级返回,绝不抛。 */
|
|
684
|
+
runInContainer,
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
ctx.effect(() => () => disposeContainerHost())
|
|
688
|
+
// 预热探测:enabled 时才触发;异步失败已内部归类,不会产生未处理拒绝。
|
|
689
|
+
if (normalized.container.enabled) {
|
|
690
|
+
refreshContainer().catch(() => {})
|
|
691
|
+
}
|
|
692
|
+
ctx.effect(() => ctx.provide(SERVICE.router, service))
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export const _internals = {
|
|
696
|
+
normalizeSource,
|
|
697
|
+
mergeSourceInput,
|
|
698
|
+
resolveContainer,
|
|
699
|
+
decidePure,
|
|
700
|
+
quickScan,
|
|
701
|
+
evidenceFrom,
|
|
702
|
+
normalizeEnvironment,
|
|
703
|
+
RANK,
|
|
704
|
+
GRADE_RANK,
|
|
705
|
+
/** 测试注入容器模块加载器(T8 模块可能尚未落地);传 null 恢复真实懒加载。 */
|
|
706
|
+
setContainerModuleLoader(fn) {
|
|
707
|
+
containerModuleLoader = typeof fn === 'function' ? fn : () => import('../lib/container/index.mjs')
|
|
708
|
+
containerModulePromise = null
|
|
709
|
+
},
|
|
710
|
+
resetContainerModuleLoader() {
|
|
711
|
+
containerModuleLoader = () => import('../lib/container/index.mjs')
|
|
712
|
+
containerModulePromise = null
|
|
713
|
+
},
|
|
714
|
+
}
|