release-skill 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +46 -0
  7. package/CONTRIBUTING.md +27 -0
  8. package/INSTALL.md +95 -139
  9. package/INSTALL.zh-CN.md +70 -121
  10. package/README.md +266 -913
  11. package/README.zh-CN.md +224 -535
  12. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  13. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  14. package/adapters/claude/bin/release-skill.bundle.mjs +19805 -17556
  15. package/adapters/claude/schemas/release-plan.schema.json +228 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +155 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +19805 -17556
  20. package/adapters/codex/schemas/release-plan.schema.json +228 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +155 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +19805 -17556
  25. package/adapters/kimi/schemas/release-plan.schema.json +228 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +155 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +19805 -17556
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +228 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +155 -2
  33. package/bin/release-skill.bundle.mjs +19805 -17556
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +228 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +155 -2
  38. package/scripts/sync-public-files.mjs +20 -9
  39. package/src/adapters/plugin-marketplace.mjs +1237 -403
  40. package/src/commands/prepare.mjs +454 -40
  41. package/src/commands/publish.mjs +169 -75
  42. package/src/commands/reconcile.mjs +92 -327
  43. package/src/commands/setup.mjs +148 -20
  44. package/src/commands/verify.mjs +454 -20
  45. package/src/core/baseline.mjs +8 -1
  46. package/src/core/checkpoints.mjs +50 -7
  47. package/src/core/config.mjs +15 -0
  48. package/src/core/errors.mjs +2 -0
  49. package/src/core/installation-contract.mjs +341 -0
  50. package/src/core/plan.mjs +158 -6
  51. package/src/core/skill-resource-closure.mjs +425 -0
  52. package/src/platforms/codebuddy.mjs +206 -280
  53. package/src/platforms/codex.mjs +369 -0
  54. package/src/platforms/kimi.mjs +191 -119
  55. package/src/platforms/registry.mjs +24 -6
@@ -15,6 +15,11 @@
15
15
  * the installed managed copy. Missing/expired/mismatched/escaping proof
16
16
  * fails closed, so a kimi unit can never reach VERIFIED without it.
17
17
  *
18
+ * 统一人工结果:收据仅需 platform, version, planDigest,
19
+ * result(passed|failed), actor, confirmedAt 和可选 note。
20
+ * 不再要求 consumer, plugin, conclusion, confirmedBy、隔离 HOME、
21
+ * 安装路径证明、载荷摘要手填或 24 小时过期。
22
+ *
18
23
  * This module is the kimi half of the platform registry's strategy table
19
24
  * (registry.mjs references these functions); the plugin-marketplace adapter
20
25
  * consumes the attestation path from here. The shared adapter primitives
@@ -29,7 +34,7 @@
29
34
  * @module platforms/kimi
30
35
  */
31
36
 
32
- import { readFile, mkdir } from 'node:fs/promises';
37
+ import { readFile } from 'node:fs/promises';
33
38
  import { join, resolve, relative, isAbsolute } from 'node:path';
34
39
 
