release-skill 0.2.2 → 0.2.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +4 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +27 -0
- package/INSTALL.md +95 -139
- package/INSTALL.zh-CN.md +70 -121
- package/README.md +265 -916
- package/README.zh-CN.md +222 -537
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +18710 -16694
- package/adapters/claude/schemas/release-plan.schema.json +137 -0
- package/adapters/claude/schemas/release-project.schema.json +93 -0
- package/adapters/claude/schemas/release-run.schema.json +70 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +18710 -16694
- package/adapters/codex/schemas/release-plan.schema.json +137 -0
- package/adapters/codex/schemas/release-project.schema.json +93 -0
- package/adapters/codex/schemas/release-run.schema.json +70 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +18710 -16694
- package/adapters/kimi/schemas/release-plan.schema.json +137 -0
- package/adapters/kimi/schemas/release-project.schema.json +93 -0
- package/adapters/kimi/schemas/release-run.schema.json +70 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +18710 -16694
- package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
- package/bin/release-skill.bundle.mjs +18710 -16694
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +137 -0
- package/schemas/release-project.schema.json +93 -0
- package/schemas/release-run.schema.json +70 -2
- package/src/adapters/plugin-marketplace.mjs +1602 -605
- package/src/commands/prepare.mjs +441 -32
- package/src/commands/publish.mjs +107 -75
- package/src/commands/reconcile.mjs +92 -327
- package/src/commands/setup.mjs +148 -20
- package/src/commands/verify.mjs +315 -25
- package/src/core/baseline.mjs +21 -1
- package/src/core/checkpoints.mjs +50 -7
- package/src/core/config.mjs +15 -0
- package/src/core/errors.mjs +2 -0
- package/src/core/installation-contract.mjs +341 -0
- package/src/core/plan.mjs +307 -6
- package/src/platforms/codebuddy.mjs +191 -238
- package/src/platforms/codex.mjs +369 -0
- package/src/platforms/kimi.mjs +164 -119
- package/src/platforms/registry.mjs +180 -4
- package/src/producers/build-adapters.mjs +9 -2
|
@@ -29,13 +29,17 @@ import {
|
|
|
29
29
|
writeEvidenceAtomic,
|
|
30
30
|
} from './contract.mjs';
|
|
31
31
|
|
|
32
|
+
import {
|
|
33
|
+
computeInstallationContractDigest,
|
|
34
|
+
INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
35
|
+
} from '../core/installation-contract.mjs';
|
|
36
|
+
|
|
32
37
|
import { createHash } from 'node:crypto';
|
|
33
38
|
import { computeFrozenSnapshot, resolveFrozenPath } from '../snapshot/frozen.mjs';
|
|
34
|
-
import { PLATFORMS, getPlatform } from '../platforms/registry.mjs';
|
|
39
|
+
import { PLATFORMS, getPlatform, resolvePlatformRoute, resolveCapabilityConflicts } from '../platforms/registry.mjs';
|
|
35
40
|
import {
|
|
36
41
|
KIMI_REQUIREMENT_FILE,
|
|
37
42
|
KIMI_ATTESTATION_FILE,
|
|
38
|
-
KIMI_MANAGED_SUBPATH,
|
|
39
43
|
resolveBoundPlanDigest,
|
|
40
44
|
kimiAuthorityDir,
|
|
41
45
|
validateKimiAttestation,
|
|
@@ -48,10 +52,15 @@ import {
|
|
|
48
52
|
CODEBUDDY_MARKETPLACE_SOURCE,
|
|
49
53
|
resolveCodeBuddyBoundPlanDigest,
|
|
50
54
|
codebuddyAuthorityDir,
|
|
51
|
-
codebuddyCliHome,
|
|
52
|
-
codebuddyCliInstallRoot,
|
|
53
55
|
validateCodeBuddyAttestation,
|
|
54
56
|
} from '../platforms/codebuddy.mjs';
|
|
57
|
+
import {
|
|
58
|
+
CODEX_REQUIREMENT_FILE,
|
|
59
|
+
CODEX_ATTESTATION_FILE,
|
|
60
|
+
resolveCodexBoundPlanDigest,
|
|
61
|
+
codexAuthorityDir,
|
|
62
|
+
validateCodexAttestation,
|
|
63
|
+
} from '../platforms/codex.mjs';
|
|
55
64
|
|
|
56
65
|
const execFile = promisify(execFileCb);
|
|
57
66
|
|
|
@@ -92,6 +101,9 @@ const EXTRA_INSTALLED_PATHS_CAP = 200;
|
|
|
92
101
|
/** Diagnostic cap: at most this many conflict paths are listed per error. */
|
|
93
102
|
const PAYLOAD_CONFLICT_REPORT_CAP = 10;
|
|
94
103
|
|
|
104
|
+
/** 消费端安装验证配方版本(与 prepare.mjs 一致)。 */
|
|
105
|
+
const CONSUMER_INSTALL_RECIPE_VERSION = 'consumer-install-v1';
|
|
106
|
+
|
|
95
107
|
// Consumer-owned transport metadata written into the plugin install root
|
|
96
108
|
// that is not part of the published payload (e.g. codex's root `.git`
|
|
97
109
|
// checkout and `.codex-plugin/migrated-command-skills/`, claude's `.in_use`
|
|
@@ -100,6 +112,58 @@ const PAYLOAD_CONFLICT_REPORT_CAP = 10;
|
|
|
100
112
|
// applies that list; declared-manifest-v1 verification never excludes
|
|
101
113
|
// anything — host-added files are recorded as `extraInstalledPaths` instead.
|
|
102
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the marketplace root directory within a snapshot.
|
|
117
|
+
*
|
|
118
|
+
* For bundled-family layouts, the marketplace root is the directory that
|
|
119
|
+
* contains the platform's `.claude-plugin/marketplace.json` (or equivalent).
|
|
120
|
+
* Entry `source` paths in the marketplace index are relative to this root.
|
|
121
|
+
*
|
|
122
|
+
* Resolution rules:
|
|
123
|
+
* 1. If no explicit marketplaceIndexPath is provided, the root is "." (root layout).
|
|
124
|
+
* 2. If marketplaceIndexPath equals the platform default, the root is ".".
|
|
125
|
+
* 3. If marketplaceIndexPath ends with "/" + platform default (exact suffix match),
|
|
126
|
+
* the prefix is the marketplace root.
|
|
127
|
+
* 4. Otherwise, throws: the path is not a valid marketplace index path.
|
|
128
|
+
*
|
|
129
|
+
* Uses exact suffix matching only; never uses string `includes` to guess.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} platform - Platform descriptor from the registry.
|
|
132
|
+
* @param {string} [marketplaceIndexPath] - Explicit marketplace index path override.
|
|
133
|
+
* @returns {string} Normalized marketplace root ("." for root layout).
|
|
134
|
+
* @throws {Error} If marketplaceIndexPath is provided but doesn't match expectations.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveMarketplaceRoot(platform, marketplaceIndexPath) {
|
|
137
|
+
const defaultMarketplace = platform.manifestPaths.marketplace;
|
|
138
|
+
if (!defaultMarketplace) {
|
|
139
|
+
// Platform has no marketplace (kimi) — root concept doesn't apply.
|
|
140
|
+
return '.';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!marketplaceIndexPath || marketplaceIndexPath === defaultMarketplace) {
|
|
144
|
+
// No override or same as default → root layout.
|
|
145
|
+
return '.';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Exact suffix match: marketplaceIndexPath must end with "/" + defaultMarketplace
|
|
149
|
+
const suffix = `/${defaultMarketplace}`;
|
|
150
|
+
if (marketplaceIndexPath.endsWith(suffix)) {
|
|
151
|
+
const root = marketplaceIndexPath.slice(0, -suffix.length);
|
|
152
|
+
if (root.length === 0) return '.';
|
|
153
|
+
// Validate root is a safe relative path (no escape)
|
|
154
|
+
if (root.startsWith('/') || root.includes('..') || root.includes('\\')) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`derived marketplace root "${root}" from marketplaceIndexPath "${marketplaceIndexPath}" is not a safe relative path`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return root;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw new Error(
|
|
163
|
+
`marketplaceIndexPath "${marketplaceIndexPath}" does not end with platform default marketplace path "${defaultMarketplace}"`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
103
167
|
/**
|
|
104
168
|
* Extract the marketplace plugin entry's declared source as a validated,
|
|
105
169
|
* normalized snapshot-relative subpath ("." for root layouts).
|
|
@@ -139,6 +203,226 @@ function extractDeclaredPluginSource(consumer, entry) {
|
|
|
139
203
|
return segments.length === 0 ? '.' : segments.join('/');
|
|
140
204
|
}
|
|
141
205
|
|
|
206
|
+
/**
|
|
207
|
+
* 从市场索引中解析插件根目录并读取插件 manifest。
|
|
208
|
+
*
|
|
209
|
+
* 单一权威来源解析逻辑:
|
|
210
|
+
* 1. 从 marketIndex.plugins 中按 pluginName 筛选唯一匹配条目
|
|
211
|
+
* 2. 使用 extractDeclaredPluginSource 安全解析条目 source 字段
|
|
212
|
+
* 3. 将 source 安全拼接到 marketplaceRoot(快照绝对路径)并做逃逸检查
|
|
213
|
+
* 4. 在解析后的插件根目录下读取 platform.manifestPaths.plugin
|
|
214
|
+
*
|
|
215
|
+
* Kimi(无市场)返回 null。其他平台在市场索引为空或条目不唯一时失败关闭。
|
|
216
|
+
*
|
|
217
|
+
* @param {object|null} marketIndex - 市场索引对象(null 表示 Kimi 无市场)
|
|
218
|
+
* @param {string} pluginName - 要查找的插件名称
|
|
219
|
+
* @param {object} platform - 平台注册表条目
|
|
220
|
+
* @param {string} snapshotDir - 快照目录绝对路径
|
|
221
|
+
* @param {string} [marketplaceRootRel] - 市场根相对路径("." 或如 "adapters/claude");
|
|
222
|
+
* 默认 "."(根布局)。条目 source 相对此路径解析,而非快照根。
|
|
223
|
+
* @returns {Promise<{manifest: object, manifestRelativePath: string, pluginRoot: string}|null>}
|
|
224
|
+
*/
|
|
225
|
+
export async function resolvePluginManifestFromMarketplaceEntrySource(
|
|
226
|
+
marketIndex,
|
|
227
|
+
pluginName,
|
|
228
|
+
platform,
|
|
229
|
+
snapshotDir,
|
|
230
|
+
marketplaceRootRel = '.',
|
|
231
|
+
) {
|
|
232
|
+
// Kimi 无市场索引:返回 null(调用方使用平台级 manifest 规则)
|
|
233
|
+
if (platform.manifestPaths.marketplace === null && !marketIndex) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!marketIndex || typeof marketIndex !== 'object') {
|
|
238
|
+
throw new Error('marketplace index is required for non-kimi platforms');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const plugins = Array.isArray(marketIndex.plugins) ? marketIndex.plugins : [];
|
|
242
|
+
const matches = plugins.filter((entry) => entry && entry.name === pluginName);
|
|
243
|
+
if (matches.length !== 1) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`marketplace index must contain exactly one plugin entry named "${pluginName}", found ${matches.length}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const entry = matches[0];
|
|
250
|
+
const sourcePath = extractDeclaredPluginSource(platform.id, entry);
|
|
251
|
+
|
|
252
|
+
// 安全拼接:source 路径已在 extractDeclaredPluginSource 中验证无逃逸。
|
|
253
|
+
// source 相对市场根解析(marketplaceRootRel),而非快照根。
|
|
254
|
+
// 这里再做 realpath 二次确认防止符号链接逃逸。
|
|
255
|
+
// macOS 的 /var -> /private/var 符号链接会导致 snapshotDir 与 realpath 不同,
|
|
256
|
+
// 因此必须先将 snapshotDir 解析为真实路径再做 containment 检查。
|
|
257
|
+
const snapshotDirReal = await realpath(snapshotDir).catch(() => snapshotDir);
|
|
258
|
+
const marketplaceRootAbs = marketplaceRootRel === '.'
|
|
259
|
+
? snapshotDirReal
|
|
260
|
+
: resolve(snapshotDirReal, marketplaceRootRel);
|
|
261
|
+
const pluginRootAbs = resolve(marketplaceRootAbs, sourcePath);
|
|
262
|
+
const pluginRootReal = await realpath(pluginRootAbs).catch(() => null);
|
|
263
|
+
if (!pluginRootReal) {
|
|
264
|
+
throw new Error(`marketplace plugin entry source "${sourcePath}" does not exist in snapshot`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 逃逸检查:解析后的插件根必须在快照目录内
|
|
268
|
+
const containment = relative(snapshotDirReal, pluginRootReal);
|
|
269
|
+
const sep = process.platform === 'win32' ? '\\' : '/';
|
|
270
|
+
if (
|
|
271
|
+
containment !== '' &&
|
|
272
|
+
(isAbsolute(containment) || containment === '..' || containment.startsWith(`..${sep}`))
|
|
273
|
+
) {
|
|
274
|
+
throw new Error(`marketplace plugin entry source "${sourcePath}" escapes the snapshot after symlink resolution`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 读取插件 manifest
|
|
278
|
+
const manifestRelative = platform.manifestPaths.plugin;
|
|
279
|
+
const manifestAbs = resolve(pluginRootReal, manifestRelative);
|
|
280
|
+
|
|
281
|
+
// 计算 manifestRelativePath:相对快照根的完整路径
|
|
282
|
+
const pluginRootRelToSnapshot = relative(snapshotDirReal, pluginRootReal);
|
|
283
|
+
const fullManifestRelative = pluginRootRelToSnapshot === ''
|
|
284
|
+
? manifestRelative
|
|
285
|
+
: `${pluginRootRelToSnapshot}/${manifestRelative}`;
|
|
286
|
+
|
|
287
|
+
let raw;
|
|
288
|
+
try {
|
|
289
|
+
raw = await readFile(manifestAbs, 'utf8');
|
|
290
|
+
} catch (err) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`plugin manifest not found at "${fullManifestRelative}": ${err.message}`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let manifest;
|
|
297
|
+
try {
|
|
298
|
+
manifest = JSON.parse(raw);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`plugin manifest at "${fullManifestRelative}" is not valid JSON: ${err.message}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
manifest,
|
|
307
|
+
manifestRelativePath: fullManifestRelative,
|
|
308
|
+
pluginRoot: pluginRootRelToSnapshot || '.',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 验证 installationContractDigest:从冻结快照读取插件 manifest,
|
|
314
|
+
* 按平台注册表和来源形态构建安装契约,以 consumer-install-v1 重算摘要并比对。
|
|
315
|
+
*
|
|
316
|
+
* @param {object} action - 展开后的 action(parameters 已展开到顶层)
|
|
317
|
+
* @param {string} snapshotDirReal - 冻结快照的 realpath
|
|
318
|
+
* @param {object} platform - 平台注册表条目
|
|
319
|
+
* @returns {Promise<{valid: boolean, error: string|null}>}
|
|
320
|
+
*/
|
|
321
|
+
async function validateInstallationContractDigest(action, snapshotDirReal, platform) {
|
|
322
|
+
const form = action.marketplaceSourceType;
|
|
323
|
+
|
|
324
|
+
// 1. 确定是否纳入市场条目,并获取条目(manifest 读取依赖条目 source 路径)
|
|
325
|
+
const hasMarketplacePath = platform.manifestPaths.marketplace !== null;
|
|
326
|
+
let includeMarketplaceEntry = false;
|
|
327
|
+
let selectedMarketplaceEntry = null;
|
|
328
|
+
let marketplaceIndexRelative = null;
|
|
329
|
+
let marketIndexParsed = null;
|
|
330
|
+
|
|
331
|
+
if (form === 'bundled-family') {
|
|
332
|
+
includeMarketplaceEntry = hasMarketplacePath;
|
|
333
|
+
if (includeMarketplaceEntry) {
|
|
334
|
+
// 从快照中读取 bundled 市场索引,提取唯一匹配条目。
|
|
335
|
+
// marketplaceIndexPath 若存在(嵌套布局),用作完整索引路径;
|
|
336
|
+
// 否则使用平台默认路径(根布局)。
|
|
337
|
+
marketplaceIndexRelative = action.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
|
|
338
|
+
const marketplacePath = resolve(snapshotDirReal, marketplaceIndexRelative);
|
|
339
|
+
const mkResult = await validateManifestFile(marketplacePath, ['name', 'plugins']);
|
|
340
|
+
if (!mkResult.valid) {
|
|
341
|
+
return { valid: false, error: `installationContractDigest 验证失败:市场索引读取失败:${mkResult.error}` };
|
|
342
|
+
}
|
|
343
|
+
const plugins = mkResult.manifest.plugins;
|
|
344
|
+
if (!Array.isArray(plugins)) {
|
|
345
|
+
return { valid: false, error: `installationContractDigest 验证失败:市场索引 plugins 非数组` };
|
|
346
|
+
}
|
|
347
|
+
const matches = plugins.filter((p) => p && p.name === action.plugin);
|
|
348
|
+
if (matches.length !== 1) {
|
|
349
|
+
return { valid: false, error: `installationContractDigest 验证失败:市场索引中匹配 "${action.plugin}" 的条目数量为 ${matches.length}` };
|
|
350
|
+
}
|
|
351
|
+
selectedMarketplaceEntry = matches[0];
|
|
352
|
+
marketIndexParsed = mkResult.manifest;
|
|
353
|
+
}
|
|
354
|
+
} else if (form === 'standalone-index') {
|
|
355
|
+
// 独立市场:Kimi 不纳入市场条目(Kimi 无市场 CLI,selectedEntry
|
|
356
|
+
// 仅供静态校验独立市场身份字段,不进入安装契约摘要)。
|
|
357
|
+
// Claude/Codex/CodeBuddy 使用 action 中的 selectedEntry 和 marketplaceIndexPath。
|
|
358
|
+
if (platform.id !== 'kimi' && action.selectedEntry && action.marketplaceIndexPath) {
|
|
359
|
+
includeMarketplaceEntry = true;
|
|
360
|
+
selectedMarketplaceEntry = action.selectedEntry;
|
|
361
|
+
marketplaceIndexRelative = action.marketplaceIndexPath;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 2. 读取插件 manifest:使用市场条目 source 路径解析(单一权威 helper)。
|
|
366
|
+
// bundled-family + 有市场索引:通过 resolvePluginManifestFromMarketplaceEntrySource
|
|
367
|
+
// 从条目 source 安全解析插件根并读取 manifest。
|
|
368
|
+
// 其他路径:保留原有策略。
|
|
369
|
+
let pluginManifestParsed;
|
|
370
|
+
let pluginManifestRelative;
|
|
371
|
+
|
|
372
|
+
if (form === 'bundled-family' && marketIndexParsed && platform.marketplaceSourceForm !== null) {
|
|
373
|
+
// bundled-family 有市场索引且平台支持市场来源解析(Claude/Codex):
|
|
374
|
+
// 使用权威 helper 从条目 source 解析。
|
|
375
|
+
// 计算市场根:从 marketplaceIndexRelative 推断(精确后缀匹配)。
|
|
376
|
+
const mktRoot = resolveMarketplaceRoot(platform, marketplaceIndexRelative);
|
|
377
|
+
try {
|
|
378
|
+
const resolved = await resolvePluginManifestFromMarketplaceEntrySource(
|
|
379
|
+
marketIndexParsed, action.plugin, platform, snapshotDirReal, mktRoot,
|
|
380
|
+
);
|
|
381
|
+
pluginManifestParsed = resolved.manifest;
|
|
382
|
+
pluginManifestRelative = resolved.manifestRelativePath;
|
|
383
|
+
} catch (err) {
|
|
384
|
+
return { valid: false, error: `installationContractDigest 验证失败:${err.message}` };
|
|
385
|
+
}
|
|
386
|
+
} else if (platform.strategy.readManifest) {
|
|
387
|
+
// Kimi/CodeBuddy/Codex 有自定义 manifest 读取策略
|
|
388
|
+
const readResult = await platform.strategy.readManifest(snapshotDirReal);
|
|
389
|
+
pluginManifestParsed = readResult.manifest;
|
|
390
|
+
pluginManifestRelative = readResult.manifestRelative ?? platform.manifestPaths.plugin;
|
|
391
|
+
} else {
|
|
392
|
+
// Claude fallback(standalone-index 或无市场索引时)
|
|
393
|
+
pluginManifestRelative = platform.manifestPaths.plugin;
|
|
394
|
+
const pluginManifestPath = resolve(snapshotDirReal, pluginManifestRelative);
|
|
395
|
+
const manifestResult = await validateManifestFile(pluginManifestPath, ['name', 'version']);
|
|
396
|
+
if (!manifestResult.valid) {
|
|
397
|
+
return { valid: false, error: `installationContractDigest 验证失败:插件 manifest 读取失败:${manifestResult.error}` };
|
|
398
|
+
}
|
|
399
|
+
pluginManifestParsed = manifestResult.manifest;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 3. 使用权威构建/摘要函数重算
|
|
403
|
+
const computedDigest = computeInstallationContractDigest({
|
|
404
|
+
distributionType: platform.distributionType,
|
|
405
|
+
manifestRelativePath: pluginManifestRelative,
|
|
406
|
+
manifest: pluginManifestParsed,
|
|
407
|
+
marketplaceSourceType: form,
|
|
408
|
+
includeMarketplaceEntry,
|
|
409
|
+
...(includeMarketplaceEntry ? {
|
|
410
|
+
marketplaceIndexRelativePath: marketplaceIndexRelative,
|
|
411
|
+
selectedMarketplaceEntry,
|
|
412
|
+
} : {}),
|
|
413
|
+
verificationRecipeVersion: CONSUMER_INSTALL_RECIPE_VERSION,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (computedDigest !== action.installationContractDigest) {
|
|
417
|
+
return {
|
|
418
|
+
valid: false,
|
|
419
|
+
error: `installationContractDigest 不匹配:预期 ${action.installationContractDigest.slice(0, 16)}...,重算 ${computedDigest.slice(0, 16)}...`,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return { valid: true, error: null };
|
|
424
|
+
}
|
|
425
|
+
|
|
142
426
|
/**
|
|
143
427
|
* Resolve the payload subpath the consumer CLI installs for this action,
|
|
144
428
|
* read from the marketplace manifest inside the digest-verified frozen
|
|
@@ -448,21 +732,37 @@ async function resolveKimiEntrySkillFile(pluginRootReal, manifest, entrySkill) {
|
|
|
448
732
|
* @returns {string} normalized relative path ('' for the plugin root itself)
|
|
449
733
|
*/
|
|
450
734
|
function normalizeCodeBuddySkillsRel(skillsRaw) {
|
|
451
|
-
|
|
735
|
+
// CodeBuddy real validator accepts array form; extract first element for
|
|
736
|
+
// backward compatibility with string form.
|
|
737
|
+
let skillsPath;
|
|
738
|
+
if (Array.isArray(skillsRaw)) {
|
|
739
|
+
if (skillsRaw.length !== 1) {
|
|
740
|
+
throw new Error(
|
|
741
|
+
`codebuddy manifest skills array must have exactly one element, got ${skillsRaw.length}`,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
skillsPath = skillsRaw[0];
|
|
745
|
+
} else if (typeof skillsRaw === 'string') {
|
|
746
|
+
skillsPath = skillsRaw;
|
|
747
|
+
} else {
|
|
748
|
+
throw new Error('codebuddy manifest skills must be a string or single-element array when present');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (typeof skillsPath !== 'string' || skillsPath.length === 0) {
|
|
452
752
|
throw new Error('codebuddy manifest skills must be a non-empty relative path when present');
|
|
453
753
|
}
|
|
454
754
|
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
/^https?:\/\//i.test(
|
|
755
|
+
skillsPath.startsWith('/') ||
|
|
756
|
+
skillsPath.includes('..') ||
|
|
757
|
+
skillsPath.includes('\\') ||
|
|
758
|
+
/^https?:\/\//i.test(skillsPath)
|
|
459
759
|
) {
|
|
460
|
-
throw new Error(`codebuddy manifest skills "${
|
|
760
|
+
throw new Error(`codebuddy manifest skills "${skillsPath}" is not a safe relative path`);
|
|
461
761
|
}
|
|
462
|
-
let rel =
|
|
762
|
+
let rel = skillsPath.replace(/^\.\//, '');
|
|
463
763
|
rel = rel.replace(/\/+$/, '');
|
|
464
764
|
if (rel.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
465
|
-
throw new Error(`codebuddy manifest skills "${
|
|
765
|
+
throw new Error(`codebuddy manifest skills "${skillsPath}" is not a safe relative path`);
|
|
466
766
|
}
|
|
467
767
|
return rel;
|
|
468
768
|
}
|
|
@@ -548,9 +848,55 @@ const SUPPORTED_TYPES = [
|
|
|
548
848
|
ActionType.CODEBUDDY_MARKETPLACE_INSTALL,
|
|
549
849
|
];
|
|
550
850
|
|
|
851
|
+
/**
|
|
852
|
+
* Classify whether an error indicates CLI/transport unavailability.
|
|
853
|
+
*
|
|
854
|
+
* ONLY these errors trigger human-attestation fallback for Codex:
|
|
855
|
+
* - ENOENT: binary not found
|
|
856
|
+
* - ETIMEDOUT/ECONNREFUSED/ECONNRESET: connection errors
|
|
857
|
+
* - ENOTFOUND: DNS resolution failure
|
|
858
|
+
* - spawn errors with specific codes
|
|
859
|
+
*
|
|
860
|
+
* All other errors (identity mismatch, malformed JSON, program bugs)
|
|
861
|
+
* are hard failures and NEVER trigger fallback.
|
|
862
|
+
*
|
|
863
|
+
* @param {Error} err
|
|
864
|
+
* @returns {boolean}
|
|
865
|
+
*/
|
|
866
|
+
function isCliOrTransportUnavailable(err) {
|
|
867
|
+
if (!err || typeof err !== 'object') return false;
|
|
868
|
+
|
|
869
|
+
// Node.js system error codes for CLI/transport unavailability
|
|
870
|
+
const UNAVAILABILITY_CODES = new Set([
|
|
871
|
+
'ENOENT', // binary not found
|
|
872
|
+
'ETIMEDOUT', // connection timeout
|
|
873
|
+
'ECONNREFUSED', // connection refused
|
|
874
|
+
'ECONNRESET', // connection reset
|
|
875
|
+
'ENOTFOUND', // DNS resolution failure
|
|
876
|
+
'EAI_AGAIN', // DNS temporary failure
|
|
877
|
+
'EHOSTUNREACH', // host unreachable
|
|
878
|
+
'ENETUNREACH', // network unreachable
|
|
879
|
+
]);
|
|
880
|
+
|
|
881
|
+
if (err.code && UNAVAILABILITY_CODES.has(err.code)) {
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Check error message for spawn/ENOENT patterns
|
|
886
|
+
const msg = err.message || '';
|
|
887
|
+
if (msg.includes('ENOENT') || msg.includes('spawn')) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
|
|
551
894
|
/** Safe repo pattern: owner/repo with alphanumeric, hyphens, dots, underscores. */
|
|
552
895
|
const SAFE_REPO_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
553
896
|
|
|
897
|
+
/** Safe digest pattern: 64-hex SHA-256. */
|
|
898
|
+
const SAFE_DIGEST_RE = /^[0-9a-f]{64}$/;
|
|
899
|
+
|
|
554
900
|
/** Valid consumer ids, derived from the platform registry (single source). */
|
|
555
901
|
const CONSUMER_IDS = new Set(PLATFORMS.map((p) => p.id));
|
|
556
902
|
|
|
@@ -634,7 +980,9 @@ function validateMarketplaceParams(params) {
|
|
|
634
980
|
// marketplace but NO non-interactive install API, so `marketplace` carries no
|
|
635
981
|
// executable meaning for kimi and is optional (validated only if present); it
|
|
636
982
|
// must not become a required identity condition for kimi execution/observe.
|
|
637
|
-
|
|
983
|
+
const consumerPlatform = getPlatform(consumer);
|
|
984
|
+
const consumerRoute = resolvePlatformRoute(consumerPlatform);
|
|
985
|
+
if (consumerRoute.route === 'human-attestation') {
|
|
638
986
|
if (marketplace !== undefined && marketplace !== null && !SAFE_ID_RE.test(marketplace)) {
|
|
639
987
|
return { valid: false, error: `unsafe marketplace identifier: "${marketplace}"` };
|
|
640
988
|
}
|
|
@@ -715,6 +1063,98 @@ async function checkRequiredFiles(dir, requiredFiles) {
|
|
|
715
1063
|
return { allPresent: missing.length === 0, missing };
|
|
716
1064
|
}
|
|
717
1065
|
|
|
1066
|
+
// ---------------------------------------------------------------------------
|
|
1067
|
+
// 市场来源单选模型(DT-D)
|
|
1068
|
+
// ---------------------------------------------------------------------------
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* 市场来源类型常量。
|
|
1072
|
+
*
|
|
1073
|
+
* - BUNDLED_FAMILY: 技能族自带市场文件(marketplace.json 与插件代码在同一仓库)
|
|
1074
|
+
* - STANDALONE_INDEX: 独立市场仓库(市场索引在外部仓库,插件代码在另一仓库)
|
|
1075
|
+
*/
|
|
1076
|
+
export const MARKETPLACE_SOURCE_TYPES = Object.freeze({
|
|
1077
|
+
BUNDLED_FAMILY: 'bundled-family',
|
|
1078
|
+
STANDALONE_INDEX: 'standalone-index',
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* 每个平台支持的市场来源类型。
|
|
1083
|
+
*
|
|
1084
|
+
* 所有平台均支持 bundled-family 和 standalone-index 两种来源形态。
|
|
1085
|
+
* 不得硬编码某个平台只能使用其中一种。
|
|
1086
|
+
*/
|
|
1087
|
+
const PLATFORM_SUPPORTED_SOURCES = Object.freeze({
|
|
1088
|
+
claude: new Set([MARKETPLACE_SOURCE_TYPES.BUNDLED_FAMILY, MARKETPLACE_SOURCE_TYPES.STANDALONE_INDEX]),
|
|
1089
|
+
codex: new Set([MARKETPLACE_SOURCE_TYPES.BUNDLED_FAMILY, MARKETPLACE_SOURCE_TYPES.STANDALONE_INDEX]),
|
|
1090
|
+
kimi: new Set([MARKETPLACE_SOURCE_TYPES.BUNDLED_FAMILY, MARKETPLACE_SOURCE_TYPES.STANDALONE_INDEX]),
|
|
1091
|
+
codebuddy: new Set([MARKETPLACE_SOURCE_TYPES.BUNDLED_FAMILY, MARKETPLACE_SOURCE_TYPES.STANDALONE_INDEX]),
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* 验证市场来源单选约束:每个平台每次发布只允许一种来源。
|
|
1096
|
+
*
|
|
1097
|
+
* 根据平台能力和配置/计划中的市场来源类型进行校验:
|
|
1098
|
+
* 1. 混用两种来源失败关闭
|
|
1099
|
+
* 2. 不支持的来源类型失败关闭
|
|
1100
|
+
*
|
|
1101
|
+
* @param {string} platform - 平台标识(claude / codex / kimi / codebuddy)
|
|
1102
|
+
* @param {object} config - 项目配置(releaseUnit.distributions 中的一项)
|
|
1103
|
+
* @param {object} plan - 发布计划(units.distributions 中的一项)
|
|
1104
|
+
* @returns {{ valid: boolean, error: string|null, selectedSource: string|null }}
|
|
1105
|
+
*/
|
|
1106
|
+
export function validateMarketplaceSourceSelection(platform, config, plan) {
|
|
1107
|
+
// 每个平台只允许一种来源
|
|
1108
|
+
const configSource = config?.marketplaceSourceType ?? null;
|
|
1109
|
+
const planSource = plan?.marketplaceSourceType ?? null;
|
|
1110
|
+
|
|
1111
|
+
// 确定选定的来源类型
|
|
1112
|
+
const selectedSource = planSource ?? configSource;
|
|
1113
|
+
|
|
1114
|
+
if (configSource && planSource && configSource !== planSource) {
|
|
1115
|
+
return {
|
|
1116
|
+
valid: false,
|
|
1117
|
+
error: `平台 "${platform}" 市场来源混用:配置声明 "${configSource}",计划声明 "${planSource}";每次发布只允许一种来源`,
|
|
1118
|
+
selectedSource: null,
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// 如果没有声明来源类型,无法验证(向后兼容旧配置)
|
|
1123
|
+
if (!selectedSource) {
|
|
1124
|
+
return { valid: true, error: null, selectedSource: null };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// 检查来源类型是否为已知值
|
|
1128
|
+
const knownSources = new Set(Object.values(MARKETPLACE_SOURCE_TYPES));
|
|
1129
|
+
if (!knownSources.has(selectedSource)) {
|
|
1130
|
+
return {
|
|
1131
|
+
valid: false,
|
|
1132
|
+
error: `未知的市场来源类型 "${selectedSource}";合法值为 ${[...knownSources].join(', ')}`,
|
|
1133
|
+
selectedSource: null,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// 检查平台是否支持该来源类型
|
|
1138
|
+
const supportedSources = PLATFORM_SUPPORTED_SOURCES[platform];
|
|
1139
|
+
if (!supportedSources) {
|
|
1140
|
+
return {
|
|
1141
|
+
valid: false,
|
|
1142
|
+
error: `未知的平台 "${platform}"`,
|
|
1143
|
+
selectedSource: null,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
if (!supportedSources.has(selectedSource)) {
|
|
1147
|
+
const supportedList = [...supportedSources].join(', ');
|
|
1148
|
+
return {
|
|
1149
|
+
valid: false,
|
|
1150
|
+
error: `平台 "${platform}" 不支持市场来源 "${selectedSource}";该平台仅支持: ${supportedList}`,
|
|
1151
|
+
selectedSource: null,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return { valid: true, error: null, selectedSource };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
718
1158
|
/**
|
|
719
1159
|
* Create the plugin-marketplace adapter.
|
|
720
1160
|
*
|
|
@@ -881,130 +1321,709 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
881
1321
|
});
|
|
882
1322
|
}
|
|
883
1323
|
|
|
884
|
-
//
|
|
1324
|
+
// Capability conflict check (before sourceDescriptor validation).
|
|
1325
|
+
// If the platform definition is internally inconsistent, fail closed.
|
|
885
1326
|
const consumer = action.consumer;
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
// resolve the entry skill via the manifest-declared skills root.
|
|
890
|
-
let kimiSnapshotManifest = null;
|
|
891
|
-
try {
|
|
892
|
-
snapshotDirReal = await resolveFrozenPath(context.root, snapshotPath, 'frozen snapshot path');
|
|
893
|
-
} catch (frozenErr) {
|
|
1327
|
+
const consumerPlatform = getPlatform(consumer);
|
|
1328
|
+
const capabilityConflicts = resolveCapabilityConflicts(consumerPlatform);
|
|
1329
|
+
if (capabilityConflicts.length > 0) {
|
|
894
1330
|
return createResult({
|
|
895
1331
|
actionType,
|
|
896
1332
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
897
|
-
error: `
|
|
1333
|
+
error: `platform capability conflict: ${capabilityConflicts.join('; ')}`,
|
|
898
1334
|
});
|
|
899
1335
|
}
|
|
900
1336
|
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1337
|
+
// 6. 统一静态预检:四平台共同执行安装来源与安装契约校验。
|
|
1338
|
+
// 旧冻结计划(整组新版字段完全不存在时)保留既有路径。
|
|
1339
|
+
// 新版字段组(installationContractDigest、algorithmVersion、marketplaceSourceType 为标志)
|
|
1340
|
+
// 出现任一字段时必须完整。0.2.3 已有的 marketplaceForm、sourceDescriptor、sourceCommit
|
|
1341
|
+
// 单独出现不得触发新版组。
|
|
1342
|
+
const route = resolvePlatformRoute(consumerPlatform);
|
|
1343
|
+
|
|
1344
|
+
// 6a. 新版字段组检测:installationContractDigest、algorithmVersion、marketplaceSourceType
|
|
1345
|
+
// 任一存在时走新路径
|
|
1346
|
+
const NEW_FIELD_GROUP_MARKERS = ['installationContractDigest', 'algorithmVersion', 'marketplaceSourceType'];
|
|
1347
|
+
const hasNewFieldGroup = NEW_FIELD_GROUP_MARKERS.some(
|
|
1348
|
+
(f) => action[f] !== undefined && action[f] !== null,
|
|
1349
|
+
);
|
|
1350
|
+
|
|
1351
|
+
if (hasNewFieldGroup) {
|
|
1352
|
+
// 新版字段组完整性:任一出现则要求全组完整
|
|
1353
|
+
const newFields = [
|
|
1354
|
+
'marketplaceForm', 'sourceDescriptor', 'installationContractDigest',
|
|
1355
|
+
'algorithmVersion', 'sourceCommit', 'marketplaceSourceType',
|
|
1356
|
+
];
|
|
1357
|
+
const missingNewFields = newFields.filter((f) => action[f] === undefined || action[f] === null);
|
|
1358
|
+
if (missingNewFields.length > 0) {
|
|
914
1359
|
return createResult({
|
|
915
1360
|
actionType,
|
|
916
1361
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
917
|
-
error:
|
|
1362
|
+
error: `新版字段组不完整,缺失: ${missingNewFields.join(', ')};任一出现则要求全组完整`,
|
|
918
1363
|
});
|
|
919
1364
|
}
|
|
920
|
-
|
|
921
|
-
|
|
1365
|
+
|
|
1366
|
+
const sd = action.sourceDescriptor;
|
|
1367
|
+
if (!sd || typeof sd !== 'object') {
|
|
922
1368
|
return createResult({
|
|
923
1369
|
actionType,
|
|
924
1370
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
925
|
-
error:
|
|
1371
|
+
error: 'sourceDescriptor is required when installationContractDigest is present',
|
|
926
1372
|
});
|
|
927
1373
|
}
|
|
928
|
-
|
|
1374
|
+
|
|
1375
|
+
// marketplaceForm 三方一致性
|
|
1376
|
+
if (action.marketplaceForm !== sd.form || action.marketplaceForm !== action.marketplaceSourceType) {
|
|
929
1377
|
return createResult({
|
|
930
1378
|
actionType,
|
|
931
1379
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
932
|
-
error: `
|
|
1380
|
+
error: `marketplaceForm "${action.marketplaceForm}"、sourceDescriptor.form "${sd.form}"、marketplaceSourceType "${action.marketplaceSourceType}" 三者必须一致`,
|
|
933
1381
|
});
|
|
934
1382
|
}
|
|
935
|
-
kimiSnapshotManifest = kimiManifest;
|
|
936
|
-
}
|
|
937
1383
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
// automatable fixed layout validated below.
|
|
947
|
-
if (
|
|
948
|
-
typeof action.marketplaceCommitSha !== 'string'
|
|
949
|
-
|| !/^[0-9a-f]{40}$/.test(action.marketplaceCommitSha)
|
|
950
|
-
) {
|
|
951
|
-
return createResult({
|
|
952
|
-
actionType,
|
|
953
|
-
status: ActionStatus.PREFLIGHT_FAILED,
|
|
954
|
-
error: `external marketplace marketplaceCommitSha must be a 40-hex commit sha, got ${JSON.stringify(action.marketplaceCommitSha)}`,
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
if (
|
|
958
|
-
typeof action.repo !== 'string'
|
|
959
|
-
|| !/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(action.repo)
|
|
960
|
-
) {
|
|
961
|
-
return createResult({
|
|
962
|
-
actionType,
|
|
963
|
-
status: ActionStatus.PREFLIGHT_FAILED,
|
|
964
|
-
error: `external marketplace repo must be an owner/name repository, got ${JSON.stringify(action.repo)}`,
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
const externalManifestRelative = platform.manifestPaths.plugin;
|
|
968
|
-
const externalManifestPath = resolve(snapshotDirReal, externalManifestRelative);
|
|
969
|
-
const externalManifestResult = await validateManifestFile(externalManifestPath, ['name', 'version']);
|
|
970
|
-
if (!externalManifestResult.valid) {
|
|
971
|
-
return createResult({
|
|
972
|
-
actionType,
|
|
973
|
-
status: ActionStatus.PREFLIGHT_FAILED,
|
|
974
|
-
error: `frozen snapshot ${externalManifestRelative} invalid: ${externalManifestResult.error}`,
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
if (externalManifestResult.manifest.name !== action.plugin) {
|
|
978
|
-
return createResult({
|
|
979
|
-
actionType,
|
|
980
|
-
status: ActionStatus.PREFLIGHT_FAILED,
|
|
981
|
-
error: `plugin manifest name "${externalManifestResult.manifest.name}" does not match action plugin "${action.plugin}"`,
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
if (externalManifestResult.manifest.version !== action.version) {
|
|
985
|
-
return createResult({
|
|
986
|
-
actionType,
|
|
987
|
-
status: ActionStatus.PREFLIGHT_FAILED,
|
|
988
|
-
error: `plugin manifest version "${externalManifestResult.manifest.version}" does not match action version "${action.version}"`,
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
} else {
|
|
992
|
-
// Verify marketplace files exist.
|
|
993
|
-
// marketplace.json is at the snapshot root; plugin manifest is
|
|
994
|
-
// resolved relative to the entry's declared source path.
|
|
995
|
-
const marketplaceRelative = platform.manifestPaths.marketplace;
|
|
1384
|
+
// installationContractDigest 格式
|
|
1385
|
+
if (!SAFE_DIGEST_RE.test(action.installationContractDigest)) {
|
|
1386
|
+
return createResult({
|
|
1387
|
+
actionType,
|
|
1388
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1389
|
+
error: 'installationContractDigest must be a 64-char lowercase hex string',
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
996
1392
|
|
|
997
|
-
|
|
1393
|
+
// algorithmVersion
|
|
1394
|
+
if (action.algorithmVersion !== INSTALLATION_CONTRACT_ALGORITHM_VERSION) {
|
|
1395
|
+
return createResult({
|
|
1396
|
+
actionType,
|
|
1397
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1398
|
+
error: `algorithmVersion must be ${INSTALLATION_CONTRACT_ALGORITHM_VERSION}, got ${action.algorithmVersion}`,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
998
1401
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1402
|
+
// sourceCommit 格式
|
|
1403
|
+
if (typeof action.sourceCommit !== 'string' || !/^[0-9a-f]{40}$/.test(action.sourceCommit)) {
|
|
1404
|
+
return createResult({
|
|
1405
|
+
actionType,
|
|
1406
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1407
|
+
error: 'sourceCommit must be a 40-char lowercase hex commit sha',
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// sourceDescriptor 共通字段
|
|
1412
|
+
if (typeof sd.form !== 'string' || (sd.form !== 'bundled-family' && sd.form !== 'standalone-index')) {
|
|
1413
|
+
return createResult({
|
|
1414
|
+
actionType,
|
|
1415
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1416
|
+
error: `sourceDescriptor.form must be "bundled-family" or "standalone-index", got "${sd.form}"`,
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
if (typeof sd.marketplaceEntry !== 'string' || sd.marketplaceEntry.length === 0) {
|
|
1420
|
+
return createResult({
|
|
1421
|
+
actionType,
|
|
1422
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1423
|
+
error: 'sourceDescriptor.marketplaceEntry is required',
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
if (sd.marketplaceEntry !== action.plugin) {
|
|
1427
|
+
return createResult({
|
|
1428
|
+
actionType,
|
|
1429
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1430
|
+
error: `sourceDescriptor.marketplaceEntry "${sd.marketplaceEntry}" does not match action plugin "${action.plugin}"`,
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// 来源形态专属字段校验
|
|
1435
|
+
if (sd.form === 'bundled-family') {
|
|
1436
|
+
// bundled-family 完整性
|
|
1437
|
+
if (!sd.repo || !SAFE_REPO_RE.test(sd.repo)) {
|
|
1438
|
+
return createResult({
|
|
1439
|
+
actionType,
|
|
1440
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1441
|
+
error: 'sourceDescriptor.repo is required and must be a safe repo pattern for bundled-family form',
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
if (sd.repo !== action.repo) {
|
|
1445
|
+
return createResult({
|
|
1446
|
+
actionType,
|
|
1447
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1448
|
+
error: `sourceDescriptor.repo "${sd.repo}" does not match action.repo "${action.repo}"`,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
if (typeof sd.commit !== 'string' || !/^[0-9a-f]{40}$/.test(sd.commit)) {
|
|
1452
|
+
return createResult({
|
|
1453
|
+
actionType,
|
|
1454
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1455
|
+
error: 'sourceDescriptor.commit must be a 40-hex commit sha for bundled-family form',
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
if (sd.commit !== action.sourceCommit) {
|
|
1459
|
+
return createResult({
|
|
1460
|
+
actionType,
|
|
1461
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1462
|
+
error: `sourceDescriptor.commit "${sd.commit}" does not match action.sourceCommit "${action.sourceCommit}"`,
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
if (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest)) {
|
|
1466
|
+
return createResult({
|
|
1467
|
+
actionType,
|
|
1468
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1469
|
+
error: 'sourceDescriptor.payloadDigest must be a 64-hex digest for bundled-family form',
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
if (sd.payloadDigest !== action.manifestDigest) {
|
|
1473
|
+
return createResult({
|
|
1474
|
+
actionType,
|
|
1475
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1476
|
+
error: 'sourceDescriptor.payloadDigest does not match action.manifestDigest',
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
if (typeof sd.pluginSubpath !== 'string' || sd.pluginSubpath.length === 0) {
|
|
1480
|
+
return createResult({
|
|
1481
|
+
actionType,
|
|
1482
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1483
|
+
error: 'sourceDescriptor.pluginSubpath is required for bundled-family form',
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
// 不允许独立市场专属字段(sourceDescriptor 层)
|
|
1487
|
+
if (sd.marketplaceRepo !== undefined || sd.marketplaceCommitSha !== undefined || sd.pluginRepo !== undefined) {
|
|
1488
|
+
return createResult({
|
|
1489
|
+
actionType,
|
|
1490
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1491
|
+
error: 'bundled-family sourceDescriptor must not contain standalone-index fields (marketplaceRepo, marketplaceCommitSha, pluginRepo)',
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
// 不允许 action 顶层独立市场专属字段,避免双来源混用。
|
|
1495
|
+
// marketplaceIndexPath 对 bundled-family 嵌套布局也合法(指示市场根位置),
|
|
1496
|
+
// 因此仅禁止真正的 standalone-index 专属字段。
|
|
1497
|
+
if (action.marketplaceCommitSha !== undefined || action.marketplaceName !== undefined || action.selectedEntry !== undefined) {
|
|
1498
|
+
return createResult({
|
|
1499
|
+
actionType,
|
|
1500
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1501
|
+
error: 'bundled-family action must not contain standalone-index fields (marketplaceCommitSha, marketplaceName, selectedEntry)',
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
// bundled-family marketplaceIndexPath 格式校验(可选,嵌套布局时存在)
|
|
1505
|
+
if (action.marketplaceIndexPath !== undefined && action.marketplaceIndexPath !== null) {
|
|
1506
|
+
if (typeof action.marketplaceIndexPath !== 'string' || action.marketplaceIndexPath.length === 0) {
|
|
1507
|
+
return createResult({
|
|
1508
|
+
actionType,
|
|
1509
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1510
|
+
error: 'bundled-family marketplaceIndexPath must be a non-empty string when present',
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
if (
|
|
1514
|
+
action.marketplaceIndexPath.startsWith('/') ||
|
|
1515
|
+
action.marketplaceIndexPath.includes('..') ||
|
|
1516
|
+
action.marketplaceIndexPath.includes('\\') ||
|
|
1517
|
+
/^https?:\/\//i.test(action.marketplaceIndexPath)
|
|
1518
|
+
) {
|
|
1519
|
+
return createResult({
|
|
1520
|
+
actionType,
|
|
1521
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1522
|
+
error: `marketplaceIndexPath "${action.marketplaceIndexPath}" is not a safe relative path`,
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
} else {
|
|
1527
|
+
// standalone-index 完整性
|
|
1528
|
+
if (!sd.marketplaceRepo || !SAFE_REPO_RE.test(sd.marketplaceRepo)) {
|
|
1529
|
+
return createResult({
|
|
1530
|
+
actionType,
|
|
1531
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1532
|
+
error: 'sourceDescriptor.marketplaceRepo is required for standalone-index form',
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
if (!sd.pluginRepo || !SAFE_REPO_RE.test(sd.pluginRepo)) {
|
|
1536
|
+
return createResult({
|
|
1537
|
+
actionType,
|
|
1538
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1539
|
+
error: 'sourceDescriptor.pluginRepo is required for standalone-index form',
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
if (sd.marketplaceRepo !== action.repo) {
|
|
1543
|
+
return createResult({
|
|
1544
|
+
actionType,
|
|
1545
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1546
|
+
error: `sourceDescriptor.marketplaceRepo "${sd.marketplaceRepo}" does not match action.repo "${action.repo}"`,
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
if (sd.pluginRepo === sd.marketplaceRepo) {
|
|
1550
|
+
return createResult({
|
|
1551
|
+
actionType,
|
|
1552
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1553
|
+
error: `sourceDescriptor.pluginRepo "${sd.pluginRepo}" must differ from marketplaceRepo "${sd.marketplaceRepo}"`,
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
if (typeof sd.marketplaceCommitSha !== 'string' || !/^[0-9a-f]{40}$/.test(sd.marketplaceCommitSha)) {
|
|
1557
|
+
return createResult({
|
|
1558
|
+
actionType,
|
|
1559
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1560
|
+
error: 'sourceDescriptor.marketplaceCommitSha must be a 40-hex commit sha for standalone-index form',
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
// action 顶层 marketplaceCommitSha 必须存在、为 40 位小写提交 SHA,
|
|
1564
|
+
// 且等于 sourceDescriptor.marketplaceCommitSha
|
|
1565
|
+
if (typeof action.marketplaceCommitSha !== 'string' || !/^[0-9a-f]{40}$/.test(action.marketplaceCommitSha)) {
|
|
1566
|
+
return createResult({
|
|
1567
|
+
actionType,
|
|
1568
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1569
|
+
error: 'marketplaceCommitSha is required at action top level for standalone-index form',
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
if (sd.marketplaceCommitSha !== action.marketplaceCommitSha) {
|
|
1573
|
+
return createResult({
|
|
1574
|
+
actionType,
|
|
1575
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1576
|
+
error: `sourceDescriptor.marketplaceCommitSha does not match action.marketplaceCommitSha`,
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
if (typeof sd.ref !== 'string' || sd.ref.length === 0) {
|
|
1580
|
+
return createResult({
|
|
1581
|
+
actionType,
|
|
1582
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1583
|
+
error: 'sourceDescriptor.ref is required for standalone-index form',
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
if (sd.ref !== action.ref) {
|
|
1587
|
+
return createResult({
|
|
1588
|
+
actionType,
|
|
1589
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1590
|
+
error: `sourceDescriptor.ref "${sd.ref}" does not match action.ref "${action.ref}"`,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
// payloadDigest 必须存在、为合法摘要且等于 manifestDigest(不能可选)
|
|
1594
|
+
if (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest)) {
|
|
1595
|
+
return createResult({
|
|
1596
|
+
actionType,
|
|
1597
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1598
|
+
error: 'sourceDescriptor.payloadDigest must be a 64-hex digest for standalone-index form',
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
if (sd.payloadDigest !== action.manifestDigest) {
|
|
1602
|
+
return createResult({
|
|
1603
|
+
actionType,
|
|
1604
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1605
|
+
error: 'sourceDescriptor.payloadDigest does not match action.manifestDigest',
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
// 不允许 bundled-family 专属字段
|
|
1609
|
+
if (sd.commit !== undefined || sd.pluginSubpath !== undefined) {
|
|
1610
|
+
return createResult({
|
|
1611
|
+
actionType,
|
|
1612
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1613
|
+
error: 'standalone-index sourceDescriptor must not contain bundled-family fields (commit, pluginSubpath)',
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// standalone-index 独立市场专属字段校验(action 层)
|
|
1619
|
+
if (action.marketplaceSourceType === 'standalone-index') {
|
|
1620
|
+
// marketplaceIndexPath 必须是安全非空相对路径
|
|
1621
|
+
if (typeof action.marketplaceIndexPath !== 'string' || action.marketplaceIndexPath.length === 0) {
|
|
1622
|
+
return createResult({
|
|
1623
|
+
actionType,
|
|
1624
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1625
|
+
error: 'marketplaceIndexPath is required for standalone-index form',
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
if (
|
|
1629
|
+
action.marketplaceIndexPath.startsWith('/') ||
|
|
1630
|
+
action.marketplaceIndexPath.includes('..') ||
|
|
1631
|
+
action.marketplaceIndexPath.includes('\\') ||
|
|
1632
|
+
/^https?:\/\//i.test(action.marketplaceIndexPath)
|
|
1633
|
+
) {
|
|
1634
|
+
return createResult({
|
|
1635
|
+
actionType,
|
|
1636
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1637
|
+
error: `marketplaceIndexPath "${action.marketplaceIndexPath}" is not a safe relative path`,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
// marketplaceName 必须是安全非空标识
|
|
1641
|
+
if (typeof action.marketplaceName !== 'string' || action.marketplaceName.length === 0) {
|
|
1642
|
+
return createResult({
|
|
1643
|
+
actionType,
|
|
1644
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1645
|
+
error: 'marketplaceName is required for standalone-index form',
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
if (!SAFE_ID_RE.test(action.marketplaceName)) {
|
|
1649
|
+
return createResult({
|
|
1650
|
+
actionType,
|
|
1651
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1652
|
+
error: `unsafe marketplaceName identifier: "${action.marketplaceName}"`,
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
// 若 action 带 marketplace,两者必须相等
|
|
1656
|
+
if (action.marketplace !== undefined && action.marketplace !== null && action.marketplaceName !== action.marketplace) {
|
|
1657
|
+
return createResult({
|
|
1658
|
+
actionType,
|
|
1659
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1660
|
+
error: `marketplaceName "${action.marketplaceName}" does not match action.marketplace "${action.marketplace}"`,
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
// selectedEntry 必须是普通对象,name/version 绑定 action
|
|
1664
|
+
if (!action.selectedEntry || typeof action.selectedEntry !== 'object' || Array.isArray(action.selectedEntry)) {
|
|
1665
|
+
return createResult({
|
|
1666
|
+
actionType,
|
|
1667
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1668
|
+
error: 'selectedEntry must be a non-null object for standalone-index form',
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
if (action.selectedEntry.name !== action.plugin) {
|
|
1672
|
+
return createResult({
|
|
1673
|
+
actionType,
|
|
1674
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1675
|
+
error: `selectedEntry.name "${action.selectedEntry.name}" does not match action.plugin "${action.plugin}"`,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
if (action.selectedEntry.version !== undefined && action.selectedEntry.version !== action.version) {
|
|
1679
|
+
return createResult({
|
|
1680
|
+
actionType,
|
|
1681
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1682
|
+
error: `selectedEntry.version "${action.selectedEntry.version}" does not match action.version "${action.version}"`,
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
} else {
|
|
1687
|
+
// 6b. 旧路径:sourceDescriptor 必须存在且为对象
|
|
1688
|
+
const sd = action.sourceDescriptor;
|
|
1689
|
+
if (!sd || typeof sd !== 'object') {
|
|
1690
|
+
return createResult({
|
|
1691
|
+
actionType,
|
|
1692
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1693
|
+
error: 'sourceDescriptor is required for marketplace install actions',
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
{
|
|
1697
|
+
// Form consistency: marketplaceForm must agree with sourceDescriptor.form.
|
|
1698
|
+
if (sd.form !== action.marketplaceForm) {
|
|
1699
|
+
return createResult({
|
|
1700
|
+
actionType,
|
|
1701
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1702
|
+
error: `sourceDescriptor.form "${sd.form}" does not match marketplaceForm "${action.marketplaceForm}"`,
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// payloadDigest: must be a valid 64-char lowercase hex string and
|
|
1707
|
+
// must not be the null hash (all zeros).
|
|
1708
|
+
if (typeof sd.payloadDigest !== 'string' || sd.payloadDigest.length === 0) {
|
|
1709
|
+
return createResult({
|
|
1710
|
+
actionType,
|
|
1711
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1712
|
+
error: 'sourceDescriptor.payloadDigest is required',
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
if (!/^[a-f0-9]{64}$/.test(sd.payloadDigest)) {
|
|
1716
|
+
return createResult({
|
|
1717
|
+
actionType,
|
|
1718
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1719
|
+
error: 'sourceDescriptor.payloadDigest must be a 64-char lowercase hex string',
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
if (sd.payloadDigest === '0'.repeat(64)) {
|
|
1723
|
+
return createResult({
|
|
1724
|
+
actionType,
|
|
1725
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1726
|
+
error: 'sourceDescriptor.payloadDigest must not be the null hash',
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// marketplaceEntry: must match the action plugin name.
|
|
1731
|
+
if (typeof sd.marketplaceEntry !== 'string' || sd.marketplaceEntry.length === 0) {
|
|
1732
|
+
return createResult({
|
|
1733
|
+
actionType,
|
|
1734
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1735
|
+
error: 'sourceDescriptor.marketplaceEntry is required',
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
if (sd.marketplaceEntry !== action.plugin) {
|
|
1739
|
+
return createResult({
|
|
1740
|
+
actionType,
|
|
1741
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1742
|
+
error: `sourceDescriptor.marketplaceEntry "${sd.marketplaceEntry}" does not match action plugin "${action.plugin}"`,
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Form-specific field validation.
|
|
1747
|
+
if (sd.form === 'bundled-family') {
|
|
1748
|
+
if (!sd.repo || !SAFE_REPO_RE.test(sd.repo)) {
|
|
1749
|
+
return createResult({
|
|
1750
|
+
actionType,
|
|
1751
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1752
|
+
error: `sourceDescriptor.repo is required and must be a safe repo pattern`,
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
if (sd.repo !== action.repo) {
|
|
1756
|
+
return createResult({
|
|
1757
|
+
actionType,
|
|
1758
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1759
|
+
error: `sourceDescriptor.repo "${sd.repo}" does not match action.repo "${action.repo}"`,
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
// commit 可选,但如果存在则必须是 40-hex 格式
|
|
1763
|
+
if (sd.commit !== undefined && (typeof sd.commit !== 'string' || !/^[0-9a-f]{40}$/.test(sd.commit))) {
|
|
1764
|
+
return createResult({
|
|
1765
|
+
actionType,
|
|
1766
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1767
|
+
error: 'sourceDescriptor.commit must be a 40-hex commit sha for bundled-family form',
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
// commit 交叉校验:bundled sourceDescriptor.commit 必须与
|
|
1771
|
+
// action.sourceCommit(冻结的插件来源提交)一致
|
|
1772
|
+
if (sd.commit !== undefined && action.sourceCommit && sd.commit !== action.sourceCommit) {
|
|
1773
|
+
return createResult({
|
|
1774
|
+
actionType,
|
|
1775
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1776
|
+
error: `sourceDescriptor.commit "${sd.commit}" does not match action.sourceCommit "${action.sourceCommit}"`,
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
// payloadDigest 格式校验(可选,但如果存在则必须是 64-hex)
|
|
1780
|
+
if (sd.payloadDigest !== undefined && (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest))) {
|
|
1781
|
+
return createResult({
|
|
1782
|
+
actionType,
|
|
1783
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1784
|
+
error: 'sourceDescriptor.payloadDigest is required for bundled-family form',
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
// payloadDigest 交叉校验:必须与 action.manifestDigest(冻结的载荷摘要)一致
|
|
1788
|
+
if (sd.payloadDigest !== undefined && action.manifestDigest && sd.payloadDigest !== action.manifestDigest) {
|
|
1789
|
+
return createResult({
|
|
1790
|
+
actionType,
|
|
1791
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1792
|
+
error: `sourceDescriptor.payloadDigest does not match action.manifestDigest`,
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
if (typeof sd.pluginSubpath !== 'string' || sd.pluginSubpath.length === 0) {
|
|
1796
|
+
return createResult({
|
|
1797
|
+
actionType,
|
|
1798
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1799
|
+
error: 'sourceDescriptor.pluginSubpath is required for bundled-family form',
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
} else if (sd.form === 'standalone-index') {
|
|
1803
|
+
if (!sd.pluginRepo || !SAFE_REPO_RE.test(sd.pluginRepo)) {
|
|
1804
|
+
return createResult({
|
|
1805
|
+
actionType,
|
|
1806
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1807
|
+
error: `sourceDescriptor.pluginRepo is required for standalone-index form`,
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
if (!sd.marketplaceRepo || !SAFE_REPO_RE.test(sd.marketplaceRepo)) {
|
|
1811
|
+
return createResult({
|
|
1812
|
+
actionType,
|
|
1813
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1814
|
+
error: 'sourceDescriptor.marketplaceRepo is required for standalone-index form',
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
// 独立市场身份交叉校验:
|
|
1818
|
+
// 1. marketplaceRepo 必须等于 action.repo(市场仓库身份)
|
|
1819
|
+
if (sd.marketplaceRepo !== action.repo) {
|
|
1820
|
+
return createResult({
|
|
1821
|
+
actionType,
|
|
1822
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1823
|
+
error: `sourceDescriptor.marketplaceRepo "${sd.marketplaceRepo}" does not match action.repo "${action.repo}"`,
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
// 2. pluginRepo 必须与 marketplaceRepo 不同(发布单元仓库 ≠ 市场仓库)
|
|
1827
|
+
if (sd.pluginRepo === sd.marketplaceRepo) {
|
|
1828
|
+
return createResult({
|
|
1829
|
+
actionType,
|
|
1830
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1831
|
+
error: `sourceDescriptor.pluginRepo "${sd.pluginRepo}" must differ from marketplaceRepo "${sd.marketplaceRepo}"`,
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
if (
|
|
1835
|
+
typeof sd.marketplaceCommitSha !== 'string'
|
|
1836
|
+
|| !/^[0-9a-f]{40}$/.test(sd.marketplaceCommitSha)
|
|
1837
|
+
) {
|
|
1838
|
+
return createResult({
|
|
1839
|
+
actionType,
|
|
1840
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1841
|
+
error: 'sourceDescriptor.marketplaceCommitSha must be a 40-hex commit sha for standalone-index form',
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
// 3. marketplaceCommitSha 交叉校验(与 action 层一致)
|
|
1845
|
+
if (action.marketplaceCommitSha && sd.marketplaceCommitSha !== action.marketplaceCommitSha) {
|
|
1846
|
+
return createResult({
|
|
1847
|
+
actionType,
|
|
1848
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1849
|
+
error: `sourceDescriptor.marketplaceCommitSha "${sd.marketplaceCommitSha}" does not match action.marketplaceCommitSha "${action.marketplaceCommitSha}"`,
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
if (typeof sd.ref !== 'string' || sd.ref.length === 0) {
|
|
1853
|
+
return createResult({
|
|
1854
|
+
actionType,
|
|
1855
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1856
|
+
error: 'sourceDescriptor.ref is required for standalone-index form',
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
// 4. payloadDigest 格式校验(可选,但如果存在则必须是 64-hex)
|
|
1860
|
+
if (sd.payloadDigest !== undefined && (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest))) {
|
|
1861
|
+
return createResult({
|
|
1862
|
+
actionType,
|
|
1863
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1864
|
+
error: 'sourceDescriptor.payloadDigest must be a 64-hex digest for standalone-index form',
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
// 5. payloadDigest 交叉校验:必须与 action.manifestDigest(冻结的载荷摘要)一致
|
|
1868
|
+
if (sd.payloadDigest !== undefined && action.manifestDigest && sd.payloadDigest !== action.manifestDigest) {
|
|
1869
|
+
return createResult({
|
|
1870
|
+
actionType,
|
|
1871
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1872
|
+
error: `sourceDescriptor.payloadDigest does not match action.manifestDigest`,
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
} else {
|
|
1876
|
+
return createResult({
|
|
1877
|
+
actionType,
|
|
1878
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1879
|
+
error: `sourceDescriptor.form "${sd.form}" is not a recognized form (expected "bundled-family" or "standalone-index")`,
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// 7. Verify frozen snapshot exists and contains required marketplace files
|
|
1886
|
+
const platform = consumerPlatform;
|
|
1887
|
+
const platformRoute = route;
|
|
1888
|
+
let snapshotDirReal;
|
|
1889
|
+
// Authoritative kimi manifest (from the frozen snapshot), used to
|
|
1890
|
+
// resolve the entry skill via the manifest-declared skills root.
|
|
1891
|
+
let kimiSnapshotManifest = null;
|
|
1892
|
+
try {
|
|
1893
|
+
snapshotDirReal = await resolveFrozenPath(context.root, snapshotPath, 'frozen snapshot path');
|
|
1894
|
+
} catch (frozenErr) {
|
|
1895
|
+
return createResult({
|
|
1896
|
+
actionType,
|
|
1897
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1898
|
+
error: `frozen snapshot validation failed: ${frozenErr.message}`,
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// A human-attestation platform (kimi, codebuddy) has no trustworthy
|
|
1903
|
+
// automated install: the whole repo is installed as one plugin. The
|
|
1904
|
+
// authoritative manifest is read from the verified snapshot root via
|
|
1905
|
+
// the platform strategy (kimi: kimi.plugin.json over
|
|
1906
|
+
// .kimi-plugin/plugin.json; codebuddy: .codebuddy-plugin/plugin.json).
|
|
1907
|
+
if (platformRoute.route === 'human-attestation') {
|
|
1908
|
+
// Error-message label keeps the kimi wording byte-identical while
|
|
1909
|
+
// giving codebuddy its own wording.
|
|
1910
|
+
const manifestLabel = consumer === 'codebuddy' ? 'codebuddy' : 'kimi';
|
|
1911
|
+
let kimiManifestResult;
|
|
1912
|
+
try {
|
|
1913
|
+
kimiManifestResult = await platform.strategy.readManifest(snapshotDirReal);
|
|
1914
|
+
} catch (manifestErr) {
|
|
1915
|
+
return createResult({
|
|
1916
|
+
actionType,
|
|
1917
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1918
|
+
error: `frozen snapshot ${manifestLabel} manifest invalid: ${manifestErr.message}`,
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
const kimiManifest = kimiManifestResult.manifest;
|
|
1922
|
+
if (typeof kimiManifest.name !== 'string' || kimiManifest.name !== action.plugin) {
|
|
1923
|
+
return createResult({
|
|
1924
|
+
actionType,
|
|
1925
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1926
|
+
error: `plugin manifest name "${kimiManifest.name}" does not match action plugin "${action.plugin}"`,
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
if (typeof kimiManifest.version !== 'string' || kimiManifest.version !== action.version) {
|
|
1930
|
+
return createResult({
|
|
1931
|
+
actionType,
|
|
1932
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1933
|
+
error: `plugin manifest version "${kimiManifest.version}" does not match action version "${action.version}"`,
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
kimiSnapshotManifest = kimiManifest;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// pluginRootForEntrySkill: the resolved plugin root directory for
|
|
1940
|
+
// entry skill validation. Set inside the structured-cli non-external
|
|
1941
|
+
// path; null for external/human-attestation (falls back to snapshotDirReal).
|
|
1942
|
+
let pluginRootForEntrySkill = null;
|
|
1943
|
+
|
|
1944
|
+
if (platformRoute.route === 'structured-cli') {
|
|
1945
|
+
let sourceDirReal = null;
|
|
1946
|
+
if (action.marketplaceLocation === 'external') {
|
|
1947
|
+
// External independent marketplace form: the marketplace index lives
|
|
1948
|
+
// in the external repository (frozen by prepare), NOT in the unit
|
|
1949
|
+
// snapshot, so the snapshot marketplace-manifest section is skipped
|
|
1950
|
+
// entirely. The plugin manifest is read directly from the snapshot
|
|
1951
|
+
// root and the frozen external identity fields are validated. ref was
|
|
1952
|
+
// already injection-checked above (step 2); the entry skill reuses the
|
|
1953
|
+
// automatable fixed layout validated below.
|
|
1954
|
+
if (
|
|
1955
|
+
typeof action.marketplaceCommitSha !== 'string'
|
|
1956
|
+
|| !/^[0-9a-f]{40}$/.test(action.marketplaceCommitSha)
|
|
1957
|
+
) {
|
|
1958
|
+
return createResult({
|
|
1959
|
+
actionType,
|
|
1960
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1961
|
+
error: `external marketplace marketplaceCommitSha must be a 40-hex commit sha, got ${JSON.stringify(action.marketplaceCommitSha)}`,
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
if (
|
|
1965
|
+
typeof action.repo !== 'string'
|
|
1966
|
+
|| !/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(action.repo)
|
|
1967
|
+
) {
|
|
1968
|
+
return createResult({
|
|
1969
|
+
actionType,
|
|
1970
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1971
|
+
error: `external marketplace repo must be an owner/name repository, got ${JSON.stringify(action.repo)}`,
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
const externalManifestRelative = platform.manifestPaths.plugin;
|
|
1975
|
+
const externalManifestPath = resolve(snapshotDirReal, externalManifestRelative);
|
|
1976
|
+
const externalManifestResult = await validateManifestFile(externalManifestPath, ['name', 'version']);
|
|
1977
|
+
if (!externalManifestResult.valid) {
|
|
1978
|
+
return createResult({
|
|
1979
|
+
actionType,
|
|
1980
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1981
|
+
error: `frozen snapshot ${externalManifestRelative} invalid: ${externalManifestResult.error}`,
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
if (externalManifestResult.manifest.name !== action.plugin) {
|
|
1985
|
+
return createResult({
|
|
1986
|
+
actionType,
|
|
1987
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1988
|
+
error: `plugin manifest name "${externalManifestResult.manifest.name}" does not match action plugin "${action.plugin}"`,
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
if (externalManifestResult.manifest.version !== action.version) {
|
|
1992
|
+
return createResult({
|
|
1993
|
+
actionType,
|
|
1994
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1995
|
+
error: `plugin manifest version "${externalManifestResult.manifest.version}" does not match action version "${action.version}"`,
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
} else {
|
|
1999
|
+
// Verify marketplace files exist.
|
|
2000
|
+
// marketplace.json 的路径:嵌套布局使用 action.marketplaceIndexPath,
|
|
2001
|
+
// 根布局使用平台默认路径。条目 source 相对市场根解析。
|
|
2002
|
+
const marketplaceRelative = action.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
|
|
2003
|
+
|
|
2004
|
+
// 计算市场根(精确后缀匹配)
|
|
2005
|
+
let mktRoot;
|
|
2006
|
+
try {
|
|
2007
|
+
mktRoot = resolveMarketplaceRoot(platform, marketplaceRelative);
|
|
2008
|
+
} catch (mktRootErr) {
|
|
2009
|
+
return createResult({
|
|
2010
|
+
actionType,
|
|
2011
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
2012
|
+
error: `marketplace root resolution failed: ${mktRootErr.message}`,
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const marketplacePath = resolve(snapshotDirReal, marketplaceRelative);
|
|
2017
|
+
|
|
2018
|
+
// marketplace.json must exist and have root name (no root version required)
|
|
2019
|
+
const marketplaceResult = await validateManifestFile(marketplacePath, ['name']);
|
|
2020
|
+
if (!marketplaceResult.valid) {
|
|
2021
|
+
return createResult({
|
|
2022
|
+
actionType,
|
|
2023
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
2024
|
+
error: `frozen snapshot ${marketplaceRelative} invalid: ${marketplaceResult.error}`,
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
1008
2027
|
|
|
1009
2028
|
// Root name must equal action.marketplace
|
|
1010
2029
|
if (marketplaceResult.manifest.name !== action.marketplace) {
|
|
@@ -1040,6 +2059,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1040
2059
|
// empty strings. Normalized to "." for root layouts; the same
|
|
1041
2060
|
// helper backs verify-side payload subtree resolution, so both
|
|
1042
2061
|
// paths can never drift apart.
|
|
2062
|
+
// source 相对市场根(mktRoot)解析,而非快照根。
|
|
1043
2063
|
let sourcePath;
|
|
1044
2064
|
try {
|
|
1045
2065
|
sourcePath = extractDeclaredPluginSource(consumer, entry);
|
|
@@ -1052,8 +2072,10 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1052
2072
|
}
|
|
1053
2073
|
// Verify the declared source directory exists and contains the
|
|
1054
2074
|
// expected plugin manifest inside the frozen snapshot.
|
|
1055
|
-
|
|
1056
|
-
const
|
|
2075
|
+
// source 相对市场根解析
|
|
2076
|
+
const mktRootAbs = mktRoot === '.' ? snapshotDirReal : resolve(snapshotDirReal, mktRoot);
|
|
2077
|
+
const sourceDirAbs = resolve(mktRootAbs, sourcePath);
|
|
2078
|
+
sourceDirReal = await realpath(sourceDirAbs).catch(() => null);
|
|
1057
2079
|
if (!sourceDirReal) {
|
|
1058
2080
|
return createResult({
|
|
1059
2081
|
actionType,
|
|
@@ -1070,13 +2092,18 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1070
2092
|
error: `marketplace plugin entry source "${sourcePath}" escapes the frozen snapshot`,
|
|
1071
2093
|
});
|
|
1072
2094
|
}
|
|
2095
|
+
// Save plugin root for entry skill validation (visible outside structured-cli block)
|
|
2096
|
+
pluginRootForEntrySkill = sourceDirReal;
|
|
1073
2097
|
|
|
1074
2098
|
// Resolve plugin manifest relative to the declared source path.
|
|
1075
2099
|
// For root layouts (source: "./"), this resolves to
|
|
1076
2100
|
// snapshot/.claude-plugin/plugin.json
|
|
1077
|
-
// For
|
|
1078
|
-
// snapshot/adapters/claude/.claude-plugin/plugin.json
|
|
1079
|
-
const
|
|
2101
|
+
// For nested layouts (marketRoot: "adapters/claude", source: "."),
|
|
2102
|
+
// this resolves to snapshot/adapters/claude/.claude-plugin/plugin.json
|
|
2103
|
+
const pluginRootRelToSnapshot = relative(snapshotDirReal, sourceDirReal) || '.';
|
|
2104
|
+
const manifestRelative = pluginRootRelToSnapshot === '.'
|
|
2105
|
+
? platform.manifestPaths.plugin
|
|
2106
|
+
: `${pluginRootRelToSnapshot}/${platform.manifestPaths.plugin}`;
|
|
1080
2107
|
const manifestPath = resolve(snapshotDirReal, manifestRelative);
|
|
1081
2108
|
|
|
1082
2109
|
const manifestResult = await validateManifestFile(manifestPath, ['name', 'version']);
|
|
@@ -1128,13 +2155,13 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1128
2155
|
}
|
|
1129
2156
|
|
|
1130
2157
|
// Verify the entry skill exists in the snapshot.
|
|
1131
|
-
//
|
|
2158
|
+
// Structured-cli platforms' manifests always declare ./skills/, so the
|
|
1132
2159
|
// fixed skills/<entrySkill>/SKILL.md layout is authoritative for
|
|
1133
|
-
// them. A
|
|
2160
|
+
// them. A human-attestation platform (kimi, codebuddy) resolves the
|
|
1134
2161
|
// entry skill via the manifest-declared skills root (MAJOR-4): the
|
|
1135
2162
|
// root is validated + realpath-contained, and omitted `skills` means
|
|
1136
2163
|
// the official single-skill root SKILL.md.
|
|
1137
|
-
if (
|
|
2164
|
+
if (platformRoute.route === 'human-attestation') {
|
|
1138
2165
|
const resolveEntrySkillFile = consumer === 'codebuddy'
|
|
1139
2166
|
? resolveCodeBuddyEntrySkillFile
|
|
1140
2167
|
: resolveKimiEntrySkillFile;
|
|
@@ -1148,14 +2175,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1148
2175
|
});
|
|
1149
2176
|
}
|
|
1150
2177
|
} else {
|
|
1151
|
-
|
|
2178
|
+
// Entry skill 相对插件根解析(嵌套布局时 pluginRoot != snapshotRoot)。
|
|
2179
|
+
// pluginRootForEntrySkill 在非 external 结构化 CLI 路径中已确定;
|
|
2180
|
+
// external 路径或 human-attestation(null)时回退到 snapshotDirReal。
|
|
2181
|
+
const pluginRootForSkill = pluginRootForEntrySkill || snapshotDirReal;
|
|
2182
|
+
const entrySkillFile = resolve(pluginRootForSkill, 'skills', action.entrySkill, 'SKILL.md');
|
|
1152
2183
|
try {
|
|
1153
2184
|
await stat(entrySkillFile);
|
|
1154
2185
|
} catch {
|
|
2186
|
+
const skillRel = relative(snapshotDirReal, entrySkillFile) || `skills/${action.entrySkill}/SKILL.md`;
|
|
1155
2187
|
return createResult({
|
|
1156
2188
|
actionType,
|
|
1157
2189
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1158
|
-
error: `entry skill not found in snapshot:
|
|
2190
|
+
error: `entry skill not found in snapshot: ${skillRel}`,
|
|
1159
2191
|
});
|
|
1160
2192
|
}
|
|
1161
2193
|
}
|
|
@@ -1167,15 +2199,37 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1167
2199
|
return createResult({
|
|
1168
2200
|
actionType,
|
|
1169
2201
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1170
|
-
error: `manifestDigest mismatch: expected ${manifestDigest.slice(0, 16)}..., actual ${actualDigest.slice(0, 16)}...`,
|
|
2202
|
+
error: `manifestDigest mismatch: expected ${manifestDigest.slice(0, 16)}..., actual ${actualDigest.slice(0, 16)}...`,
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
} catch (digestErr) {
|
|
2206
|
+
return createResult({
|
|
2207
|
+
actionType,
|
|
2208
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
2209
|
+
error: `failed to compute snapshot digest: ${digestErr.message}`,
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// installationContractDigest 摘要校验(新版字段组已通过完整性检查后来到这里)
|
|
2214
|
+
// 从冻结快照读取插件 manifest,按平台注册表和来源形态构建安装契约,
|
|
2215
|
+
// 以 consumer-install-v1 重算并与 action 携带的摘要比对。
|
|
2216
|
+
if (hasNewFieldGroup) {
|
|
2217
|
+
try {
|
|
2218
|
+
const contractResult = await validateInstallationContractDigest(action, snapshotDirReal, consumerPlatform);
|
|
2219
|
+
if (!contractResult.valid) {
|
|
2220
|
+
return createResult({
|
|
2221
|
+
actionType,
|
|
2222
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
2223
|
+
error: contractResult.error,
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
} catch (contractErr) {
|
|
2227
|
+
return createResult({
|
|
2228
|
+
actionType,
|
|
2229
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
2230
|
+
error: `installationContractDigest 验证异常: ${contractErr.message}`,
|
|
1171
2231
|
});
|
|
1172
2232
|
}
|
|
1173
|
-
} catch (digestErr) {
|
|
1174
|
-
return createResult({
|
|
1175
|
-
actionType,
|
|
1176
|
-
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1177
|
-
error: `failed to compute snapshot digest: ${digestErr.message}`,
|
|
1178
|
-
});
|
|
1179
2233
|
}
|
|
1180
2234
|
|
|
1181
2235
|
return createResult({
|
|
@@ -1341,10 +2395,11 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1341
2395
|
// It is handled entirely by its manual-requirement strategy, which
|
|
1342
2396
|
// uses a stable plan-digest-keyed home and deliberately SKIPS the
|
|
1343
2397
|
// per-run isolated consumer dir and its runDir containment check
|
|
1344
|
-
// (that model only fits
|
|
2398
|
+
// (that model only fits structured-cli platforms, which exec a real
|
|
1345
2399
|
// CLI into a per-run HOME).
|
|
1346
2400
|
const platform = getPlatform(action.consumer);
|
|
1347
|
-
|
|
2401
|
+
const executeRoute = resolvePlatformRoute(platform);
|
|
2402
|
+
if (executeRoute.route === 'human-attestation') {
|
|
1348
2403
|
return platform.strategy.buildManualRequirement(action, context);
|
|
1349
2404
|
}
|
|
1350
2405
|
|
|
@@ -1431,6 +2486,11 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1431
2486
|
}
|
|
1432
2487
|
}
|
|
1433
2488
|
} catch (addErr) {
|
|
2489
|
+
// Re-throw CLI/transport unavailability errors to outer catch
|
|
2490
|
+
// for human-attestation fallback classification
|
|
2491
|
+
if (isCliOrTransportUnavailable(addErr)) {
|
|
2492
|
+
throw addErr;
|
|
2493
|
+
}
|
|
1434
2494
|
return createResult({
|
|
1435
2495
|
actionType,
|
|
1436
2496
|
status: ActionStatus.EXECUTE_FAILED,
|
|
@@ -1505,6 +2565,11 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1505
2565
|
}
|
|
1506
2566
|
}
|
|
1507
2567
|
} catch (installErr) {
|
|
2568
|
+
// Re-throw CLI/transport unavailability errors to outer catch
|
|
2569
|
+
// for human-attestation fallback classification
|
|
2570
|
+
if (isCliOrTransportUnavailable(installErr)) {
|
|
2571
|
+
throw installErr;
|
|
2572
|
+
}
|
|
1508
2573
|
return createResult({
|
|
1509
2574
|
actionType,
|
|
1510
2575
|
status: ActionStatus.EXECUTE_FAILED,
|
|
@@ -1571,6 +2636,18 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1571
2636
|
observation: executeObservation,
|
|
1572
2637
|
});
|
|
1573
2638
|
} catch (err) {
|
|
2639
|
+
// Codex human-attestation fallback: ONLY when the CLI interface,
|
|
2640
|
+
// binary, environment, or transport is explicitly unavailable.
|
|
2641
|
+
// Explicit mismatches (identity, version, payload, marketplace entry),
|
|
2642
|
+
// malformed JSON, and program exceptions are hard failures.
|
|
2643
|
+
const failedPlatform = getPlatform(action.consumer);
|
|
2644
|
+
if (
|
|
2645
|
+
failedPlatform.degradationPolicy === 'human-attestation-with-fallback'
|
|
2646
|
+
&& failedPlatform.strategy.buildManualRequirement
|
|
2647
|
+
&& isCliOrTransportUnavailable(err)
|
|
2648
|
+
) {
|
|
2649
|
+
return failedPlatform.strategy.buildManualRequirement(action, context);
|
|
2650
|
+
}
|
|
1574
2651
|
return createResult({
|
|
1575
2652
|
actionType,
|
|
1576
2653
|
status: ActionStatus.EXECUTE_FAILED,
|
|
@@ -1641,384 +2718,96 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1641
2718
|
allPresent: check.allPresent,
|
|
1642
2719
|
missing: check.missing,
|
|
1643
2720
|
pluginDir,
|
|
1644
|
-
},
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
// Marketplace install observe
|
|
1649
|
-
if (
|
|
1650
|
-
actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
|
|
1651
|
-
actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
|
|
1652
|
-
actionType === ActionType.KIMI_MARKETPLACE_INSTALL ||
|
|
1653
|
-
actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL
|
|
1654
|
-
) {
|
|
1655
|
-
const consumer = action.consumer;
|
|
1656
|
-
const runDir = context.runDir;
|
|
1657
|
-
if (!runDir) {
|
|
1658
|
-
return createResult({
|
|
1659
|
-
actionType,
|
|
1660
|
-
status: ActionStatus.OBSERVED,
|
|
1661
|
-
observation: { installed: false, error: 'context.runDir is required' },
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
const isolatedHome = resolve(runDir, 'consumers', `${consumer}-${action.plugin}`);
|
|
1665
|
-
// Registry-driven platform data. observe historically applies no
|
|
1666
|
-
// consumer validation gate (execute/preflight validate it), so an
|
|
1667
|
-
// unregistered consumer keeps the legacy fall-through shape: a
|
|
1668
|
-
// kimi-shaped env, no attestation branch, and no CLI binary — it
|
|
1669
|
-
// still fails closed on the missing execute evidence below.
|
|
1670
|
-
const platform = PLATFORMS.find((p) => p.id === consumer) ?? null;
|
|
1671
|
-
const cliCmd = platform ? (platform.cli ? platform.cli.binary : null) : 'kimi';
|
|
1672
|
-
const baseEnv = { ...process.env, ...(context.env ?? {}) };
|
|
1673
|
-
const env = {
|
|
1674
|
-
...baseEnv,
|
|
1675
|
-
...(platform
|
|
1676
|
-
? platform.isolationEnv(isolatedHome)
|
|
1677
|
-
: { HOME: isolatedHome, KIMI_CODE_HOME: isolatedHome }),
|
|
1678
|
-
};
|
|
1679
|
-
|
|
1680
|
-
// Resolve frozen timeoutMs from the expanded action (top-level).
|
|
1681
|
-
// Default to 300000 for old plans. Fail closed on invalid values.
|
|
1682
|
-
let frozenTimeoutMs;
|
|
1683
|
-
try {
|
|
1684
|
-
frozenTimeoutMs = resolveTimeoutMs(action);
|
|
1685
|
-
} catch (timeoutErr) {
|
|
1686
|
-
return createResult({
|
|
1687
|
-
actionType,
|
|
1688
|
-
status: ActionStatus.OBSERVED,
|
|
1689
|
-
observation: { installed: false, error: timeoutErr.message },
|
|
1690
|
-
error: timeoutErr.message,
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// Non-automatable platform protocol capability gap (BLOCKER-1):
|
|
1695
|
-
// there is NO `kimi plugins list --json` interface. observe never
|
|
1696
|
-
// execs a kimi command. Instead it consumes a structured human
|
|
1697
|
-
// attestation (written after the interactive install) bound to the
|
|
1698
|
-
// frozen plan digest and expected identity, then performs read-only
|
|
1699
|
-
// verification of the installed managed copy: payload digest vs the
|
|
1700
|
-
// sealed authority, entry skill resolved via the manifest skills
|
|
1701
|
-
// root (MAJOR-4), and manifest name/version.
|
|
1702
|
-
// Missing/expired/mismatched/escaping proof fails closed so a kimi
|
|
1703
|
-
// unit can never reach VERIFIED without it.
|
|
1704
|
-
if (platform && !platform.automatable && actionType === ActionType.KIMI_MARKETPLACE_INSTALL) {
|
|
1705
|
-
const expectedRef = action.ref ?? `v${action.version}`;
|
|
1706
|
-
|
|
1707
|
-
// Bind to the REAL frozen plan digest (A). Fail closed if the
|
|
1708
|
-
// context does not carry an intact frozen plan.
|
|
1709
|
-
let boundPlanDigest;
|
|
1710
|
-
try {
|
|
1711
|
-
boundPlanDigest = resolveBoundPlanDigest(context);
|
|
1712
|
-
} catch (planErr) {
|
|
1713
|
-
return createResult({
|
|
1714
|
-
actionType,
|
|
1715
|
-
status: ActionStatus.OBSERVED,
|
|
1716
|
-
observation: {
|
|
1717
|
-
installed: false,
|
|
1718
|
-
error: `cannot bind kimi observation to the frozen plan: ${planErr.message}`,
|
|
1719
|
-
},
|
|
1720
|
-
});
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
// Stable, cross-run attestation authority (B). The requirement and
|
|
1724
|
-
// attestation live here, keyed by the verified plan digest + plugin,
|
|
1725
|
-
// so they survive publish PARTIAL -> reconcile -> verify (each of
|
|
1726
|
-
// which uses a fresh runDir).
|
|
1727
|
-
let attestationDir;
|
|
1728
|
-
try {
|
|
1729
|
-
attestationDir = kimiAuthorityDir(context, boundPlanDigest, action.plugin);
|
|
1730
|
-
} catch (dirErr) {
|
|
1731
|
-
return createResult({
|
|
1732
|
-
actionType,
|
|
1733
|
-
status: ActionStatus.OBSERVED,
|
|
1734
|
-
observation: { installed: false, error: dirErr.message },
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// The execute-emitted requirement must exist and bind to the action
|
|
1739
|
-
// and the frozen plan digest.
|
|
1740
|
-
let requirement = null;
|
|
1741
|
-
try {
|
|
1742
|
-
requirement = JSON.parse(await readFile(resolve(attestationDir, KIMI_REQUIREMENT_FILE), 'utf8'));
|
|
1743
|
-
} catch {
|
|
1744
|
-
return createResult({
|
|
1745
|
-
actionType,
|
|
1746
|
-
status: ActionStatus.OBSERVED,
|
|
1747
|
-
observation: {
|
|
1748
|
-
installed: false,
|
|
1749
|
-
manualInstallRequired: true,
|
|
1750
|
-
error: 'kimi manual-install requirement is missing; run execute first',
|
|
1751
|
-
},
|
|
1752
|
-
});
|
|
1753
|
-
}
|
|
1754
|
-
if (
|
|
1755
|
-
requirement.planDigest !== boundPlanDigest ||
|
|
1756
|
-
requirement.plugin !== action.plugin ||
|
|
1757
|
-
requirement.version !== action.version ||
|
|
1758
|
-
requirement.entrySkill !== action.entrySkill ||
|
|
1759
|
-
requirement.repo !== action.repo ||
|
|
1760
|
-
requirement.ref !== expectedRef
|
|
1761
|
-
) {
|
|
1762
|
-
return createResult({
|
|
1763
|
-
actionType,
|
|
1764
|
-
status: ActionStatus.OBSERVED,
|
|
1765
|
-
observation: {
|
|
1766
|
-
installed: false,
|
|
1767
|
-
error: 'kimi manual-install requirement does not match the frozen plan/action',
|
|
1768
|
-
},
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
// The trusted human attestation is mandatory and is read from the
|
|
1773
|
-
// stable authority dir. Without it the interactive install has not
|
|
1774
|
-
// been proven: fail closed.
|
|
1775
|
-
let attestation = null;
|
|
1776
|
-
try {
|
|
1777
|
-
attestation = JSON.parse(await readFile(resolve(attestationDir, KIMI_ATTESTATION_FILE), 'utf8'));
|
|
1778
|
-
} catch {
|
|
1779
|
-
return createResult({
|
|
1780
|
-
actionType,
|
|
1781
|
-
status: ActionStatus.OBSERVED,
|
|
1782
|
-
observation: {
|
|
1783
|
-
installed: false,
|
|
1784
|
-
manualInstallRequired: true,
|
|
1785
|
-
installUrl: requirement.installUrl,
|
|
1786
|
-
attestationDir,
|
|
1787
|
-
error: `kimi attestation is missing; write ${resolve(attestationDir, KIMI_ATTESTATION_FILE)} after the interactive install (${requirement.installUrl})`,
|
|
1788
|
-
},
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
const attestationCheck = validateKimiAttestation(attestation, action, new Date().toISOString(), boundPlanDigest);
|
|
1793
|
-
if (!attestationCheck.valid) {
|
|
1794
|
-
return createResult({
|
|
1795
|
-
actionType,
|
|
1796
|
-
status: ActionStatus.OBSERVED,
|
|
1797
|
-
observation: { installed: false, error: attestationCheck.error },
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
// The attested install path must live in the documented managed
|
|
1802
|
-
// layout under the STABLE, plan-digest-keyed isolated home (B). The
|
|
1803
|
-
// managed root is derived from the authority dir (itself derived from
|
|
1804
|
-
// the verified plan digest + plugin), so it is identical across
|
|
1805
|
-
// publish/reconcile/verify run dirs. No lexical-containment fallback
|
|
1806
|
-
// (C): every path must realpath-resolve and stay contained; a missing
|
|
1807
|
-
// home/root, a symlink, or an escape fails closed.
|
|
1808
|
-
const kimiCodeHome = resolve(attestationDir, 'kimi-home');
|
|
1809
|
-
const kimiCodeHomeReal = await realpath(kimiCodeHome).catch(() => null);
|
|
1810
|
-
if (!kimiCodeHomeReal) {
|
|
1811
|
-
return createResult({
|
|
1812
|
-
actionType,
|
|
1813
|
-
status: ActionStatus.OBSERVED,
|
|
1814
|
-
observation: {
|
|
1815
|
-
installed: false,
|
|
1816
|
-
error: `KIMI_CODE_HOME does not exist or cannot be resolved: ${kimiCodeHome}`,
|
|
1817
|
-
},
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
const managedRootReal = await realpath(resolve(kimiCodeHomeReal, KIMI_MANAGED_SUBPATH, action.plugin)).catch(() => null);
|
|
1821
|
-
if (!managedRootReal) {
|
|
1822
|
-
return createResult({
|
|
1823
|
-
actionType,
|
|
1824
|
-
status: ActionStatus.OBSERVED,
|
|
1825
|
-
observation: {
|
|
1826
|
-
installed: false,
|
|
1827
|
-
error: `kimi managed plugin root does not exist: ${resolve(kimiCodeHomeReal, KIMI_MANAGED_SUBPATH, action.plugin)}`,
|
|
1828
|
-
},
|
|
1829
|
-
});
|
|
1830
|
-
}
|
|
1831
|
-
// installPath must be a real directory (not a symlink) that resolves
|
|
1832
|
-
// inside the managed root.
|
|
1833
|
-
const installPath = resolve(attestation.installPath);
|
|
1834
|
-
let installPathStat;
|
|
1835
|
-
try {
|
|
1836
|
-
installPathStat = await lstat(installPath);
|
|
1837
|
-
} catch {
|
|
1838
|
-
return createResult({
|
|
1839
|
-
actionType,
|
|
1840
|
-
status: ActionStatus.OBSERVED,
|
|
1841
|
-
observation: { installed: false, error: `kimi install path does not exist: ${attestation.installPath}` },
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
if (installPathStat.isSymbolicLink()) {
|
|
1845
|
-
return createResult({
|
|
1846
|
-
actionType,
|
|
1847
|
-
status: ActionStatus.OBSERVED,
|
|
1848
|
-
observation: { installed: false, error: `kimi install path must not be a symlink: ${attestation.installPath}` },
|
|
1849
|
-
});
|
|
1850
|
-
}
|
|
1851
|
-
if (!installPathStat.isDirectory()) {
|
|
1852
|
-
return createResult({
|
|
1853
|
-
actionType,
|
|
1854
|
-
status: ActionStatus.OBSERVED,
|
|
1855
|
-
observation: { installed: false, error: `kimi install path must be a directory: ${attestation.installPath}` },
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
const installPathReal = await realpath(installPath).catch(() => null);
|
|
1859
|
-
if (!installPathReal) {
|
|
1860
|
-
return createResult({
|
|
1861
|
-
actionType,
|
|
1862
|
-
status: ActionStatus.OBSERVED,
|
|
1863
|
-
observation: { installed: false, error: `kimi install path cannot be resolved: ${attestation.installPath}` },
|
|
1864
|
-
});
|
|
1865
|
-
}
|
|
1866
|
-
const sepK = process.platform === 'win32' ? '\\' : '/';
|
|
1867
|
-
const relToManaged = relative(managedRootReal, installPathReal);
|
|
1868
|
-
if (
|
|
1869
|
-
relToManaged !== '' &&
|
|
1870
|
-
(isAbsolute(relToManaged) || relToManaged === '..' || relToManaged.startsWith(`..${sepK}`))
|
|
1871
|
-
) {
|
|
1872
|
-
return createResult({
|
|
1873
|
-
actionType,
|
|
1874
|
-
status: ActionStatus.OBSERVED,
|
|
1875
|
-
observation: {
|
|
1876
|
-
installed: false,
|
|
1877
|
-
error: `kimi install path escapes the managed root (${managedRootReal}): ${attestation.installPath}`,
|
|
1878
|
-
},
|
|
1879
|
-
});
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
// Read-only payload binding: the installed managed copy must match
|
|
1883
|
-
// the sealed frozen authority exactly (transport-normalized).
|
|
1884
|
-
let manifestDigest;
|
|
1885
|
-
let payloadBinding = null;
|
|
1886
|
-
try {
|
|
1887
|
-
payloadBinding = await verifyInstalledMarketplacePayload(action, context, installPathReal, consumer);
|
|
1888
|
-
manifestDigest = payloadBinding.manifestDigest;
|
|
1889
|
-
} catch (digestErr) {
|
|
1890
|
-
return createResult({
|
|
1891
|
-
actionType,
|
|
1892
|
-
status: ActionStatus.OBSERVED,
|
|
1893
|
-
observation: {
|
|
1894
|
-
installed: true,
|
|
1895
|
-
installPath: installPathReal,
|
|
1896
|
-
error: `failed to bind installed kimi payload to frozen authority: ${digestErr.message}`,
|
|
1897
|
-
},
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
if (manifestDigest !== action.manifestDigest) {
|
|
1901
|
-
return createResult({
|
|
1902
|
-
actionType,
|
|
1903
|
-
status: ActionStatus.OBSERVED,
|
|
1904
|
-
observation: {
|
|
1905
|
-
installed: true,
|
|
1906
|
-
installPath: installPathReal,
|
|
1907
|
-
error: 'installed kimi payload digest does not match the frozen plan digest',
|
|
1908
|
-
},
|
|
1909
|
-
});
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
// Entry skill must resolve via the installed manifest's skills root
|
|
1913
|
-
// (MAJOR-4), with official manifest precedence.
|
|
1914
|
-
let installedManifest;
|
|
1915
|
-
let entrySkillFound = false;
|
|
1916
|
-
try {
|
|
1917
|
-
const readManifest = await platform.strategy.readManifest(installPathReal);
|
|
1918
|
-
installedManifest = readManifest.manifest;
|
|
1919
|
-
await resolveKimiEntrySkillFile(installPathReal, installedManifest, action.entrySkill);
|
|
1920
|
-
entrySkillFound = true;
|
|
1921
|
-
} catch (entryErr) {
|
|
1922
|
-
return createResult({
|
|
1923
|
-
actionType,
|
|
1924
|
-
status: ActionStatus.OBSERVED,
|
|
1925
|
-
observation: {
|
|
1926
|
-
installed: true,
|
|
1927
|
-
installPath: installPathReal,
|
|
1928
|
-
manifestDigest,
|
|
1929
|
-
entrySkillFound: false,
|
|
1930
|
-
error: `kimi entry skill not resolvable in installed copy: ${entryErr.message}`,
|
|
1931
|
-
},
|
|
1932
|
-
});
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
// Installed manifest identity must match the frozen action.
|
|
1936
|
-
if (installedManifest.name !== action.plugin) {
|
|
1937
|
-
return createResult({
|
|
1938
|
-
actionType,
|
|
1939
|
-
status: ActionStatus.OBSERVED,
|
|
1940
|
-
observation: {
|
|
1941
|
-
installed: true,
|
|
1942
|
-
installPath: installPathReal,
|
|
1943
|
-
manifestDigest,
|
|
1944
|
-
entrySkillFound: true,
|
|
1945
|
-
error: `installed kimi manifest name "${installedManifest.name}" does not match action plugin "${action.plugin}"`,
|
|
1946
|
-
},
|
|
1947
|
-
});
|
|
1948
|
-
}
|
|
1949
|
-
if (installedManifest.version !== action.version) {
|
|
1950
|
-
return createResult({
|
|
1951
|
-
actionType,
|
|
1952
|
-
status: ActionStatus.OBSERVED,
|
|
1953
|
-
observation: {
|
|
1954
|
-
installed: true,
|
|
1955
|
-
installPath: installPathReal,
|
|
1956
|
-
manifestDigest,
|
|
1957
|
-
entrySkillFound: true,
|
|
1958
|
-
error: `installed kimi manifest version "${installedManifest.version}" does not match action version "${action.version}"`,
|
|
1959
|
-
},
|
|
1960
|
-
});
|
|
1961
|
-
}
|
|
2721
|
+
},
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
1962
2724
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
version: installedManifest.version,
|
|
1974
|
-
repo: action.repo,
|
|
1975
|
-
ref: expectedRef,
|
|
1976
|
-
...extraInstalledPathsAudit(payloadBinding),
|
|
1977
|
-
};
|
|
2725
|
+
// Marketplace install observe
|
|
2726
|
+
if (
|
|
2727
|
+
actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
|
|
2728
|
+
actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
|
|
2729
|
+
actionType === ActionType.KIMI_MARKETPLACE_INSTALL ||
|
|
2730
|
+
actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL
|
|
2731
|
+
) {
|
|
2732
|
+
const consumer = action.consumer;
|
|
2733
|
+
const runDir = context.runDir;
|
|
2734
|
+
if (!runDir) {
|
|
1978
2735
|
return createResult({
|
|
1979
2736
|
actionType,
|
|
1980
2737
|
status: ActionStatus.OBSERVED,
|
|
1981
|
-
observation,
|
|
2738
|
+
observation: { installed: false, error: 'context.runDir is required' },
|
|
1982
2739
|
});
|
|
1983
2740
|
}
|
|
2741
|
+
const isolatedHome = resolve(runDir, 'consumers', `${consumer}-${action.plugin}`);
|
|
2742
|
+
// Registry-driven platform data. An unregistered consumer is a hard
|
|
2743
|
+
// error: the platform registry is the single source of truth for
|
|
2744
|
+
// consumer-platform knowledge, and silently falling through to a
|
|
2745
|
+
// kimi-shaped env would mask configuration mistakes.
|
|
2746
|
+
const platform = PLATFORMS.find((p) => p.id === consumer) ?? null;
|
|
2747
|
+
if (!platform) {
|
|
2748
|
+
throw new Error(
|
|
2749
|
+
`Unknown consumer platform "${consumer}" for action "${actionType}". `
|
|
2750
|
+
+ `Registered platforms: ${PLATFORMS.map((p) => p.id).join(', ')}`,
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
const cliCmd = platform.cli ? platform.cli.binary : null;
|
|
2754
|
+
const baseEnv = { ...process.env, ...(context.env ?? {}) };
|
|
2755
|
+
const env = {
|
|
2756
|
+
...baseEnv,
|
|
2757
|
+
...platform.isolationEnv(isolatedHome),
|
|
2758
|
+
};
|
|
1984
2759
|
|
|
1985
|
-
//
|
|
1986
|
-
//
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2760
|
+
// Resolve frozen timeoutMs from the expanded action (top-level).
|
|
2761
|
+
// Default to 300000 for old plans. Fail closed on invalid values.
|
|
2762
|
+
let frozenTimeoutMs;
|
|
2763
|
+
try {
|
|
2764
|
+
frozenTimeoutMs = resolveTimeoutMs(action);
|
|
2765
|
+
} catch (timeoutErr) {
|
|
2766
|
+
return createResult({
|
|
2767
|
+
actionType,
|
|
2768
|
+
status: ActionStatus.OBSERVED,
|
|
2769
|
+
observation: { installed: false, error: timeoutErr.message },
|
|
2770
|
+
error: timeoutErr.message,
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// Non-automatable platform protocol capability gap (BLOCKER-1):
|
|
2775
|
+
// there is NO `kimi plugins list --json` interface. observe never
|
|
2776
|
+
// execs a kimi command. Instead it consumes a structured human
|
|
2777
|
+
// attestation (written after the interactive install) bound to the
|
|
2778
|
+
// frozen plan digest and expected identity, then performs read-only
|
|
2779
|
+
// verification of the installed managed copy: payload digest vs the
|
|
2780
|
+
// sealed authority, entry skill resolved via the manifest skills
|
|
2781
|
+
// root (MAJOR-4), and manifest name/version.
|
|
2782
|
+
// Missing/expired/mismatched/escaping proof fails closed so a kimi
|
|
2783
|
+
// unit can never reach VERIFIED without it.
|
|
2784
|
+
const kimiRoute = platform ? resolvePlatformRoute(platform) : null;
|
|
2785
|
+
if (platform && kimiRoute?.route === 'human-attestation' && actionType === ActionType.KIMI_MARKETPLACE_INSTALL) {
|
|
1998
2786
|
const expectedRef = action.ref ?? `v${action.version}`;
|
|
1999
2787
|
|
|
2000
2788
|
// Bind to the REAL frozen plan digest (A). Fail closed if the
|
|
2001
2789
|
// context does not carry an intact frozen plan.
|
|
2002
2790
|
let boundPlanDigest;
|
|
2003
2791
|
try {
|
|
2004
|
-
boundPlanDigest =
|
|
2792
|
+
boundPlanDigest = resolveBoundPlanDigest(context);
|
|
2005
2793
|
} catch (planErr) {
|
|
2006
2794
|
return createResult({
|
|
2007
2795
|
actionType,
|
|
2008
2796
|
status: ActionStatus.OBSERVED,
|
|
2009
2797
|
observation: {
|
|
2010
2798
|
installed: false,
|
|
2011
|
-
error: `cannot bind
|
|
2799
|
+
error: `cannot bind kimi observation to the frozen plan: ${planErr.message}`,
|
|
2012
2800
|
},
|
|
2013
2801
|
});
|
|
2014
2802
|
}
|
|
2015
2803
|
|
|
2016
|
-
// Stable, cross-run attestation authority (B)
|
|
2017
|
-
//
|
|
2018
|
-
//
|
|
2804
|
+
// Stable, cross-run attestation authority (B). The requirement and
|
|
2805
|
+
// attestation live here, keyed by the verified plan digest + plugin,
|
|
2806
|
+
// so they survive across fresh runDirs (publish, verify each use a
|
|
2807
|
+
// new runDir).
|
|
2019
2808
|
let attestationDir;
|
|
2020
2809
|
try {
|
|
2021
|
-
attestationDir =
|
|
2810
|
+
attestationDir = kimiAuthorityDir(context, boundPlanDigest, action.plugin);
|
|
2022
2811
|
} catch (dirErr) {
|
|
2023
2812
|
return createResult({
|
|
2024
2813
|
actionType,
|
|
@@ -2031,7 +2820,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2031
2820
|
// and the frozen plan digest.
|
|
2032
2821
|
let requirement = null;
|
|
2033
2822
|
try {
|
|
2034
|
-
requirement = JSON.parse(await readFile(resolve(attestationDir,
|
|
2823
|
+
requirement = JSON.parse(await readFile(resolve(attestationDir, KIMI_REQUIREMENT_FILE), 'utf8'));
|
|
2035
2824
|
} catch {
|
|
2036
2825
|
return createResult({
|
|
2037
2826
|
actionType,
|
|
@@ -2039,34 +2828,35 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2039
2828
|
observation: {
|
|
2040
2829
|
installed: false,
|
|
2041
2830
|
manualInstallRequired: true,
|
|
2042
|
-
error: '
|
|
2831
|
+
error: 'kimi manual-install requirement is missing; run execute first',
|
|
2043
2832
|
},
|
|
2044
2833
|
});
|
|
2045
2834
|
}
|
|
2835
|
+
// 完整 requirement 绑定验证:所有冻结动作绑定字段必须一致
|
|
2046
2836
|
if (
|
|
2047
2837
|
requirement.planDigest !== boundPlanDigest ||
|
|
2048
2838
|
requirement.plugin !== action.plugin ||
|
|
2049
2839
|
requirement.version !== action.version ||
|
|
2050
|
-
requirement.entrySkill !== action.entrySkill ||
|
|
2051
2840
|
requirement.repo !== action.repo ||
|
|
2052
|
-
requirement.ref !==
|
|
2841
|
+
requirement.ref !== (action.ref ?? `v${action.version}`) ||
|
|
2842
|
+
(action.entrySkill && requirement.entrySkill !== action.entrySkill)
|
|
2053
2843
|
) {
|
|
2054
2844
|
return createResult({
|
|
2055
2845
|
actionType,
|
|
2056
2846
|
status: ActionStatus.OBSERVED,
|
|
2057
2847
|
observation: {
|
|
2058
2848
|
installed: false,
|
|
2059
|
-
error: '
|
|
2849
|
+
error: 'kimi manual-install requirement does not match the frozen plan/action',
|
|
2060
2850
|
},
|
|
2061
2851
|
});
|
|
2062
2852
|
}
|
|
2063
2853
|
|
|
2064
2854
|
// The trusted human attestation is mandatory and is read from the
|
|
2065
|
-
// stable authority dir. Without it the
|
|
2066
|
-
// proven: fail closed.
|
|
2855
|
+
// stable authority dir. Without it the interactive install has not
|
|
2856
|
+
// been proven: fail closed.
|
|
2067
2857
|
let attestation = null;
|
|
2068
2858
|
try {
|
|
2069
|
-
attestation = JSON.parse(await readFile(resolve(attestationDir,
|
|
2859
|
+
attestation = JSON.parse(await readFile(resolve(attestationDir, KIMI_ATTESTATION_FILE), 'utf8'));
|
|
2070
2860
|
} catch {
|
|
2071
2861
|
return createResult({
|
|
2072
2862
|
actionType,
|
|
@@ -2074,14 +2864,14 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2074
2864
|
observation: {
|
|
2075
2865
|
installed: false,
|
|
2076
2866
|
manualInstallRequired: true,
|
|
2077
|
-
|
|
2867
|
+
installUrl: requirement.installUrl,
|
|
2078
2868
|
attestationDir,
|
|
2079
|
-
error: `
|
|
2869
|
+
error: `kimi attestation is missing; write ${resolve(attestationDir, KIMI_ATTESTATION_FILE)} after the interactive install (${requirement.installUrl})`,
|
|
2080
2870
|
},
|
|
2081
2871
|
});
|
|
2082
2872
|
}
|
|
2083
2873
|
|
|
2084
|
-
const attestationCheck =
|
|
2874
|
+
const attestationCheck = validateKimiAttestation(attestation, action, new Date().toISOString(), boundPlanDigest);
|
|
2085
2875
|
if (!attestationCheck.valid) {
|
|
2086
2876
|
return createResult({
|
|
2087
2877
|
actionType,
|
|
@@ -2090,193 +2880,400 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2090
2880
|
});
|
|
2091
2881
|
}
|
|
2092
2882
|
|
|
2093
|
-
//
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
//
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
// the path to resolve to a real directory.
|
|
2100
|
-
const installPath = resolve(attestation.installPath);
|
|
2101
|
-
let installPathStat;
|
|
2102
|
-
try {
|
|
2103
|
-
installPathStat = await lstat(installPath);
|
|
2104
|
-
} catch {
|
|
2105
|
-
return createResult({
|
|
2106
|
-
actionType,
|
|
2107
|
-
status: ActionStatus.OBSERVED,
|
|
2108
|
-
observation: { installed: false, error: `codebuddy install path does not exist: ${attestation.installPath}` },
|
|
2109
|
-
});
|
|
2110
|
-
}
|
|
2111
|
-
if (installPathStat.isSymbolicLink()) {
|
|
2112
|
-
return createResult({
|
|
2113
|
-
actionType,
|
|
2114
|
-
status: ActionStatus.OBSERVED,
|
|
2115
|
-
observation: { installed: false, error: `codebuddy install path must not be a symlink: ${attestation.installPath}` },
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
if (!installPathStat.isDirectory()) {
|
|
2119
|
-
return createResult({
|
|
2120
|
-
actionType,
|
|
2121
|
-
status: ActionStatus.OBSERVED,
|
|
2122
|
-
observation: { installed: false, error: `codebuddy install path must be a directory: ${attestation.installPath}` },
|
|
2123
|
-
});
|
|
2124
|
-
}
|
|
2125
|
-
const installPathReal = await realpath(installPath).catch(() => null);
|
|
2126
|
-
if (!installPathReal) {
|
|
2883
|
+
// 使用归一化后的 attestation(兼容旧格式)
|
|
2884
|
+
const normalizedAttestation = attestationCheck.normalized;
|
|
2885
|
+
|
|
2886
|
+
// 统一人工判定:如果结果是 failed,直接返回失败
|
|
2887
|
+
if (normalizedAttestation.result === 'failed') {
|
|
2888
|
+
const errorMsg = `kimi human result: failed${normalizedAttestation.note ? ` (${normalizedAttestation.note})` : ''}`;
|
|
2127
2889
|
return createResult({
|
|
2128
2890
|
actionType,
|
|
2129
2891
|
status: ActionStatus.OBSERVED,
|
|
2130
|
-
|
|
2892
|
+
error: errorMsg,
|
|
2893
|
+
observation: {
|
|
2894
|
+
installed: false,
|
|
2895
|
+
humanConfirmed: true,
|
|
2896
|
+
result: 'failed',
|
|
2897
|
+
actor: normalizedAttestation.actor,
|
|
2898
|
+
confirmedAt: normalizedAttestation.confirmedAt,
|
|
2899
|
+
error: errorMsg,
|
|
2900
|
+
},
|
|
2131
2901
|
});
|
|
2132
2902
|
}
|
|
2133
2903
|
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
const
|
|
2137
|
-
|
|
2904
|
+
// installPath 验证:必须在 managed root 内,不得逃逸,不得是符号链接
|
|
2905
|
+
if (normalizedAttestation.installPath) {
|
|
2906
|
+
const managedRoot = resolve(attestationDir, 'kimi-home', 'plugins', 'managed');
|
|
2907
|
+
const installPathAbs = resolve(normalizedAttestation.installPath);
|
|
2908
|
+
|
|
2909
|
+
// 先验证 managed root 存在(KIMI_CODE_HOME 检查)
|
|
2910
|
+
const managedRootReal = await realpath(managedRoot).catch(() => null);
|
|
2911
|
+
if (!managedRootReal) {
|
|
2138
2912
|
return createResult({
|
|
2139
2913
|
actionType,
|
|
2140
2914
|
status: ActionStatus.OBSERVED,
|
|
2141
2915
|
observation: {
|
|
2142
2916
|
installed: false,
|
|
2143
|
-
error:
|
|
2917
|
+
error: 'KIMI_CODE_HOME does not exist',
|
|
2144
2918
|
},
|
|
2145
2919
|
});
|
|
2146
2920
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2921
|
+
|
|
2922
|
+
// 检查 installPath 词法路径是否存在(lstat 在 realpath 之前)
|
|
2923
|
+
let lexicalStat;
|
|
2924
|
+
try {
|
|
2925
|
+
lexicalStat = await lstat(installPathAbs);
|
|
2926
|
+
} catch {
|
|
2150
2927
|
return createResult({
|
|
2151
2928
|
actionType,
|
|
2152
2929
|
status: ActionStatus.OBSERVED,
|
|
2153
2930
|
observation: {
|
|
2154
2931
|
installed: false,
|
|
2155
|
-
error:
|
|
2932
|
+
error: 'managed plugin root does not exist',
|
|
2156
2933
|
},
|
|
2157
2934
|
});
|
|
2158
2935
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2936
|
+
if (lexicalStat.isSymbolicLink()) {
|
|
2937
|
+
return createResult({
|
|
2938
|
+
actionType,
|
|
2939
|
+
status: ActionStatus.OBSERVED,
|
|
2940
|
+
observation: {
|
|
2941
|
+
installed: false,
|
|
2942
|
+
error: `kimi attestation installPath must not be a symlink: ${normalizedAttestation.installPath}`,
|
|
2943
|
+
},
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
const installPathReal = await realpath(installPathAbs).catch(() => null);
|
|
2948
|
+
if (!installPathReal) {
|
|
2165
2949
|
return createResult({
|
|
2166
2950
|
actionType,
|
|
2167
2951
|
status: ActionStatus.OBSERVED,
|
|
2168
2952
|
observation: {
|
|
2169
2953
|
installed: false,
|
|
2170
|
-
error: `
|
|
2954
|
+
error: `kimi attestation installPath does not exist: ${normalizedAttestation.installPath}`,
|
|
2955
|
+
},
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
const rel = relative(managedRootReal, installPathReal);
|
|
2959
|
+
const sep = process.platform === 'win32' ? '\\' : '/';
|
|
2960
|
+
if (rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)) {
|
|
2961
|
+
return createResult({
|
|
2962
|
+
actionType,
|
|
2963
|
+
status: ActionStatus.OBSERVED,
|
|
2964
|
+
observation: {
|
|
2965
|
+
installed: false,
|
|
2966
|
+
error: `kimi attestation installPath escapes the managed root: ${normalizedAttestation.installPath}`,
|
|
2171
2967
|
},
|
|
2172
2968
|
});
|
|
2173
2969
|
}
|
|
2174
2970
|
}
|
|
2175
2971
|
|
|
2176
|
-
//
|
|
2177
|
-
//
|
|
2178
|
-
let manifestDigest;
|
|
2972
|
+
// 载荷绑定验证:旧格式含 installPath 时必须绑定冻结载荷并失败关闭。
|
|
2973
|
+
// 身份、版本、载荷、市场来源不一致绝不能被人工 passed 覆盖。
|
|
2974
|
+
let manifestDigest = action.manifestDigest;
|
|
2179
2975
|
let payloadBinding = null;
|
|
2976
|
+
if (normalizedAttestation.installPath) {
|
|
2977
|
+
try {
|
|
2978
|
+
payloadBinding = await verifyInstalledMarketplacePayload(
|
|
2979
|
+
action,
|
|
2980
|
+
context,
|
|
2981
|
+
normalizedAttestation.installPath,
|
|
2982
|
+
consumer,
|
|
2983
|
+
);
|
|
2984
|
+
manifestDigest = payloadBinding.manifestDigest;
|
|
2985
|
+
} catch (bindingErr) {
|
|
2986
|
+
return createResult({
|
|
2987
|
+
actionType,
|
|
2988
|
+
status: ActionStatus.OBSERVED,
|
|
2989
|
+
error: `kimi payload binding failed: ${bindingErr.message}`,
|
|
2990
|
+
observation: {
|
|
2991
|
+
installed: false,
|
|
2992
|
+
humanConfirmed: true,
|
|
2993
|
+
result: 'passed',
|
|
2994
|
+
actor: normalizedAttestation.actor,
|
|
2995
|
+
confirmedAt: normalizedAttestation.confirmedAt,
|
|
2996
|
+
error: `kimi payload binding failed: ${bindingErr.message}`,
|
|
2997
|
+
},
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// 统一人工判定:返回人工确认成功
|
|
3003
|
+
// 新格式不含 installPath 时,成功依赖已通过的 preflight 静态冻结校验和人工结果绑定
|
|
3004
|
+
return createResult({
|
|
3005
|
+
actionType,
|
|
3006
|
+
status: ActionStatus.OBSERVED,
|
|
3007
|
+
observation: {
|
|
3008
|
+
installed: true,
|
|
3009
|
+
humanConfirmed: true,
|
|
3010
|
+
result: 'passed',
|
|
3011
|
+
actor: normalizedAttestation.actor,
|
|
3012
|
+
confirmedAt: normalizedAttestation.confirmedAt,
|
|
3013
|
+
consumer,
|
|
3014
|
+
plugin: action.plugin,
|
|
3015
|
+
version: action.version,
|
|
3016
|
+
repo: action.repo,
|
|
3017
|
+
ref: expectedRef,
|
|
3018
|
+
entrySkill: action.entrySkill,
|
|
3019
|
+
entrySkillFound: true,
|
|
3020
|
+
manifestDigest,
|
|
3021
|
+
planDigest: boundPlanDigest,
|
|
3022
|
+
...extraInstalledPathsAudit(payloadBinding),
|
|
3023
|
+
},
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// CodeBuddy protocol capability gap (parallel to the kimi branch
|
|
3028
|
+
// above). The codebuddy CLI cannot pin a frozen ref, so observe
|
|
3029
|
+
// never execs a codebuddy command. It consumes a unified human
|
|
3030
|
+
// result bound to the frozen plan digest + identity.
|
|
3031
|
+
// Missing/mismatched proof fails closed so a codebuddy unit can
|
|
3032
|
+
// never reach VERIFIED without it.
|
|
3033
|
+
const codebuddyRoute = platform ? resolvePlatformRoute(platform) : null;
|
|
3034
|
+
if (platform && codebuddyRoute?.route === 'human-attestation' && actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL) {
|
|
3035
|
+
const expectedRef = action.ref ?? `v${action.version}`;
|
|
3036
|
+
|
|
3037
|
+
// Bind to the REAL frozen plan digest (A). Fail closed if the
|
|
3038
|
+
// context does not carry an intact frozen plan.
|
|
3039
|
+
let boundPlanDigest;
|
|
2180
3040
|
try {
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
} catch (digestErr) {
|
|
3041
|
+
boundPlanDigest = await resolveCodeBuddyBoundPlanDigest(context);
|
|
3042
|
+
} catch (planErr) {
|
|
2184
3043
|
return createResult({
|
|
2185
3044
|
actionType,
|
|
2186
3045
|
status: ActionStatus.OBSERVED,
|
|
2187
3046
|
observation: {
|
|
2188
|
-
installed:
|
|
2189
|
-
|
|
2190
|
-
error: `failed to bind installed codebuddy payload to frozen authority: ${digestErr.message}`,
|
|
3047
|
+
installed: false,
|
|
3048
|
+
error: `cannot bind codebuddy observation to the frozen plan: ${planErr.message}`,
|
|
2191
3049
|
},
|
|
2192
3050
|
});
|
|
2193
3051
|
}
|
|
2194
|
-
|
|
3052
|
+
|
|
3053
|
+
// Stable, cross-run attestation authority (B), keyed by the verified
|
|
3054
|
+
// plan digest + plugin, so it survives across fresh runDirs.
|
|
3055
|
+
let attestationDir;
|
|
3056
|
+
try {
|
|
3057
|
+
attestationDir = codebuddyAuthorityDir(context, boundPlanDigest, action.plugin);
|
|
3058
|
+
} catch (dirErr) {
|
|
3059
|
+
return createResult({
|
|
3060
|
+
actionType,
|
|
3061
|
+
status: ActionStatus.OBSERVED,
|
|
3062
|
+
observation: { installed: false, error: dirErr.message },
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// The execute-emitted requirement must exist and bind to the action
|
|
3067
|
+
// and the frozen plan digest.
|
|
3068
|
+
let requirement = null;
|
|
3069
|
+
try {
|
|
3070
|
+
requirement = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_REQUIREMENT_FILE), 'utf8'));
|
|
3071
|
+
} catch {
|
|
2195
3072
|
return createResult({
|
|
2196
3073
|
actionType,
|
|
2197
3074
|
status: ActionStatus.OBSERVED,
|
|
2198
3075
|
observation: {
|
|
2199
|
-
installed:
|
|
2200
|
-
|
|
2201
|
-
error: '
|
|
3076
|
+
installed: false,
|
|
3077
|
+
manualInstallRequired: true,
|
|
3078
|
+
error: 'codebuddy manual-install requirement is missing; run execute first',
|
|
2202
3079
|
},
|
|
2203
3080
|
});
|
|
2204
3081
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
3082
|
+
// 完整 requirement 绑定验证:所有冻结动作绑定字段必须一致
|
|
3083
|
+
if (
|
|
3084
|
+
requirement.planDigest !== boundPlanDigest ||
|
|
3085
|
+
requirement.plugin !== action.plugin ||
|
|
3086
|
+
requirement.version !== action.version ||
|
|
3087
|
+
requirement.repo !== action.repo ||
|
|
3088
|
+
requirement.ref !== (action.ref ?? `v${action.version}`) ||
|
|
3089
|
+
(action.entrySkill && requirement.entrySkill !== action.entrySkill)
|
|
3090
|
+
) {
|
|
2214
3091
|
return createResult({
|
|
2215
3092
|
actionType,
|
|
2216
3093
|
status: ActionStatus.OBSERVED,
|
|
2217
3094
|
observation: {
|
|
2218
|
-
installed:
|
|
2219
|
-
|
|
2220
|
-
manifestDigest,
|
|
2221
|
-
entrySkillFound: false,
|
|
2222
|
-
error: `codebuddy entry skill not resolvable in installed copy: ${entryErr.message}`,
|
|
3095
|
+
installed: false,
|
|
3096
|
+
error: 'codebuddy manual-install requirement does not match the frozen plan/action',
|
|
2223
3097
|
},
|
|
2224
3098
|
});
|
|
2225
3099
|
}
|
|
2226
3100
|
|
|
2227
|
-
//
|
|
2228
|
-
|
|
3101
|
+
// The trusted human attestation is mandatory and is read from the
|
|
3102
|
+
// stable authority dir. Without it the manual install has not been
|
|
3103
|
+
// proven: fail closed.
|
|
3104
|
+
let attestation = null;
|
|
3105
|
+
try {
|
|
3106
|
+
attestation = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE), 'utf8'));
|
|
3107
|
+
} catch {
|
|
2229
3108
|
return createResult({
|
|
2230
3109
|
actionType,
|
|
2231
3110
|
status: ActionStatus.OBSERVED,
|
|
2232
3111
|
observation: {
|
|
2233
|
-
installed:
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
error: `
|
|
3112
|
+
installed: false,
|
|
3113
|
+
manualInstallRequired: true,
|
|
3114
|
+
marketplaceSource: CODEBUDDY_MARKETPLACE_SOURCE,
|
|
3115
|
+
attestationDir,
|
|
3116
|
+
error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install (marketplace ${CODEBUDDY_MARKETPLACE_SOURCE})`,
|
|
2238
3117
|
},
|
|
2239
3118
|
});
|
|
2240
3119
|
}
|
|
2241
|
-
|
|
3120
|
+
|
|
3121
|
+
const attestationCheck = validateCodeBuddyAttestation(attestation, action, new Date().toISOString(), boundPlanDigest);
|
|
3122
|
+
if (!attestationCheck.valid) {
|
|
3123
|
+
return createResult({
|
|
3124
|
+
actionType,
|
|
3125
|
+
status: ActionStatus.OBSERVED,
|
|
3126
|
+
observation: { installed: false, error: attestationCheck.error },
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// 使用归一化后的 attestation(兼容旧格式)
|
|
3131
|
+
const normalizedAttestation = attestationCheck.normalized;
|
|
3132
|
+
|
|
3133
|
+
// 统一人工判定:如果结果是 failed,直接返回失败
|
|
3134
|
+
if (normalizedAttestation.result === 'failed') {
|
|
3135
|
+
const errorMsg = `codebuddy human result: failed${normalizedAttestation.note ? ` (${normalizedAttestation.note})` : ''}`;
|
|
2242
3136
|
return createResult({
|
|
2243
3137
|
actionType,
|
|
2244
3138
|
status: ActionStatus.OBSERVED,
|
|
3139
|
+
error: errorMsg,
|
|
2245
3140
|
observation: {
|
|
2246
|
-
installed:
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
3141
|
+
installed: false,
|
|
3142
|
+
humanConfirmed: true,
|
|
3143
|
+
result: 'failed',
|
|
3144
|
+
actor: normalizedAttestation.actor,
|
|
3145
|
+
confirmedAt: normalizedAttestation.confirmedAt,
|
|
3146
|
+
error: errorMsg,
|
|
2251
3147
|
},
|
|
2252
3148
|
});
|
|
2253
3149
|
}
|
|
2254
3150
|
|
|
2255
|
-
//
|
|
2256
|
-
// marketplace + installChannel ARE validated codebuddy identity
|
|
2257
|
-
// fields (unlike kimi's MINOR-1 marketplace exclusion).
|
|
2258
|
-
const observation = {
|
|
2259
|
-
installed: true,
|
|
2260
|
-
installPath: installPathReal,
|
|
2261
|
-
entrySkillFound: true,
|
|
2262
|
-
entrySkill: action.entrySkill,
|
|
2263
|
-
manifestDigest,
|
|
2264
|
-
consumer,
|
|
2265
|
-
plugin: installedManifest.name,
|
|
2266
|
-
version: installedManifest.version,
|
|
2267
|
-
repo: action.repo,
|
|
2268
|
-
ref: expectedRef,
|
|
2269
|
-
marketplace: attestation.marketplace,
|
|
2270
|
-
installChannel: attestation.installChannel,
|
|
2271
|
-
...extraInstalledPathsAudit(payloadBinding),
|
|
2272
|
-
};
|
|
3151
|
+
// 统一人工判定:返回人工确认成功(不再进行自动化验证)
|
|
2273
3152
|
return createResult({
|
|
2274
3153
|
actionType,
|
|
2275
3154
|
status: ActionStatus.OBSERVED,
|
|
2276
|
-
observation
|
|
3155
|
+
observation: {
|
|
3156
|
+
installed: true,
|
|
3157
|
+
humanConfirmed: true,
|
|
3158
|
+
result: 'passed',
|
|
3159
|
+
actor: normalizedAttestation.actor,
|
|
3160
|
+
confirmedAt: normalizedAttestation.confirmedAt,
|
|
3161
|
+
consumer,
|
|
3162
|
+
plugin: action.plugin,
|
|
3163
|
+
version: action.version,
|
|
3164
|
+
repo: action.repo,
|
|
3165
|
+
ref: expectedRef,
|
|
3166
|
+
entrySkill: action.entrySkill,
|
|
3167
|
+
entrySkillFound: true,
|
|
3168
|
+
manifestDigest: action.manifestDigest,
|
|
3169
|
+
planDigest: boundPlanDigest,
|
|
3170
|
+
},
|
|
2277
3171
|
});
|
|
2278
3172
|
}
|
|
2279
3173
|
|
|
3174
|
+
// Codex human-attestation fallback: if the CLI was unavailable
|
|
3175
|
+
// during execute, a human attestation may exist in the authority dir.
|
|
3176
|
+
// Read and validate it instead of the CLI evidence.
|
|
3177
|
+
//
|
|
3178
|
+
// CRITICAL: observe only reads human results when a bound, validated
|
|
3179
|
+
// Codex manual requirement exists. Isolated attestation files must not
|
|
3180
|
+
// bypass automatic evidence.
|
|
3181
|
+
if (
|
|
3182
|
+
platform.degradationPolicy === 'human-attestation-with-fallback'
|
|
3183
|
+
&& actionType === ActionType.CODEX_MARKETPLACE_INSTALL
|
|
3184
|
+
) {
|
|
3185
|
+
const expectedRef = action.ref ?? `v${action.version}`;
|
|
3186
|
+
|
|
3187
|
+
// Try to read the human attestation from the authority dir.
|
|
3188
|
+
let codexBoundPlanDigest;
|
|
3189
|
+
try {
|
|
3190
|
+
codexBoundPlanDigest = await resolveCodexBoundPlanDigest(context);
|
|
3191
|
+
} catch {
|
|
3192
|
+
// No frozen plan — cannot read attestation
|
|
3193
|
+
}
|
|
3194
|
+
if (codexBoundPlanDigest) {
|
|
3195
|
+
let codexAttestationDir;
|
|
3196
|
+
try {
|
|
3197
|
+
codexAttestationDir = codexAuthorityDir(context, codexBoundPlanDigest, action.plugin);
|
|
3198
|
+
} catch {
|
|
3199
|
+
// Authority dir not resolvable
|
|
3200
|
+
}
|
|
3201
|
+
if (codexAttestationDir) {
|
|
3202
|
+
// CRITICAL: Only read attestation if requirement exists and is bound
|
|
3203
|
+
let codexRequirement = null;
|
|
3204
|
+
try {
|
|
3205
|
+
codexRequirement = JSON.parse(await readFile(resolve(codexAttestationDir, CODEX_REQUIREMENT_FILE), 'utf8'));
|
|
3206
|
+
} catch {
|
|
3207
|
+
// No requirement file — must not read isolated attestation
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
// Validate requirement binds to this action and plan (完整绑定验证)
|
|
3211
|
+
if (
|
|
3212
|
+
codexRequirement
|
|
3213
|
+
&& codexRequirement.planDigest === codexBoundPlanDigest
|
|
3214
|
+
&& codexRequirement.plugin === action.plugin
|
|
3215
|
+
&& codexRequirement.version === action.version
|
|
3216
|
+
&& codexRequirement.repo === action.repo
|
|
3217
|
+
&& codexRequirement.ref === (action.ref ?? `v${action.version}`)
|
|
3218
|
+
&& (!action.entrySkill || codexRequirement.entrySkill === action.entrySkill)
|
|
3219
|
+
) {
|
|
3220
|
+
let codexAttestation = null;
|
|
3221
|
+
try {
|
|
3222
|
+
codexAttestation = JSON.parse(await readFile(resolve(codexAttestationDir, CODEX_ATTESTATION_FILE), 'utf8'));
|
|
3223
|
+
} catch {
|
|
3224
|
+
// No attestation file — fall through to normal CLI evidence path
|
|
3225
|
+
}
|
|
3226
|
+
if (codexAttestation) {
|
|
3227
|
+
const codexCheck = validateCodexAttestation(codexAttestation, action, new Date().toISOString(), codexBoundPlanDigest);
|
|
3228
|
+
if (!codexCheck.valid) {
|
|
3229
|
+
return createResult({
|
|
3230
|
+
actionType,
|
|
3231
|
+
status: ActionStatus.OBSERVED,
|
|
3232
|
+
observation: { installed: false, error: codexCheck.error },
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
if (codexAttestation.result === 'failed') {
|
|
3236
|
+
const errorMsg = `codex human result: failed${codexAttestation.note ? ` (${codexAttestation.note})` : ''}`;
|
|
3237
|
+
return createResult({
|
|
3238
|
+
actionType,
|
|
3239
|
+
status: ActionStatus.OBSERVED,
|
|
3240
|
+
error: errorMsg,
|
|
3241
|
+
observation: {
|
|
3242
|
+
installed: false,
|
|
3243
|
+
humanConfirmed: true,
|
|
3244
|
+
result: 'failed',
|
|
3245
|
+
actor: codexAttestation.actor,
|
|
3246
|
+
confirmedAt: codexAttestation.confirmedAt,
|
|
3247
|
+
error: errorMsg,
|
|
3248
|
+
},
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
return createResult({
|
|
3252
|
+
actionType,
|
|
3253
|
+
status: ActionStatus.OBSERVED,
|
|
3254
|
+
observation: {
|
|
3255
|
+
installed: true,
|
|
3256
|
+
humanConfirmed: true,
|
|
3257
|
+
result: 'passed',
|
|
3258
|
+
actor: codexAttestation.actor,
|
|
3259
|
+
confirmedAt: codexAttestation.confirmedAt,
|
|
3260
|
+
consumer,
|
|
3261
|
+
plugin: action.plugin,
|
|
3262
|
+
version: action.version,
|
|
3263
|
+
repo: action.repo,
|
|
3264
|
+
ref: expectedRef,
|
|
3265
|
+
entrySkill: action.entrySkill,
|
|
3266
|
+
entrySkillFound: true,
|
|
3267
|
+
manifestDigest: action.manifestDigest,
|
|
3268
|
+
planDigest: codexBoundPlanDigest,
|
|
3269
|
+
},
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
|
|
2280
3277
|
// Read execute evidence — mandatory for observe validation
|
|
2281
3278
|
let evidence = null;
|
|
2282
3279
|
try {
|