release-skill 0.2.3 → 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.
Files changed (53) 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 +22 -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 +264 -913
  11. package/README.zh-CN.md +222 -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 +19054 -17573
  15. package/adapters/claude/schemas/release-plan.schema.json +137 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +70 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +19054 -17573
  20. package/adapters/codex/schemas/release-plan.schema.json +137 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +70 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +19054 -17573
  25. package/adapters/kimi/schemas/release-plan.schema.json +137 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +70 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +19054 -17573
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
  33. package/bin/release-skill.bundle.mjs +19054 -17573
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +137 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +70 -2
  38. package/src/adapters/plugin-marketplace.mjs +1190 -435
  39. package/src/commands/prepare.mjs +380 -40
  40. package/src/commands/publish.mjs +107 -75
  41. package/src/commands/reconcile.mjs +92 -327
  42. package/src/commands/setup.mjs +148 -20
  43. package/src/commands/verify.mjs +304 -20
  44. package/src/core/baseline.mjs +8 -1
  45. package/src/core/checkpoints.mjs +50 -7
  46. package/src/core/config.mjs +15 -0
  47. package/src/core/errors.mjs +2 -0
  48. package/src/core/installation-contract.mjs +341 -0
  49. package/src/core/plan.mjs +129 -6
  50. package/src/platforms/codebuddy.mjs +193 -280
  51. package/src/platforms/codex.mjs +369 -0
  52. package/src/platforms/kimi.mjs +164 -119
  53. package/src/platforms/registry.mjs +24 -6
@@ -11,28 +11,10 @@
11
11
  * requires. The install state DOES have a stable on-disk layout that serves as
12
12
  * verification evidence.
13
13
  *