35
40
  import {
@@ -47,10 +52,6 @@ import { canonicalJson } from '../core/digest.mjs';
47
52
  export const KIMI_REQUIREMENT_FILE = 'release-skill-kimi-manual-install.json';
48
53
  /** Structured human attestation consumed by kimi observe. */
49
54
  export const KIMI_ATTESTATION_FILE = 'release-skill-kimi-attestation.json';
50
- /** Kimi Code managed install layout: $KIMI_CODE_HOME/plugins/managed/<id>/. */
51
- export const KIMI_MANAGED_SUBPATH = join('plugins', 'managed');
52
- /** Maximum attestation validity window (mirrors the 24h approval expiry). */
53
- export const KIMI_MAX_ATTESTATION_VALIDITY_MS = 24 * 60 * 60 * 1000;
54
55
 
55
56
  /** 64-char lowercase hex plan/payload digest pattern. */
56
57
  export const HEX_DIGEST_RE = /^[a-f0-9]{64}$/;
@@ -184,21 +185,28 @@ function buildKimiInstallUrl(repo, ref) {
184
185
  }
185
186
 
186
187
  /**
187
- * Human-facing, actionable manual-install closed-loop instructions for Kimi Code.
188
+ * 统一人工安装说明:面向 Kimi Code 的人工结果流程。
188
189
  *
189
- * @param {{installUrl:string, plugin:string, version:string, ref:string, isolatedHome:string, attestationDir:string}} p
190
+ * @param {{installUrl:string, plugin:string, version:string, ref:string, attestationDir:string, requiresInstalledClosure:boolean, managedRoot:string|null}} p
190
191
  * @returns {string[]}
191
192
  */
192
- function buildKimiManualInstructions({ installUrl, plugin, version, ref, isolatedHome, attestationDir }) {
193
+ function buildKimiManualInstructions({
194
+ installUrl,
195
+ plugin,
196
+ version,
197
+ ref,
198
+ attestationDir,
199
+ requiresInstalledClosure,
200
+ managedRoot,
201
+ }) {
193
202
  return [
194
- `Kimi Code has no scriptable plugin-install CLI; installation is a manual, interactive step.`,
195
- `1) publish fails closed at this kimi checkpoint and leaves the run PARTIAL (the automated Git branch/tag, npm, and GitHub Release writes still complete first).`,
196
- `2) Launch Kimi Code with the ISOLATED home from this requirement so the managed copy lands inside it: set HOME="${isolatedHome}" and KIMI_CODE_HOME="${isolatedHome}". The plugin installs to "${isolatedHome}/plugins/managed/${plugin}/".`,
197
- `3) In that isolated Kimi Code session run: /plugins install ${installUrl} (pinned to frozen ref "${ref}", version ${version}; never install the bare repository URL). Confirm the trust prompt for plugin "${plugin}", then run /plugins reload (or /new).`,
198
- `4) Write the attestation JSON to: ${attestationDir}/${KIMI_ATTESTATION_FILE}. planDigest MUST be the frozen plan digest; payloadDigest MUST be the frozen snapshot payload digest; installPath MUST be the isolated managed directory above. attestedAt must not be in the future and expiresAt must be within 24 hours of attestedAt.`,
199
- ` Required fields: consumer="kimi", plugin, version, entrySkill, repo, ref, installPath, planDigest, payloadDigest, attestedBy, attestedAt, expiresAt.`,
200
- `5) Re-run release-skill reconcile (promotes PARTIAL -> PUBLISHED) and then verify (-> VERIFIED). Both read the attestation from this same plan-digest-keyed authority directory, so a fresh run directory does not lose the proof.`,
201
- `An install into the ordinary ~/.kimi-code is NOT acceptable proof: the attested installPath must resolve inside this requirement's isolated KIMI_CODE_HOME managed root, otherwise verification fails closed.`,
203
+ `Kimi Code 没有可脚本化的插件安装命令行工具;安装是手动交互步骤。`,
204
+ `1) publish 完成所有远端写入后进入 PUBLISHED 状态(自动化 Git 分支/标签、npm GitHub Release 写入已完成)。此 kimi 检查点标记为需要人工安装。`,
205
+ `2) ${requiresInstalledClosure ? `以 KIMI_CODE_HOME="${resolve(managedRoot, '..', '..')}" 启动 Kimi Code,然后` : '在 Kimi Code 中'}运行: /plugins install ${installUrl}(锁定到冻结 ref "${ref}",版本 ${version})。确认插件 "${plugin}" 的信任提示,然后运行 /plugins reload(或 /new)。`,
206
+ `3) 将人工结果 JSON 写入: ${attestationDir}/${KIMI_ATTESTATION_FILE}`,
207
+ ` 必填字段: platform="kimi", version, planDigest(冻结计划摘要), result("passed" "failed"), actor(确认人), confirmedAt(ISO 8601 时间戳)${requiresInstalledClosure ? ',installPath(实际安装后的插件目录,必须位于该证明目录的 kimi-home/plugins/managed/ 内)' : ''}`,
208
+ ` 可选字段: note(备注)`,
209
+ `4) 运行 release-skill reconcile(对账远端状态并跳过已完成步骤),然后 release-skill verify(从同一个计划摘要索引的权威目录读取结果,成功后 -> VERIFIED)。`,
202
210
  ];
203
211
  }
204
212
 
@@ -236,82 +244,144 @@ export async function readKimiManifest(pluginRootReal) {
236
244
  }
237
245
 
238
246
  /**
239
- * Validate a structured kimi manual-install attestation against the frozen
240
- * action and the verified frozen plan digest.
247
+ * 统一人工结果验证:验证 kimi 人工结果是否匹配冻结计划。
248
+ *
249
+ * 统一后的结果只需:
250
+ * - 必填:platform, version, planDigest, result(passed|failed), actor, confirmedAt
251
+ * - 可选:note
252
+ *
253
+ * 旧格式兼容(0.2.3 及更早):
254
+ * - consumer → platform (必须是 'kimi')
255
+ * - attestedBy → actor
256
+ * - attestedAt → confirmedAt
257
+ * - payloadDigest → 载荷绑定验证(如果存在)
241
258
  *
242
- * Bindings (fail closed on any mismatch):
243
- * - `planDigest` binds to the REAL frozen plan digest (`boundPlanDigest`, from
244
- * `context.plan.digest`) NOT to `action.manifestDigest`.
245
- * - `payloadDigest` binds separately to `action.manifestDigest` (the sealed
246
- * snapshot payload digest).
247
- * - plugin identity, version, entry skill, repo, and frozen ref must match.
248
- * - Time bounds: `attestedAt` must not be in the future, the validity window
249
- * (`expiresAt - attestedAt`) must not exceed 24h, and the attestation must
250
- * not be expired relative to `isoNow`.
259
+ * 绑定验证(任何不匹配都失败):
260
+ * - planDigest 绑定到真正的冻结计划摘要(boundPlanDigest
261
+ * - version 静态一致性检查
262
+ * - result 只接受 passed failed
251
263
  *
252
- * @param {object} attestation - parsed attestation JSON.
253
- * @param {object} action - the expanded kimi action (top-level fields).
254
- * @param {string} isoNow - current ISO timestamp.
255
- * @param {string} boundPlanDigest - verified frozen plan digest.
256
- * @returns {{valid:boolean, error:string|null}}
264
+ * @param {object} attestation - 解析后的人工结果 JSON
265
+ * @param {object} action - 展开的 kimi 动作(顶层字段)。
266
+ * @param {string} isoNow - 当前 ISO 时间戳(保留签名兼容,不再用于过期检查)。
267
+ * @param {string} boundPlanDigest - 验证过的冻结计划摘要。
268
+ * @returns {{valid:boolean, error:string|null, normalized:object|null}}
257
269
  */
258
270
  export function validateKimiAttestation(attestation, action, isoNow, boundPlanDigest) {
259
271
  if (!attestation || typeof attestation !== 'object' || Array.isArray(attestation)) {
260
- return { valid: false, error: 'kimi attestation is not an object' };
261
- }
262
- const requiredStrings = ['plugin', 'version', 'entrySkill', 'repo', 'ref', 'installPath', 'payloadDigest', 'planDigest', 'attestedBy', 'attestedAt', 'expiresAt'];
263
- for (const field of requiredStrings) {
264
- if (typeof attestation[field] !== 'string' || attestation[field].length === 0) {
265
- return { valid: false, error: `kimi attestation missing required field "${field}"` };
266
- }
272
+ return { valid: false, error: 'kimi attestation is not an object', normalized: null };
267
273
  }
268
- if (attestation.consumer !== 'kimi') {
269
- return { valid: false, error: `kimi attestation consumer "${attestation.consumer}" must be "kimi"` };
270
- }
271
- if (!HEX_DIGEST_RE.test(attestation.planDigest)) {
272
- return { valid: false, error: 'kimi attestation planDigest must be a 64-char lowercase hex digest' };
274
+
275
+ // 旧格式归一化:将旧字段映射到新字段
276
+ const normalized = { ...attestation };
277
+
278
+ // 旧格式识别:完整旧标识组(consumer, attestedBy, attestedAt)全部存在,
279
+ // 且新格式字段组(result, actor, confirmedAt)未混入。
280
+ // 不能通过随意添加一个旧标识字段绕过新格式 result 必填。
281
+ const hasCompleteOldMarkers = !!(normalized.consumer && normalized.attestedBy && normalized.attestedAt);
282
+ const hasNewFormatFields = !!(normalized.result || normalized.actor || normalized.confirmedAt);
283
+ const isOldFormat = hasCompleteOldMarkers && !hasNewFormatFields;
284
+
285
+ // 新格式严格要求 result 字段;仅当确认为旧格式时才允许缺省
286
+ if (!normalized.result && isOldFormat) {
287
+ normalized.result = 'passed';
273
288
  }
274
- if (attestation.planDigest !== boundPlanDigest) {
275
- return { valid: false, error: 'kimi attestation planDigest does not match the frozen plan digest' };
289
+
290
+ // consumer platform (旧格式使用 consumer)
291
+ if (!normalized.platform && normalized.consumer) {
292
+ normalized.platform = normalized.consumer;
276
293
  }
277
- if (attestation.plugin !== action.plugin) {
278
- return { valid: false, error: `kimi attestation plugin "${attestation.plugin}" does not match action plugin "${action.plugin}"` };
294
+ // attestedBy → actor (旧格式使用 attestedBy)
295
+ if (!normalized.actor && normalized.attestedBy) {
296
+ normalized.actor = normalized.attestedBy;
279
297
  }
280
- if (attestation.version !== action.version) {
281
- return { valid: false, error: `kimi attestation version "${attestation.version}" does not match action version "${action.version}"` };
298
+ // attestedAt → confirmedAt (旧格式使用 attestedAt)
299
+ if (!normalized.confirmedAt && normalized.attestedAt) {
300
+ normalized.confirmedAt = normalized.attestedAt;
282
301
  }
283
- if (attestation.entrySkill !== action.entrySkill) {
284
- return { valid: false, error: `kimi attestation entrySkill "${attestation.entrySkill}" does not match action entrySkill "${action.entrySkill}"` };
302
+
303
+ // 统一必填字段
304
+ const requiredStrings = ['platform', 'version', 'planDigest', 'result', 'actor', 'confirmedAt'];
305
+ for (const field of requiredStrings) {
306
+ if (typeof normalized[field] !== 'string' || normalized[field].length === 0) {
307
+ return { valid: false, error: `kimi attestation missing required field "${field}"`, normalized: null };
308
+ }
285
309
  }
286
- if (attestation.repo !== action.repo) {
287
- return { valid: false, error: `kimi attestation repo "${attestation.repo}" does not match action repo "${action.repo}"` };
310
+ if (normalized.platform !== 'kimi') {
311
+ return { valid: false, error: `kimi attestation platform "${normalized.platform}" must be "kimi"`, normalized: null };
288
312
  }
289
- const expectedRef = action.ref ?? `v${action.version}`;
290
- if (attestation.ref !== expectedRef) {
291
- return { valid: false, error: `kimi attestation ref "${attestation.ref}" does not match frozen ref "${expectedRef}"` };
313
+ // result 只接受 passed failed
314
+ if (normalized.result !== 'passed' && normalized.result !== 'failed') {
315
+ return { valid: false, error: `kimi attestation result "${normalized.result}" must be "passed" or "failed"`, normalized: null };
292
316
  }
293
- if (attestation.payloadDigest !== action.manifestDigest) {
294
- return { valid: false, error: 'kimi attestation payloadDigest does not match the frozen payload digest' };
317
+ // planDigest 绑定验证
318
+ if (!HEX_DIGEST_RE.test(normalized.planDigest)) {
319
+ return { valid: false, error: 'kimi attestation planDigest must be a 64-char lowercase hex digest', normalized: null };
295
320
  }
296
- const attestedMs = Date.parse(attestation.attestedAt);
297
- const expiresMs = Date.parse(attestation.expiresAt);
298
- const nowMs = Date.parse(isoNow);
299
- if (!Number.isFinite(attestedMs) || !Number.isFinite(expiresMs) || !Number.isFinite(nowMs)) {
300
- return { valid: false, error: 'kimi attestation attestedAt/expiresAt must be valid ISO timestamps' };
321
+ if (normalized.planDigest !== boundPlanDigest) {
322
+ return { valid: false, error: 'kimi attestation planDigest does not match the frozen plan digest', normalized: null };
301
323
  }
302
- if (attestedMs > nowMs) {
303
- return { valid: false, error: 'kimi attestation attestedAt is in the future' };
324
+ // version 静态一致性检查
325
+ if (normalized.version !== action.version) {
326
+ return { valid: false, error: `kimi attestation version "${normalized.version}" does not match action version "${action.version}"`, normalized: null };
304
327
  }
305
- if (expiresMs <= attestedMs) {
306
- return { valid: false, error: 'kimi attestation expiresAt must be after attestedAt' };
328
+
329
+ // 旧格式额外绑定字段校验:plugin, repo, ref, entrySkill, payloadDigest, installPath
330
+ // 旧格式必须包含完整旧字段组;缺任一旧必填字段失败。
331
+ if (isOldFormat) {
332
+ // plugin 必须匹配
333
+ if (normalized.plugin !== action.plugin) {
334
+ return { valid: false, error: `kimi attestation plugin "${normalized.plugin}" does not match action plugin "${action.plugin}"`, normalized: null };
335
+ }
336
+ // repo 必须匹配
337
+ if (normalized.repo !== action.repo) {
338
+ return { valid: false, error: `kimi attestation repo "${normalized.repo}" does not match action repo "${action.repo}"`, normalized: null };
339
+ }
340
+ // ref 必须匹配
341
+ const expectedRef = action.ref ?? `v${action.version}`;
342
+ if (normalized.ref !== expectedRef) {
343
+ return { valid: false, error: `kimi attestation ref "${normalized.ref}" does not match action ref "${expectedRef}"`, normalized: null };
344
+ }
345
+ // entrySkill 必须匹配
346
+ if (normalized.entrySkill !== action.entrySkill) {
347
+ return { valid: false, error: `kimi attestation entrySkill "${normalized.entrySkill}" does not match action entrySkill "${action.entrySkill}"`, normalized: null };
348
+ }
349
+ // payloadDigest 旧格式必填、格式合法且等于冻结动作 manifestDigest
350
+ if (typeof normalized.payloadDigest !== 'string' || normalized.payloadDigest.length === 0) {
351
+ return { valid: false, error: 'kimi attestation payloadDigest is required for old-format receipts', normalized: null };
352
+ }
353
+ // installPath 旧格式必填:旧格式用 installPath 证明实际安装
354
+ if (typeof normalized.installPath !== 'string' || normalized.installPath.length === 0) {
355
+ return { valid: false, error: 'kimi attestation installPath is required for old-format receipts', normalized: null };
356
+ }
357
+ if (!HEX_DIGEST_RE.test(normalized.payloadDigest)) {
358
+ return { valid: false, error: 'kimi attestation payloadDigest must be a 64-char lowercase hex digest', normalized: null };
359
+ }
360
+ if (action.manifestDigest && normalized.payloadDigest !== action.manifestDigest) {
361
+ return { valid: false, error: 'kimi attestation payloadDigest does not match the frozen manifest digest', normalized: null };
362
+ }
363
+ } else {
364
+ // 新格式 payloadDigest 可选,但如果存在则必须合法
365
+ if (normalized.payloadDigest !== undefined) {
366
+ if (!HEX_DIGEST_RE.test(normalized.payloadDigest)) {
367
+ return { valid: false, error: 'kimi attestation payloadDigest must be a 64-char lowercase hex digest when present', normalized: null };
368
+ }
369
+ if (action.manifestDigest && normalized.payloadDigest !== action.manifestDigest) {
370
+ return { valid: false, error: 'kimi attestation payloadDigest does not match the frozen manifest digest', normalized: null };
371
+ }
372
+ }
307
373
  }
308
- if (expiresMs - attestedMs > KIMI_MAX_ATTESTATION_VALIDITY_MS) {
309
- return { valid: false, error: 'kimi attestation validity must not exceed 24 hours' };
374
+
375
+ // confirmedAt 必须是有效的时间戳
376
+ const confirmedMs = Date.parse(normalized.confirmedAt);
377
+ if (!Number.isFinite(confirmedMs)) {
378
+ return { valid: false, error: 'kimi attestation confirmedAt must be a valid ISO timestamp', normalized: null };
310
379
  }
311
- if (nowMs > expiresMs) {
312
- return { valid: false, error: 'kimi attestation has expired' };
380
+ // 可选字段 note 如果存在必须是字符串
381
+ if (normalized.note !== undefined && typeof normalized.note !== 'string') {
382
+ return { valid: false, error: 'kimi attestation note must be a string when present', normalized: null };
313
383
  }
314
- return { valid: true, error: null };
384
+ return { valid: true, error: null, normalized };
315
385
  }
316
386
 
317
387
  /**
@@ -323,15 +393,8 @@ export function validateKimiAttestation(attestation, action, isoNow, boundPlanDi
323
393
  * read-only verification. Without that proof the checkpoint fails closed and can
324
394
  * never reach VERIFIED.
325
395
  *
326
- * Isolation model (B/C): the kimi home is a STABLE, plan-digest-keyed directory
327
- * under the attestation authority (`<authorityDir>/kimi-home`), not the per-run
328
- * runDir consumer dir. The operator launches Kimi Code with that KIMI_CODE_HOME
329
- * so the managed copy lands at `<kimiHome>/plugins/managed/<plugin>/`, a
330
- * location that is identical across publish/reconcile/verify run dirs. execute
331
- * creates ONLY the managed parent (`plugins/managed`), never `managed/<plugin>`
332
- * (the operator's interactive install creates that). The requirement write is
333
- * idempotent: an identical existing requirement is left untouched, a divergent
334
- * one fails closed.
396
+ * 统一人工判定:不再创建隔离目录,不再要求隔离 HOME。
397
+ * 人工结果只需 platform, version, planDigest, result, actor, confirmedAt。
335
398
  *
336
399
  * Referenced from the registry as the kimi strategy.buildManualRequirement —
337
400
  * the automatable=false manual-requirement path.
@@ -370,8 +433,8 @@ export async function executeKimiManualRequirement(action, context) {
370
433
  const ref = action.ref ?? `v${action.version}`;
371
434
  const installUrl = buildKimiInstallUrl(action.repo, ref);
372
435
 
373
- // (B) Stable, plan-digest-keyed authority dir — the ONLY kimi home, shared
374
- // across publish/reconcile/verify run dirs.
436
+ // (B) Stable, plan-digest-keyed authority dir, shared across
437
+ // publish/reconcile/verify run dirs.
375
438
  let attestationDir;
376
439
  try {
377
440
  attestationDir = kimiAuthorityDir(context, planDigest, action.plugin);
@@ -382,65 +445,78 @@ export async function executeKimiManualRequirement(action, context) {
382
445
  error: dirErr.message,
383
446
  });
384
447
  }
385
- const kimiHome = resolve(attestationDir, 'kimi-home');
386
- const managedParent = resolve(kimiHome, KIMI_MANAGED_SUBPATH); // plugins/managed
387
- // plugins/managed/<plugin> — created by the operator's interactive install.
388
- const managedInstallRoot = resolve(managedParent, action.plugin);
389
448
 
449
+ const requiresInstalledClosure = Boolean(context.plan?.skillResourceClosure);
450
+ const managedRoot = requiresInstalledClosure
451
+ ? resolve(attestationDir, 'kimi-home', 'plugins', 'managed')
452
+ : null;
390
453
  const instructions = buildKimiManualInstructions({
391
454
  installUrl,
392
455
  plugin: action.plugin,
393
456
  version: action.version,
394
457
  ref,
395
- isolatedHome: kimiHome,
396
458
  attestationDir,
459
+ requiresInstalledClosure,
460
+ managedRoot,
397
461
  });
398
462
 
463
+ // 统一 requirement 结构:不再包含隔离目录信息
399
464
  const requirement = {
400
465
  kind: 'kimi-manual-install-requirement',
401
- consumer: 'kimi',
466
+ platform: 'kimi',
402
467
  plugin: action.plugin,
403
468
  version: action.version,
404
- entrySkill: action.entrySkill,
405
469
  repo: action.repo,
406
470
  ref,
471
+ entrySkill: action.entrySkill,
407
472
  installUrl,
408
- // (A) planDigest binds to the real frozen plan digest;
409
- // expectedPayloadDigest binds separately to the snapshot payload digest.
410
473
  planDigest,
411
- expectedPayloadDigest: action.manifestDigest,
412
- isolatedHome: kimiHome,
413
- kimiCodeHome: kimiHome,
414
- managedInstallRoot,
415
474
  attestationDir,
416
475
  attestationFile: KIMI_ATTESTATION_FILE,
417
476
  attestationTemplate: {
418
- consumer: 'kimi',
419
- plugin: action.plugin,
477
+ platform: 'kimi',
420
478
  version: action.version,
421
- entrySkill: action.entrySkill,
422
- repo: action.repo,
423
- ref,
424
- installPath: managedInstallRoot,
425
479
  planDigest,
426
- payloadDigest: action.manifestDigest,
427
- attestedBy: '<person responsible for the manual install>',
428
- attestedAt: '<ISO 8601 now; must not be in the future>',
429
- expiresAt: '<ISO 8601; within 24h of attestedAt>',
480
+ result: '<"passed" or "failed">',
481
+ actor: '<person who confirmed the install>',
482
+ confirmedAt: '<ISO 8601 timestamp>',
483
+ ...(requiresInstalledClosure
484
+ ? { installPath: resolve(managedRoot, action.plugin) }
485
+ : {}),
486
+ note: '<optional note>',
430
487
  },
431
488
  instructions,
432
489
  };
433
490
 
434
- // Create ONLY the managed parent (plugins/managed); never pre-create
435
- // managed/<plugin> the operator's interactive install creates that.
491
+ // Ensure the authority directory exists (no isolated home creation needed).
492
+ const { mkdir } = await import('node:fs/promises');
436
493
  try {
437
- await mkdir(managedParent, { recursive: true, mode: 0o700 });
494
+ await mkdir(attestationDir, { recursive: true, mode: 0o700 });
438
495
  } catch (mkdirErr) {
439
- return createResult({
440
- actionType,
441
- status: ActionStatus.EXECUTE_FAILED,
442
- error: `cannot create kimi managed parent directory: ${mkdirErr.message}`,
443
- });
496
+ // EEXIST is fine; other errors are fatal.
497
+ if (mkdirErr?.code !== 'EEXIST') {
498
+ return createResult({
499
+ actionType,
500
+ status: ActionStatus.EXECUTE_FAILED,
501
+ error: `cannot create kimi attestation directory: ${mkdirErr.message}`,
502
+ });
503
+ }
504
+ }
505
+
506
+ // New closure plans must scan an actual installed consumer tree. Create only
507
+ // the isolated KIMI_CODE_HOME container; the interactive host remains the
508
+ // sole owner of managed/<plugin>. Legacy plans retain the previous no-home
509
+ // behavior byte-for-byte.
510
+ if (requiresInstalledClosure) {
511
+ try {
512
+ await mkdir(managedRoot, { recursive: true, mode: 0o700 });
513
+ } catch (mkdirErr) {
514
+ return createResult({
515
+ actionType,
516
+ status: ActionStatus.EXECUTE_FAILED,
517
+ error: `cannot create kimi resource-closure managed root: ${mkdirErr.message}`,
518
+ });
519
+ }
444
520
  }
445
521
 
446
522
  // Idempotent requirement write: an identical existing requirement is left
@@ -500,14 +576,10 @@ export async function executeKimiManualRequirement(action, context) {
500
576
  consumer: 'kimi',
501
577
  plugin: action.plugin,
502
578
  version: action.version,
503
- entrySkill: action.entrySkill,
504
- repo: action.repo,
505
579
  ref,
506
580
  installUrl,
507
581
  planDigest,
508
582
  attestationDir,
509
- kimiCodeHome: kimiHome,
510
- managedInstallRoot,
511
583
  instructions,
512
584
  },
513
585
  });
@@ -42,6 +42,10 @@ import {
42
42
  executeCodeBuddyManualRequirement,
43
43
  readCodeBuddyManifest,
44
44
  } from './codebuddy.mjs';
45
+ import {
46
+ executeCodexManualRequirement,
47
+ readCodexManifest,
48
+ } from './codex.mjs';
45
49
 
46
50
  // --- claude strategy -------------------------------------------------------
47
51
 
@@ -201,7 +205,7 @@ const CODEX = Object.freeze({
201
205
  refStrength: 'commit-sha',
202
206
  outputProtocol: 'structured',
203
207
  identityEvidence: 'install-output',
204
- degradationPolicy: 'block',
208
+ degradationPolicy: 'human-attestation-with-fallback',
205
209
  cli: Object.freeze({
206
210
  binary: 'codex',
207
211
  marketplaceAdd: (repo, ref) => ['plugin', 'marketplace', 'add', repo, '--ref', ref, '--json'],
@@ -243,8 +247,9 @@ const CODEX = Object.freeze({
243
247
  extractInstallPath: codexExtractInstallPath,
244
248
  extractListIdentity: codexExtractListIdentity,
245
249
  crossValidateListEntry: codexCrossValidateListEntry,
246
- buildManualRequirement: null,
247
- readManifest: null,
250
+ // codex's human-attestation fallback strategy lives in ./codex.mjs
251
+ buildManualRequirement: executeCodexManualRequirement,
252
+ readManifest: readCodexManifest,
248
253
  }),
249
254
  });
250
255
 
@@ -267,7 +272,7 @@ const KIMI = Object.freeze({
267
272
  marketplaceAddOutput: null,
268
273
  pluginInstallOutput: null,
269
274
  }),
270
- isolationEnv: (home) => ({ HOME: home, KIMI_CODE_HOME: home }),
275
+ isolationEnv: (home) => ({ HOME: home }),
271
276
  // kimi never execs a CLI; its stable home is created by the
272
277
  // manual-requirement strategy under the attestation authority.
273
278
  isolationSubdirs: Object.freeze([]),
@@ -348,7 +353,10 @@ const CODEBUDDY = Object.freeze({
348
353
  manifestPaths: Object.freeze({
349
354
  // Single authoritative manifest (no precedence chain, unlike kimi).
350
355
  plugin: '.codebuddy-plugin/plugin.json',
351
- marketplace: null,
356
+ // CodeBuddy bundled-family uses the skill-family marketplace path.
357
+ // This ensures bundled-family installation contracts always include a
358
+ // marketplace entry without requiring an explicit marketplaceIndexPath.
359
+ marketplace: '.claude-plugin/marketplace.json',
352
360
  }),
353
361
  marketplaceSourceForm: null,
354
362
  // codebuddy installs from a unified marketplace but carries no marketplace
@@ -401,7 +409,7 @@ const VALID_INSTALL_METHODS = new Set(['structured-cli', 'interactive-only', 'hu
401
409
  const VALID_REF_STRENGTHS = new Set(['commit-sha', 'name-ref', 'unfixable']);
402
410
  const VALID_OUTPUT_PROTOCOLS = new Set(['structured', 'text', 'none']);
403
411
  const VALID_IDENTITY_EVIDENCE = new Set(['list-record', 'install-output', 'filesystem-payload', 'human-attestation']);
404
- const VALID_DEGRADATION_POLICIES = new Set(['block', 'human-attestation']);
412
+ const VALID_DEGRADATION_POLICIES = new Set(['block', 'human-attestation', 'human-attestation-with-fallback']);
405
413
 
406
414
  /**
407
415
  * Look up a platform descriptor by id.
@@ -513,6 +521,16 @@ export function assertRegistry(registry = PLATFORMS) {
513
521
  if (typeof platform.strategy.extractInstallPath !== 'function') {
514
522
  throw new Error(`platform registry: automatable platform ${label} needs strategy.extractInstallPath`);
515
523
  }
524
+ // Platforms with degradationPolicy 'human-attestation-with-fallback'
525
+ // (codex) also need manual-requirement strategies for the fallback path.
526
+ if (platform.degradationPolicy === 'human-attestation-with-fallback') {
527
+ if (typeof platform.strategy.buildManualRequirement !== 'function') {
528
+ throw new Error(`platform registry: ${label} with human-attestation-with-fallback needs strategy.buildManualRequirement`);
529
+ }
530
+ if (typeof platform.strategy.readManifest !== 'function') {
531
+ throw new Error(`platform registry: ${label} with human-attestation-with-fallback needs strategy.readManifest`);
532
+ }
533
+ }
516
534
  } else {
517
535
  if (platform.cli !== null) {
518
536
  throw new Error(`platform registry: non-automatable platform ${label} must have cli === null`);