14
- * So codebuddy-marketplace-install is modeled exactly like kimi's protocol
15
- * capability gap (a human attestation closed loop), with two differences driven
16
- * by the verified CodeBuddy state model:
17
- *
18
- * - Install is from a unified marketplace (`artifact-skill-set`,
19
- * https://github.com/ifoohoo/artifact-skill-set), NOT a release-tag URL.
20
- * - Two install channels are accepted, both evidenced on disk:
21
- * desktop: the WorkBuddy desktop app installs into
22
- * `~/.workbuddy/plugins/marketplaces/<marketplace>/plugins/<plugin>/`
23
- * cli: the bundled `codebuddy` CLI, run with an isolated
24
- * `HOME=<codebuddyHome>`, installs into
25
- * `<codebuddyHome>/.codebuddy/plugins/marketplaces/<marketplace>/plugins/<plugin>/`
26
- * The attestation names the channel (`installChannel`) and the marketplace;
27
- * the install path is validated per channel and fails closed otherwise.
28
- *
29
- * - execute NEVER execs the codebuddy CLI. It emits an actionable manual
30
- * install requirement bound to the frozen plan digest + identity.
31
- * - observe/verify consume a structured human attestation plus read-only
32
- * verification of the installed copy. Missing/expired/mismatched/escaping
33
- * proof fails closed, so a codebuddy unit can never reach VERIFIED without
34
- * it — the same fail-closed severity as kimi ("post-publish verification
35
- * cannot be waived").
14
+ * 统一人工结果:收据仅需 platform, version, planDigest,
15
+ * result(passed|failed), actor, confirmedAt 和可选 note。
16
+ * 不再要求 consumer, plugin, conclusion, confirmedBy、隔离 HOME、
17
+ * 安装路径证明、载荷摘要手填、24 小时过期、marketplace 或 installChannel。
36
18
  *
37
19
  * This module is the codebuddy half of the platform registry's strategy table
38
20
  * (registry.mjs references executeCodeBuddyManualRequirement /
@@ -54,7 +36,7 @@
54
36
  * @module platforms/codebuddy
55
37
  */
56
38
 
57
- import { readFile, mkdir } from 'node:fs/promises';
39
+ import { readFile } from 'node:fs/promises';
58
40
  import { join, resolve, relative, isAbsolute } from 'node:path';
59
41
 
60
42
  import {
@@ -104,8 +86,6 @@ function normalizePlanForDigest(plan) {
104
86
  export const CODEBUDDY_REQUIREMENT_FILE = 'release-skill-codebuddy-manual-install.json';
105
87
  /** Structured human attestation consumed by codebuddy observe/verify. */
106
88
  export const CODEBUDDY_ATTESTATION_FILE = 'release-skill-codebuddy-attestation.json';
107
- /** Maximum attestation validity window (mirrors the 24h approval expiry). */
108
- export const CODEBUDDY_MAX_ATTESTATION_VALIDITY_MS = 24 * 60 * 60 * 1000;
109
89
 
110
90
  /**
111
91
  * Unified marketplace the WorkBuddy desktop app and the codebuddy CLI install
@@ -120,13 +100,11 @@ export const CODEBUDDY_MARKETPLACE_SOURCE = 'https://github.com/ifoohoo/artifact
120
100
  /** Authoritative codebuddy plugin manifest (single candidate, no precedence). */
121
101
  export const CODEBUDDY_PLUGIN_MANIFEST_RELATIVE = join('.codebuddy-plugin', 'plugin.json');
122
102
 
123
- /** The only two install channels with verified on-disk evidence. */
124
- const CODEBUDDY_INSTALL_CHANNELS = new Set(['desktop', 'cli']);
125
-
126
- /** Well-known desktop install root (the WorkBuddy app's home dir name). */
127
- const CODEBUDDY_DESKTOP_HOME_DIR = '.workbuddy';
128
- /** CLI-side plugin state dir under the isolated HOME. */
129
- const CODEBUDDY_CLI_STATE_DIR = '.codebuddy';
103
+ /**
104
+ * Maximum validity window for a codebuddy human attestation (24 hours).
105
+ * attestations with expiresAt exceeding this window from attestedAt are rejected.
106
+ */
107
+ export const CODEBUDDY_MAX_ATTESTATION_VALIDITY_MS = 24 * 60 * 60 * 1000;
130
108
 
131
109
  /**
132
110
  * Resolve and verify the genuine frozen plan digest from the adapter context,
@@ -203,115 +181,20 @@ export function codebuddyAuthorityDir(context, planDigest, plugin) {
203
181
  }
204
182
 
205
183
  /**
206
- * The isolated CodeBuddy CLI HOME for the cli install channel. The operator
207
- * runs the bundled `codebuddy` CLI with `HOME=<codebuddyHome>` so the managed
208
- * marketplace clone lands inside the plan-digest-keyed authority rather than
209
- * the user's real `~/.codebuddy`. Identical across publish/reconcile/verify
210
- * run dirs because it derives from the authority dir.
211
- *
212
- * @param {string} attestationDir - plan-digest-keyed authority dir.
213
- * @returns {string}
214
- */
215
- export function codebuddyCliHome(attestationDir) {
216
- return resolve(attestationDir, 'codebuddy-home');
217
- }
218
-
219
- /**
220
- * The expected CLI-channel install root for a plugin under an isolated home.
221
- * `<codebuddyHome>/.codebuddy/plugins/marketplaces/<marketplace>/plugins/<plugin>`
222
- *
223
- * @param {string} cliHome - isolated codebuddy HOME.
224
- * @param {string} marketplace - marketplace name.
225
- * @param {string} plugin - plugin id.
226
- * @returns {string}
227
- */
228
- export function codebuddyCliInstallRoot(cliHome, marketplace, plugin) {
229
- return resolve(cliHome, CODEBUDDY_CLI_STATE_DIR, 'plugins', 'marketplaces', marketplace, 'plugins', plugin);
230
- }
231
-
232
- /**
233
- * Segment-level tail an attested desktop installPath must end with:
234
- * `/.workbuddy/plugins/marketplaces/<marketplace>/plugins/<plugin>`
235
- * (verified WorkBuddy desktop layout).
184
+ * 统一人工安装说明:面向 CodeBuddy / WorkBuddy 的人工结果流程。
236
185
  *
237
- * @param {string} marketplace
238
- * @param {string} plugin
186
+ * @param {{plugin:string, version:string, ref:string, attestationDir:string}} p
239
187
  * @returns {string[]}
240
188
  */
241
- function codebuddyDesktopTailSegments(marketplace, plugin) {
242
- return [CODEBUDDY_DESKTOP_HOME_DIR, 'plugins', 'marketplaces', marketplace, 'plugins', plugin];
243
- }
244
-
245
- /**
246
- * Segment-level tail an attested cli installPath must end with:
247
- * `/.codebuddy/plugins/marketplaces/<marketplace>/plugins/<plugin>`
248
- * (verified CodeBuddy CLI layout).
249
- *
250
- * @param {string} marketplace
251
- * @param {string} plugin
252
- * @returns {string[]}
253
- */
254
- function codebuddyCliTailSegments(marketplace, plugin) {
255
- return [CODEBUDDY_CLI_STATE_DIR, 'plugins', 'marketplaces', marketplace, 'plugins', plugin];
256
- }
257
-
258
- /**
259
- * Segment-level check that a desktop installPath ends with the well-known
260
- * WorkBuddy marketplace layout. Pure string check (no filesystem): the path's
261
- * segments (split on `/` or `\`, dropping empty/`.` segments) must end with the
262
- * desktop tail. Fails closed otherwise.
263
- *
264
- * @param {string} installPath
265
- * @param {string} marketplace
266
- * @param {string} plugin
267
- * @returns {boolean}
268
- */
269
- function matchesDesktopLayout(installPath, marketplace, plugin) {
270
- const segments = installPath.split(/[\\/]+/).filter((s) => s !== '' && s !== '.');
271
- const tail = codebuddyDesktopTailSegments(marketplace, plugin);
272
- if (segments.length < tail.length) return false;
273
- const offset = segments.length - tail.length;
274
- return tail.every((segment, index) => segments[offset + index] === segment);
275
- }
276
-
277
- /**
278
- * Segment-level check that a cli installPath ends with the well-known
279
- * CodeBuddy CLI marketplace layout. Pure string check (no filesystem): the
280
- * path's segments (split on `/` or `\`, dropping empty/`.` segments) must end
281
- * with the cli tail. Fails closed otherwise.
282
- *
283
- * @param {string} installPath
284
- * @param {string} marketplace
285
- * @param {string} plugin
286
- * @returns {boolean}
287
- */
288
- function matchesCliLayout(installPath, marketplace, plugin) {
289
- const segments = installPath.split(/[\\/]+/).filter((s) => s !== '' && s !== '.');
290
- const tail = codebuddyCliTailSegments(marketplace, plugin);
291
- if (segments.length < tail.length) return false;
292
- const offset = segments.length - tail.length;
293
- return tail.every((segment, index) => segments[offset + index] === segment);
294
- }
295
-
296
- /**
297
- * Human-facing, actionable manual-install closed-loop instructions for
298
- * CodeBuddy / WorkBuddy.
299
- *
300
- * @param {{plugin:string, version:string, ref:string, cliHome:string, attestationDir:string}} p
301
- * @returns {string[]}
302
- */
303
- function buildCodeBuddyManualInstructions({ plugin, version, ref, cliHome, attestationDir }) {
304
- const cliInstallRoot = codebuddyCliInstallRoot(cliHome, CODEBUDDY_MARKETPLACE_NAME, plugin);
305
- const desktopInstallRoot = `~/${CODEBUDDY_DESKTOP_HOME_DIR}/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${plugin}`;
189
+ function buildCodeBuddyManualInstructions({ plugin, version, ref, attestationDir }) {
306
190
  return [
307
- `CodeBuddy/WorkBuddy plugin install cannot pin a frozen ref (the codebuddy CLI marketplace add/install have no ref option and track the default branch), so installation is a manual step proven by a human attestation.`,
308
- `1) publish fails closed at this codebuddy checkpoint and leaves the run PARTIAL (the automated Git branch/tag, npm, and GitHub Release writes still complete first).`,
309
- `2) PRIMARY PATH (WorkBuddy desktop): install release-skill from the unified marketplace "${CODEBUDDY_MARKETPLACE_NAME}" (${CODEBUDDY_MARKETPLACE_SOURCE}). Confirm "~/.workbuddy/settings.json" enabledPlugins contains "${plugin}@${CODEBUDDY_MARKETPLACE_NAME}": true. The plugin lands at "${desktopInstallRoot}/".`,
310
- `3) ALTERNATE PATH (bundled codebuddy CLI, isolatable): run the CLI with the ISOLATED home from this requirement so the clone lands inside it: HOME="${cliHome}" <codebuddy binary> plugin marketplace add ${CODEBUDDY_MARKETPLACE_SOURCE} ; then HOME="${cliHome}" <codebuddy binary> plugin install ${plugin}@${CODEBUDDY_MARKETPLACE_NAME}. The CLI ships with WorkBuddy.app (macOS known path /Applications/WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy). The plugin lands at "${cliInstallRoot}/".`,
311
- `4) REF LIMITATION WARNING: a codebuddy install tracks the marketplace default branch and CANNOT be pinned to frozen ref "${ref}". Before writing the attestation you MUST confirm the installed plugin manifest version equals the frozen version ${version}; otherwise do NOT issue an attestation.`,
312
- `5) Write the attestation JSON to: ${attestationDir}/${CODEBUDDY_ATTESTATION_FILE}. planDigest MUST be the frozen plan digest; payloadDigest MUST be the frozen snapshot payload digest; installChannel MUST be "desktop" or "cli"; marketplace MUST be "${CODEBUDDY_MARKETPLACE_NAME}"; installPath MUST be the actual installed plugin directory for the chosen channel. attestedAt must not be in the future and expiresAt must be within 24 hours of attestedAt.`,
313
- ` Required fields: consumer="codebuddy", plugin, version, entrySkill, repo, ref, marketplace, installChannel, installPath, planDigest, payloadDigest, attestedBy, attestedAt, expiresAt.`,
314
- `6) 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.`,
191
+ `CodeBuddy/WorkBuddy 插件安装无法锁定冻结 refcodebuddy CLI marketplace add/install 没有 ref 选项,跟踪默认分支),因此安装是需要人工结果证明的手动步骤。`,
192
+ `1) publish 完成所有远端写入后进入 PUBLISHED 状态(自动化 Git 分支/标签、npm GitHub Release 写入已完成)。此 codebuddy 检查点标记为需要人工安装。`,
193
+ `2) 从统一市场 "${CODEBUDDY_MARKETPLACE_NAME}" (${CODEBUDDY_MARKETPLACE_SOURCE}) 安装 release-skill。确认安装的插件版本等于冻结版本 ${version}。`,
194
+ `3) 将人工结果 JSON 写入: ${attestationDir}/${CODEBUDDY_ATTESTATION_FILE}`,
195
+ ` 必填字段: platform="codebuddy", version, planDigest(冻结计划摘要), result("passed" "failed"), actor(确认人), confirmedAt(ISO 8601 时间戳)`,
196
+ ` 可选字段: note(备注)`,
197
+ `4) 运行 release-skill verify(从同一个计划摘要索引的权威目录读取结果,成功后 -> VERIFIED)。`,
315
198
  ];
316
199
  }
317
200
 
@@ -347,129 +230,185 @@ export async function readCodeBuddyManifest(pluginRootReal) {
347
230
  }
348
231
 
349
232
  /**
350
- * Validate a structured codebuddy manual-install attestation against the frozen
351
- * action and the verified frozen plan digest.
233
+ * 统一人工结果验证:验证 codebuddy 人工结果是否匹配冻结计划。
234
+ *
235
+ * 统一后的结果只需:
236
+ * - 必填:platform, version, planDigest, result(passed|failed), actor, confirmedAt
237
+ * - 可选:note
238
+ *
239
+ * 旧格式兼容(0.2.3 及更早):
240
+ * - consumer → platform (必须是 'codebuddy')
241
+ * - attestedBy → actor
242
+ * - attestedAt → confirmedAt
243
+ * - payloadDigest → 载荷绑定验证(如果存在)
352
244
  *
353
- * Bindings (fail closed on any mismatch):
354
- * - `consumer` must be "codebuddy".
355
- * - `installChannel` must be "desktop" or "cli".
356
- * - `marketplace` must equal the requirement-declared unified marketplace
357
- * (`artifact-skill-set`).
358
- * - `planDigest` binds to the REAL frozen plan digest (`boundPlanDigest`, from
359
- * `context.plan.digest`) — NOT to `action.manifestDigest`.
360
- * - `payloadDigest` binds separately to `action.manifestDigest`.
361
- * - plugin identity, version, entry skill, repo, and frozen ref must match.
362
- * - Time bounds: `attestedAt` not in the future, window (`expiresAt -
363
- * attestedAt`) <= 24h, and not expired relative to `isoNow`.
364
- * - installPath per channel (lexical): desktop must end with the well-known
365
- * `/.workbuddy/plugins/marketplaces/<marketplace>/plugins/<plugin>` segment
366
- * tail. The cli channel's containment within the isolated home is enforced in
367
- * the adapter observe branch (it needs the context-derived isolated home and
368
- * realpath resolution, mirroring kimi's observe-side managed-root check).
245
+ * 绑定验证(任何不匹配都失败):
246
+ * - planDigest 绑定到真正的冻结计划摘要(boundPlanDigest)
247
+ * - version 静态一致性检查
248
+ * - result 只接受 passed failed
369
249
  *
370
- * @param {object} attestation - parsed attestation JSON.
371
- * @param {object} action - the expanded codebuddy action (top-level fields).
372
- * @param {string} isoNow - current ISO timestamp.
373
- * @param {string} boundPlanDigest - verified frozen plan digest.
374
- * @returns {{valid:boolean, error:string|null}}
250
+ * @param {object} attestation - 解析后的人工结果 JSON
251
+ * @param {object} action - 展开的 codebuddy 动作(顶层字段)。
252
+ * @param {string} isoNow - 当前 ISO 时间戳(保留签名兼容,不再用于过期检查)。
253
+ * @param {string} boundPlanDigest - 验证过的冻结计划摘要。
254
+ * @returns {{valid:boolean, error:string|null, normalized:object|null}}
375
255
  */
376
256
  export function validateCodeBuddyAttestation(attestation, action, isoNow, boundPlanDigest) {
377
257
  if (!attestation || typeof attestation !== 'object' || Array.isArray(attestation)) {
378
- return { valid: false, error: 'codebuddy attestation is not an object' };
258
+ return { valid: false, error: 'codebuddy attestation is not an object', normalized: null };
379
259
  }
380
- const requiredStrings = ['plugin', 'version', 'entrySkill', 'repo', 'ref', 'installPath', 'payloadDigest', 'planDigest', 'attestedBy', 'attestedAt', 'expiresAt', 'marketplace', 'installChannel'];
381
- for (const field of requiredStrings) {
382
- if (typeof attestation[field] !== 'string' || attestation[field].length === 0) {
383
- return { valid: false, error: `codebuddy attestation missing required field "${field}"` };
384
- }
260
+
261
+ // 旧格式归一化:将旧字段映射到新字段
262
+ const normalized = { ...attestation };
263
+
264
+ // 旧格式识别:完整旧标识组(consumer, attestedBy, attestedAt)全部存在,
265
+ // 且新格式字段组(result, actor, confirmedAt)未混入。
266
+ const hasCompleteOldMarkers = !!(normalized.consumer && normalized.attestedBy && normalized.attestedAt);
267
+ const hasNewFormatFields = !!(normalized.result || normalized.actor || normalized.confirmedAt);
268
+ const isOldFormat = hasCompleteOldMarkers && !hasNewFormatFields;
269
+
270
+ // 新格式严格要求 result 字段;仅当确认为旧格式时才允许缺省
271
+ if (!normalized.result && isOldFormat) {
272
+ normalized.result = 'passed';
385
273
  }
386
- if (attestation.consumer !== 'codebuddy') {
387
- return { valid: false, error: `codebuddy attestation consumer "${attestation.consumer}" must be "codebuddy"` };
274
+
275
+ // consumer platform (旧格式使用 consumer)
276
+ if (!normalized.platform && normalized.consumer) {
277
+ normalized.platform = normalized.consumer;
388
278
  }
389
- if (!CODEBUDDY_INSTALL_CHANNELS.has(attestation.installChannel)) {
390
- return { valid: false, error: `codebuddy attestation installChannel "${attestation.installChannel}" must be "desktop" or "cli"` };
279
+ // attestedBy → actor (旧格式使用 attestedBy)
280
+ if (!normalized.actor && normalized.attestedBy) {
281
+ normalized.actor = normalized.attestedBy;
391
282
  }
392
- if (attestation.marketplace !== CODEBUDDY_MARKETPLACE_NAME) {
393
- return { valid: false, error: `codebuddy attestation marketplace "${attestation.marketplace}" must be "${CODEBUDDY_MARKETPLACE_NAME}"` };
283
+ // attestedAt → confirmedAt (旧格式使用 attestedAt)
284
+ if (!normalized.confirmedAt && normalized.attestedAt) {
285
+ normalized.confirmedAt = normalized.attestedAt;
394
286
  }
395
- if (!HEX_DIGEST_RE.test(attestation.planDigest)) {
396
- return { valid: false, error: 'codebuddy attestation planDigest must be a 64-char lowercase hex digest' };
287
+
288
+ // 统一必填字段
289
+ const requiredStrings = ['platform', 'version', 'planDigest', 'result', 'actor', 'confirmedAt'];
290
+ for (const field of requiredStrings) {
291
+ if (typeof normalized[field] !== 'string' || normalized[field].length === 0) {
292
+ return { valid: false, error: `codebuddy attestation missing required field "${field}"`, normalized: null };
293
+ }
397
294
  }
398
- if (attestation.planDigest !== boundPlanDigest) {
399
- return { valid: false, error: 'codebuddy attestation planDigest does not match the frozen plan digest' };
295
+ if (normalized.platform !== 'codebuddy') {
296
+ return { valid: false, error: `codebuddy attestation platform "${normalized.platform}" must be "codebuddy"`, normalized: null };
400
297
  }
401
- if (attestation.plugin !== action.plugin) {
402
- return { valid: false, error: `codebuddy attestation plugin "${attestation.plugin}" does not match action plugin "${action.plugin}"` };
298
+
299
+ // installPath 路径安全校验:CLI 通道必须在预期目录内
300
+ if (normalized.installPath !== undefined) {
301
+ if (typeof normalized.installPath !== 'string' || normalized.installPath.length === 0) {
302
+ return { valid: false, error: 'codebuddy attestation installPath must be a non-empty string when present', normalized: null };
303
+ }
304
+ if (normalized.installPath.includes('..')) {
305
+ return { valid: false, error: 'codebuddy attestation installPath must not contain path traversal ("..")', normalized: null };
306
+ }
307
+ // CLI 通道:installPath 必须以预期后缀结尾
308
+ if (normalized.installChannel === 'cli') {
309
+ const expectedSuffix = `.workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
310
+ if (!normalized.installPath.endsWith(expectedSuffix)) {
311
+ return { valid: false, error: `codebuddy CLI attestation installPath must end with "${expectedSuffix}", got "${normalized.installPath}"`, normalized: null };
312
+ }
313
+ }
403
314
  }
404
- if (attestation.version !== action.version) {
405
- return { valid: false, error: `codebuddy attestation version "${attestation.version}" does not match action version "${action.version}"` };
315
+
316
+ // result 只接受 passed failed
317
+ if (normalized.result !== 'passed' && normalized.result !== 'failed') {
318
+ return { valid: false, error: `codebuddy attestation result "${normalized.result}" must be "passed" or "failed"`, normalized: null };
319
+ }
320
+ // planDigest 绑定验证
321
+ if (!HEX_DIGEST_RE.test(normalized.planDigest)) {
322
+ return { valid: false, error: 'codebuddy attestation planDigest must be a 64-char lowercase hex digest', normalized: null };
406
323
  }
407
- if (attestation.entrySkill !== action.entrySkill) {
408
- return { valid: false, error: `codebuddy attestation entrySkill "${attestation.entrySkill}" does not match action entrySkill "${action.entrySkill}"` };
324
+ if (normalized.planDigest !== boundPlanDigest) {
325
+ return { valid: false, error: 'codebuddy attestation planDigest does not match the frozen plan digest', normalized: null };
409
326
  }
410
- if (attestation.repo !== action.repo) {
411
- return { valid: false, error: `codebuddy attestation repo "${attestation.repo}" does not match action repo "${action.repo}"` };
327
+ // version 静态一致性检查
328
+ if (normalized.version !== action.version) {
329
+ return { valid: false, error: `codebuddy attestation version "${normalized.version}" does not match action version "${action.version}"`, normalized: null };
412
330
  }
413
- const expectedRef = action.ref ?? `v${action.version}`;
414
- if (attestation.ref !== expectedRef) {
415
- return { valid: false, error: `codebuddy attestation ref "${attestation.ref}" does not match frozen ref "${expectedRef}"` };
331
+
332
+ // 旧格式额外绑定字段校验:plugin, repo, ref, entrySkill, payloadDigest
333
+ if (isOldFormat) {
334
+ if (normalized.plugin !== action.plugin) {
335
+ return { valid: false, error: `codebuddy attestation plugin "${normalized.plugin}" does not match action plugin "${action.plugin}"`, normalized: null };
336
+ }
337
+ if (normalized.repo !== action.repo) {
338
+ return { valid: false, error: `codebuddy attestation repo "${normalized.repo}" does not match action repo "${action.repo}"`, normalized: null };
339
+ }
340
+ const expectedRef = action.ref ?? `v${action.version}`;
341
+ if (normalized.ref !== expectedRef) {
342
+ return { valid: false, error: `codebuddy attestation ref "${normalized.ref}" does not match action ref "${expectedRef}"`, normalized: null };
343
+ }
344
+ if (normalized.entrySkill !== action.entrySkill) {
345
+ return { valid: false, error: `codebuddy attestation entrySkill "${normalized.entrySkill}" does not match action entrySkill "${action.entrySkill}"`, normalized: null };
346
+ }
347
+ // payloadDigest 旧格式必填、格式合法且等于冻结动作 manifestDigest
348
+ if (typeof normalized.payloadDigest !== 'string' || normalized.payloadDigest.length === 0) {
349
+ return { valid: false, error: 'codebuddy attestation payloadDigest is required for old-format receipts', normalized: null };
350
+ }
351
+ if (!HEX_DIGEST_RE.test(normalized.payloadDigest)) {
352
+ return { valid: false, error: 'codebuddy attestation payloadDigest must be a 64-char lowercase hex digest', normalized: null };
353
+ }
354
+ if (action.manifestDigest && normalized.payloadDigest !== action.manifestDigest) {
355
+ return { valid: false, error: 'codebuddy attestation payloadDigest does not match the frozen manifest digest', normalized: null };
356
+ }
357
+ } else {
358
+ // 新格式 payloadDigest 可选,但如果存在则必须合法
359
+ if (normalized.payloadDigest !== undefined) {
360
+ if (!HEX_DIGEST_RE.test(normalized.payloadDigest)) {
361
+ return { valid: false, error: 'codebuddy attestation payloadDigest must be a 64-char lowercase hex digest when present', normalized: null };
362
+ }
363
+ if (action.manifestDigest && normalized.payloadDigest !== action.manifestDigest) {
364
+ return { valid: false, error: 'codebuddy attestation payloadDigest does not match the frozen manifest digest', normalized: null };
365
+ }
366
+ }
416
367
  }
417
- if (attestation.payloadDigest !== action.manifestDigest) {
418
- return { valid: false, error: 'codebuddy attestation payloadDigest does not match the frozen payload digest' };
368
+
369
+ // confirmedAt 必须是有效的时间戳
370
+ const confirmedMs = Date.parse(normalized.confirmedAt);
371
+ if (!Number.isFinite(confirmedMs)) {
372
+ return { valid: false, error: 'codebuddy attestation confirmedAt must be a valid ISO timestamp', normalized: null };
419
373
  }
420
- const attestedMs = Date.parse(attestation.attestedAt);
421
- const expiresMs = Date.parse(attestation.expiresAt);
374
+ // confirmedAt 不得在未来
422
375
  const nowMs = Date.parse(isoNow);
423
- if (!Number.isFinite(attestedMs) || !Number.isFinite(expiresMs) || !Number.isFinite(nowMs)) {
424
- return { valid: false, error: 'codebuddy attestation attestedAt/expiresAt must be valid ISO timestamps' };
425
- }
426
- if (attestedMs > nowMs) {
427
- return { valid: false, error: 'codebuddy attestation attestedAt is in the future' };
428
- }
429
- if (expiresMs <= attestedMs) {
430
- return { valid: false, error: 'codebuddy attestation expiresAt must be after attestedAt' };
431
- }
432
- if (expiresMs - attestedMs > CODEBUDDY_MAX_ATTESTATION_VALIDITY_MS) {
433
- return { valid: false, error: 'codebuddy attestation validity must not exceed 24 hours' };
434
- }
435
- if (nowMs > expiresMs) {
436
- return { valid: false, error: 'codebuddy attestation has expired' };
437
- }
438
- // Desktop installPath must end with the well-known WorkBuddy marketplace
439
- // layout (segment-level). The cli channel's isolated-home containment is
440
- // enforced in the adapter observe branch (needs the context-derived isolated
441
- // home + realpath), mirroring kimi's observe-side managed-root check.
442
- if (attestation.installChannel === 'desktop'
443
- && !matchesDesktopLayout(attestation.installPath, attestation.marketplace, attestation.plugin)) {
444
- return { valid: false, error: `codebuddy attestation installPath does not match the WorkBuddy desktop marketplace layout (.workbuddy/plugins/marketplaces/${attestation.marketplace}/plugins/${attestation.plugin})` };
445
- }
446
- // CLI installPath must end with the well-known CodeBuddy CLI marketplace
447
- // layout (segment-level). This is a lexical check; the full realpath
448
- // containment within the isolated home is enforced in the adapter observe
449
- // branch (needs the context-derived isolated home + realpath resolution).
450
- if (attestation.installChannel === 'cli'
451
- && !matchesCliLayout(attestation.installPath, attestation.marketplace, attestation.plugin)) {
452
- return { valid: false, error: `codebuddy attestation installPath does not match the CodeBuddy CLI marketplace layout (.codebuddy/plugins/marketplaces/${attestation.marketplace}/plugins/${attestation.plugin})` };
453
- }
454
- return { valid: true, error: null };
376
+ if (Number.isFinite(nowMs) && confirmedMs > nowMs) {
377
+ return { valid: false, error: 'codebuddy attestation confirmedAt must not be in the future', normalized: null };
378
+ }
379
+ // expiresAt 校验(可选字段)
380
+ if (normalized.expiresAt !== undefined) {
381
+ const expiresMs = Date.parse(normalized.expiresAt);
382
+ if (!Number.isFinite(expiresMs)) {
383
+ return { valid: false, error: 'codebuddy attestation expiresAt must be a valid ISO timestamp', normalized: null };
384
+ }
385
+ // 已过期
386
+ if (Number.isFinite(nowMs) && expiresMs < nowMs) {
387
+ return { valid: false, error: 'codebuddy attestation has expired', normalized: null };
388
+ }
389
+ // 有效期不得超过 24 小时
390
+ if (expiresMs - confirmedMs > CODEBUDDY_MAX_ATTESTATION_VALIDITY_MS) {
391
+ return { valid: false, error: 'codebuddy attestation validity exceeds 24 hours', normalized: null };
392
+ }
393
+ }
394
+ // 可选字段 note 如果存在必须是字符串
395
+ if (normalized.note !== undefined && typeof normalized.note !== 'string') {
396
+ return { valid: false, error: 'codebuddy attestation note must be a string when present', normalized: null };
397
+ }
398
+ return { valid: true, error: null, normalized };
455
399
  }
456
400
 
457
401
  /**
458
402
  * CodeBuddy protocol capability gap: the codebuddy CLI cannot pin a frozen ref,
459
403
  * so there is NO trustworthy automated install checkpoint. execute NEVER execs a
460
404
  * codebuddy command. Instead it emits an actionable manual-install requirement
461
- * bound to the real frozen plan digest + identity (naming both install channels
462
- * and the unified marketplace), and leaves success to observe/verify, which
463
- * consume only a trusted human attestation plus read-only verification. Without
464
- * that proof the checkpoint fails closed and can never reach VERIFIED.
405
+ * bound to the real frozen plan digest + identity, and leaves success to
406
+ * observe/verify, which consume only a trusted human attestation plus read-only
407
+ * verification. Without that proof the checkpoint fails closed and can never
408
+ * reach VERIFIED.
465
409
  *
466
- * Isolation model: the cli channel's isolated home is a STABLE, plan-digest-keyed
467
- * directory under the attestation authority (`<authorityDir>/codebuddy-home`).
468
- * execute creates the isolated home (so the operator's CLI has a HOME to write
469
- * into) but never the marketplace install subdir (the operator's CLI install
470
- * creates that). The desktop channel uses the user's real `~/.workbuddy` and
471
- * needs no isolated home. The requirement write is idempotent: an identical
472
- * existing requirement is left untouched, a divergent one fails closed.
410
+ * 统一人工判定:不再创建隔离目录,不再要求隔离 HOME。
411
+ * 人工结果只需 platform, version, planDigest, result, actor, confirmedAt。
473
412
  *
474
413
  * Referenced from the registry as the codebuddy strategy.buildManualRequirement
475
414
  * — the automatable=false manual-requirement path.
@@ -519,70 +458,51 @@ export async function executeCodeBuddyManualRequirement(action, context) {
519
458
  error: dirErr.message,
520
459
  });
521
460
  }
522
- const cliHome = codebuddyCliHome(attestationDir);
523
- const cliInstallRoot = codebuddyCliInstallRoot(cliHome, CODEBUDDY_MARKETPLACE_NAME, action.plugin);
524
- const desktopInstallRoot = `~/${CODEBUDDY_DESKTOP_HOME_DIR}/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
525
461
 
526
462
  const instructions = buildCodeBuddyManualInstructions({
527
463
  plugin: action.plugin,
528
464
  version: action.version,
529
465
  ref,
530
- cliHome,
531
466
  attestationDir,
532
467
  });
533
468
 
469
+ // 统一 requirement 结构:不再包含隔离目录信息
534
470
  const requirement = {
535
471
  kind: 'codebuddy-manual-install-requirement',
536
- consumer: 'codebuddy',
472
+ platform: 'codebuddy',
537
473
  plugin: action.plugin,
538
474
  version: action.version,
539
- entrySkill: action.entrySkill,
540
475
  repo: action.repo,
541
476
  ref,
542
- marketplace: CODEBUDDY_MARKETPLACE_NAME,
543
- marketplaceSource: CODEBUDDY_MARKETPLACE_SOURCE,
544
- installChannels: ['desktop', 'cli'],
545
- // (A) planDigest binds to the real frozen plan digest;
546
- // expectedPayloadDigest binds separately to the snapshot payload digest.
477
+ entrySkill: action.entrySkill,
547
478
  planDigest,
548
- expectedPayloadDigest: action.manifestDigest,
549
- isolatedHome: cliHome,
550
- codebuddyHome: cliHome,
551
- cliInstallRoot,
552
- desktopInstallRoot,
553
479
  attestationDir,
554
480
  attestationFile: CODEBUDDY_ATTESTATION_FILE,
555
481
  attestationTemplate: {
556
- consumer: 'codebuddy',
557
- plugin: action.plugin,
482
+ platform: 'codebuddy',
558
483
  version: action.version,
559
- entrySkill: action.entrySkill,
560
- repo: action.repo,
561
- ref,
562
- marketplace: CODEBUDDY_MARKETPLACE_NAME,
563
- installChannel: '<"desktop" or "cli">',
564
- installPath: '<actual installed plugin directory for the chosen channel>',
565
484
  planDigest,
566
- payloadDigest: action.manifestDigest,
567
- attestedBy: '<person responsible for the manual install>',
568
- attestedAt: '<ISO 8601 now; must not be in the future>',
569
- expiresAt: '<ISO 8601; within 24h of attestedAt>',
485
+ result: '<"passed" or "failed">',
486
+ actor: '<person who confirmed the install>',
487
+ confirmedAt: '<ISO 8601 timestamp>',
488
+ note: '<optional note>',
570
489
  },
571
490
  instructions,
572
491
  };
573
492
 
574
- // Create ONLY the isolated cli home (which transitively creates the authority
575
- // dir); never pre-create the marketplace install subdir — the operator's CLI
576
- // install (cli channel) or the WorkBuddy desktop app (desktop channel) owns
577
- // the actual install directory.
493
+ // Ensure the authority directory exists (no isolated home creation needed).
494
+ const { mkdir } = await import('node:fs/promises');
578
495
  try {
579
- await mkdir(cliHome, { recursive: true, mode: 0o700 });
496
+ await mkdir(attestationDir, { recursive: true, mode: 0o700 });
580
497
  } catch (mkdirErr) {
581
- return createResult({
582
- actionType,
583
- status: ActionStatus.EXECUTE_FAILED,
584
- error: `cannot create codebuddy isolated home directory: ${mkdirErr.message}`,
585
- });
498
+ // EEXIST is fine; other errors are fatal.
499
+ if (mkdirErr?.code !== 'EEXIST') {
500
+ return createResult({
501
+ actionType,
502
+ status: ActionStatus.EXECUTE_FAILED,
503
+ error: `cannot create codebuddy attestation directory: ${mkdirErr.message}`,
504
+ });
505
+ }
586
506
  }
587
507
 
588
508
  // Idempotent requirement write: an identical existing requirement is left
@@ -639,19 +559,12 @@ export async function executeCodeBuddyManualRequirement(action, context) {
639
559
  observation: {
640
560
  installed: false,
641
561
  manualInstallRequired: true,
642
- consumer: 'codebuddy',
562
+ platform: 'codebuddy',
643
563
  plugin: action.plugin,
644
564
  version: action.version,
645
- entrySkill: action.entrySkill,
646
- repo: action.repo,
647
565
  ref,
648
- marketplace: CODEBUDDY_MARKETPLACE_NAME,
649
- installChannels: ['desktop', 'cli'],
650
566
  planDigest,
651
567
  attestationDir,
652
- codebuddyHome: cliHome,
653
- cliInstallRoot,
654
- desktopInstallRoot,
655
568
  instructions,
656
569
  },
657
570
  });