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.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +4 -2
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +41 -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 +265 -916
  11. package/README.zh-CN.md +222 -537
  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 +18710 -16694
  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 +18710 -16694
  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 +18710 -16694
  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 +18710 -16694
  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 +18710 -16694
  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 +1602 -605
  39. package/src/commands/prepare.mjs +441 -32
  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 +315 -25
  44. package/src/core/baseline.mjs +21 -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 +307 -6
  50. package/src/platforms/codebuddy.mjs +191 -238
  51. package/src/platforms/codex.mjs +369 -0
  52. package/src/platforms/kimi.mjs +164 -119
  53. package/src/platforms/registry.mjs +180 -4
  54. package/src/producers/build-adapters.mjs +9 -2
@@ -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,83 +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).
236
- *
237
- * @param {string} marketplace
238
- * @param {string} plugin
239
- * @returns {string[]}
240
- */
241
- function codebuddyDesktopTailSegments(marketplace, plugin) {
242
- return [CODEBUDDY_DESKTOP_HOME_DIR, 'plugins', 'marketplaces', marketplace, 'plugins', plugin];
243
- }
244
-
245
- /**
246
- * Segment-level check that a desktop installPath ends with the well-known
247
- * WorkBuddy marketplace layout. Pure string check (no filesystem): the path's
248
- * segments (split on `/` or `\`, dropping empty/`.` segments) must end with the
249
- * desktop tail. Fails closed otherwise.
250
- *
251
- * @param {string} installPath
252
- * @param {string} marketplace
253
- * @param {string} plugin
254
- * @returns {boolean}
255
- */
256
- function matchesDesktopLayout(installPath, marketplace, plugin) {
257
- const segments = installPath.split(/[\\/]+/).filter((s) => s !== '' && s !== '.');
258
- const tail = codebuddyDesktopTailSegments(marketplace, plugin);
259
- if (segments.length < tail.length) return false;
260
- const offset = segments.length - tail.length;
261
- return tail.every((segment, index) => segments[offset + index] === segment);
262
- }
263
-
264
- /**
265
- * Human-facing, actionable manual-install closed-loop instructions for
266
- * CodeBuddy / WorkBuddy.
184
+ * 统一人工安装说明:面向 CodeBuddy / WorkBuddy 的人工结果流程。
267
185
  *
268
- * @param {{plugin:string, version:string, ref:string, cliHome:string, attestationDir:string}} p
186
+ * @param {{plugin:string, version:string, ref:string, attestationDir:string}} p
269
187
  * @returns {string[]}
270
188
  */
271
- function buildCodeBuddyManualInstructions({ plugin, version, ref, cliHome, attestationDir }) {
272
- const cliInstallRoot = codebuddyCliInstallRoot(cliHome, CODEBUDDY_MARKETPLACE_NAME, plugin);
273
- const desktopInstallRoot = `~/${CODEBUDDY_DESKTOP_HOME_DIR}/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${plugin}`;
189
+ function buildCodeBuddyManualInstructions({ plugin, version, ref, attestationDir }) {
274
190
  return [
275
- `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.`,
276
- `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).`,
277
- `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}/".`,
278
- `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}/".`,
279
- `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.`,
280
- `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.`,
281
- ` Required fields: consumer="codebuddy", plugin, version, entrySkill, repo, ref, marketplace, installChannel, installPath, planDigest, payloadDigest, attestedBy, attestedAt, expiresAt.`,
282
- `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)。`,
283
198
  ];
284
199
  }
285
200
 
@@ -315,121 +230,185 @@ export async function readCodeBuddyManifest(pluginRootReal) {
315
230
  }
316
231
 
317
232
  /**
318
- * Validate a structured codebuddy manual-install attestation against the frozen
319
- * action and the verified frozen plan digest.
233
+ * 统一人工结果验证:验证 codebuddy 人工结果是否匹配冻结计划。
234
+ *
235
+ * 统一后的结果只需:
236
+ * - 必填:platform, version, planDigest, result(passed|failed), actor, confirmedAt
237
+ * - 可选:note
320
238
  *
321
- * Bindings (fail closed on any mismatch):
322
- * - `consumer` must be "codebuddy".
323
- * - `installChannel` must be "desktop" or "cli".
324
- * - `marketplace` must equal the requirement-declared unified marketplace
325
- * (`artifact-skill-set`).
326
- * - `planDigest` binds to the REAL frozen plan digest (`boundPlanDigest`, from
327
- * `context.plan.digest`) — NOT to `action.manifestDigest`.
328
- * - `payloadDigest` binds separately to `action.manifestDigest`.
329
- * - plugin identity, version, entry skill, repo, and frozen ref must match.
330
- * - Time bounds: `attestedAt` not in the future, window (`expiresAt -
331
- * attestedAt`) <= 24h, and not expired relative to `isoNow`.
332
- * - installPath per channel (lexical): desktop must end with the well-known
333
- * `/.workbuddy/plugins/marketplaces/<marketplace>/plugins/<plugin>` segment
334
- * tail. The cli channel's containment within the isolated home is enforced in
335
- * the adapter observe branch (it needs the context-derived isolated home and
336
- * realpath resolution, mirroring kimi's observe-side managed-root check).
239
+ * 旧格式兼容(0.2.3 及更早):
240
+ * - consumer platform (必须是 'codebuddy')
241
+ * - attestedBy actor
242
+ * - attestedAt confirmedAt
243
+ * - payloadDigest → 载荷绑定验证(如果存在)
337
244
  *
338
- * @param {object} attestation - parsed attestation JSON.
339
- * @param {object} action - the expanded codebuddy action (top-level fields).
340
- * @param {string} isoNow - current ISO timestamp.
341
- * @param {string} boundPlanDigest - verified frozen plan digest.
342
- * @returns {{valid:boolean, error:string|null}}
245
+ * 绑定验证(任何不匹配都失败):
246
+ * - planDigest 绑定到真正的冻结计划摘要(boundPlanDigest)
247
+ * - version 静态一致性检查
248
+ * - result 只接受 passed failed
249
+ *
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}}
343
255
  */
344
256
  export function validateCodeBuddyAttestation(attestation, action, isoNow, boundPlanDigest) {
345
257
  if (!attestation || typeof attestation !== 'object' || Array.isArray(attestation)) {
346
- return { valid: false, error: 'codebuddy attestation is not an object' };
347
- }
348
- const requiredStrings = ['plugin', 'version', 'entrySkill', 'repo', 'ref', 'installPath', 'payloadDigest', 'planDigest', 'attestedBy', 'attestedAt', 'expiresAt', 'marketplace', 'installChannel'];
349
- for (const field of requiredStrings) {
350
- if (typeof attestation[field] !== 'string' || attestation[field].length === 0) {
351
- return { valid: false, error: `codebuddy attestation missing required field "${field}"` };
352
- }
353
- }
354
- if (attestation.consumer !== 'codebuddy') {
355
- return { valid: false, error: `codebuddy attestation consumer "${attestation.consumer}" must be "codebuddy"` };
258
+ return { valid: false, error: 'codebuddy attestation is not an object', normalized: null };
356
259
  }
357
- if (!CODEBUDDY_INSTALL_CHANNELS.has(attestation.installChannel)) {
358
- return { valid: false, error: `codebuddy attestation installChannel "${attestation.installChannel}" must be "desktop" or "cli"` };
359
- }
360
- if (attestation.marketplace !== CODEBUDDY_MARKETPLACE_NAME) {
361
- return { valid: false, error: `codebuddy attestation marketplace "${attestation.marketplace}" must be "${CODEBUDDY_MARKETPLACE_NAME}"` };
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';
362
273
  }
363
- if (!HEX_DIGEST_RE.test(attestation.planDigest)) {
364
- return { valid: false, error: 'codebuddy attestation planDigest must be a 64-char lowercase hex digest' };
274
+
275
+ // consumer platform (旧格式使用 consumer)
276
+ if (!normalized.platform && normalized.consumer) {
277
+ normalized.platform = normalized.consumer;
365
278
  }
366
- if (attestation.planDigest !== boundPlanDigest) {
367
- return { valid: false, error: 'codebuddy attestation planDigest does not match the frozen plan digest' };
279
+ // attestedBy → actor (旧格式使用 attestedBy)
280
+ if (!normalized.actor && normalized.attestedBy) {
281
+ normalized.actor = normalized.attestedBy;
368
282
  }
369
- if (attestation.plugin !== action.plugin) {
370
- return { valid: false, error: `codebuddy attestation plugin "${attestation.plugin}" does not match action plugin "${action.plugin}"` };
283
+ // attestedAt → confirmedAt (旧格式使用 attestedAt)
284
+ if (!normalized.confirmedAt && normalized.attestedAt) {
285
+ normalized.confirmedAt = normalized.attestedAt;
371
286
  }
372
- if (attestation.version !== action.version) {
373
- return { valid: false, error: `codebuddy attestation version "${attestation.version}" does not match action version "${action.version}"` };
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
+ }
374
294
  }
375
- if (attestation.entrySkill !== action.entrySkill) {
376
- return { valid: false, error: `codebuddy attestation entrySkill "${attestation.entrySkill}" does not match action entrySkill "${action.entrySkill}"` };
295
+ if (normalized.platform !== 'codebuddy') {
296
+ return { valid: false, error: `codebuddy attestation platform "${normalized.platform}" must be "codebuddy"`, normalized: null };
377
297
  }
378
- if (attestation.repo !== action.repo) {
379
- return { valid: false, error: `codebuddy attestation repo "${attestation.repo}" does not match action repo "${action.repo}"` };
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
+ }
380
314
  }
381
- const expectedRef = action.ref ?? `v${action.version}`;
382
- if (attestation.ref !== expectedRef) {
383
- return { valid: false, error: `codebuddy attestation ref "${attestation.ref}" does not match frozen ref "${expectedRef}"` };
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 };
384
319
  }
385
- if (attestation.payloadDigest !== action.manifestDigest) {
386
- return { valid: false, error: 'codebuddy attestation payloadDigest does not match the frozen payload digest' };
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 };
387
323
  }
388
- const attestedMs = Date.parse(attestation.attestedAt);
389
- const expiresMs = Date.parse(attestation.expiresAt);
390
- const nowMs = Date.parse(isoNow);
391
- if (!Number.isFinite(attestedMs) || !Number.isFinite(expiresMs) || !Number.isFinite(nowMs)) {
392
- return { valid: false, error: 'codebuddy attestation attestedAt/expiresAt must be valid ISO timestamps' };
324
+ if (normalized.planDigest !== boundPlanDigest) {
325
+ return { valid: false, error: 'codebuddy attestation planDigest does not match the frozen plan digest', normalized: null };
393
326
  }
394
- if (attestedMs > nowMs) {
395
- return { valid: false, error: 'codebuddy attestation attestedAt is in the future' };
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 };
396
330
  }
397
- if (expiresMs <= attestedMs) {
398
- return { valid: false, error: 'codebuddy attestation expiresAt must be after attestedAt' };
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
+ }
399
367
  }
400
- if (expiresMs - attestedMs > CODEBUDDY_MAX_ATTESTATION_VALIDITY_MS) {
401
- return { valid: false, error: 'codebuddy attestation validity must not exceed 24 hours' };
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 };
402
373
  }
403
- if (nowMs > expiresMs) {
404
- return { valid: false, error: 'codebuddy attestation has expired' };
374
+ // confirmedAt 不得在未来
375
+ const nowMs = Date.parse(isoNow);
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
+ }
405
393
  }
406
- // Desktop installPath must end with the well-known WorkBuddy marketplace
407
- // layout (segment-level). The cli channel's isolated-home containment is
408
- // enforced in the adapter observe branch (needs the context-derived isolated
409
- // home + realpath), mirroring kimi's observe-side managed-root check.
410
- if (attestation.installChannel === 'desktop'
411
- && !matchesDesktopLayout(attestation.installPath, attestation.marketplace, attestation.plugin)) {
412
- return { valid: false, error: `codebuddy attestation installPath does not match the WorkBuddy desktop marketplace layout (.workbuddy/plugins/marketplaces/${attestation.marketplace}/plugins/${attestation.plugin})` };
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 };
413
397
  }
414
- return { valid: true, error: null };
398
+ return { valid: true, error: null, normalized };
415
399
  }
416
400
 
417
401
  /**
418
402
  * CodeBuddy protocol capability gap: the codebuddy CLI cannot pin a frozen ref,
419
403
  * so there is NO trustworthy automated install checkpoint. execute NEVER execs a
420
404
  * codebuddy command. Instead it emits an actionable manual-install requirement
421
- * bound to the real frozen plan digest + identity (naming both install channels
422
- * and the unified marketplace), and leaves success to observe/verify, which
423
- * consume only a trusted human attestation plus read-only verification. Without
424
- * 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.
425
409
  *
426
- * Isolation model: the cli channel's isolated home is a STABLE, plan-digest-keyed
427
- * directory under the attestation authority (`<authorityDir>/codebuddy-home`).
428
- * execute creates the isolated home (so the operator's CLI has a HOME to write
429
- * into) but never the marketplace install subdir (the operator's CLI install
430
- * creates that). The desktop channel uses the user's real `~/.workbuddy` and
431
- * needs no isolated home. The requirement write is idempotent: an identical
432
- * existing requirement is left untouched, a divergent one fails closed.
410
+ * 统一人工判定:不再创建隔离目录,不再要求隔离 HOME。
411
+ * 人工结果只需 platform, version, planDigest, result, actor, confirmedAt。
433
412
  *
434
413
  * Referenced from the registry as the codebuddy strategy.buildManualRequirement
435
414
  * — the automatable=false manual-requirement path.
@@ -479,70 +458,51 @@ export async function executeCodeBuddyManualRequirement(action, context) {
479
458
  error: dirErr.message,
480
459
  });
481
460
  }
482
- const cliHome = codebuddyCliHome(attestationDir);
483
- const cliInstallRoot = codebuddyCliInstallRoot(cliHome, CODEBUDDY_MARKETPLACE_NAME, action.plugin);
484
- const desktopInstallRoot = `~/${CODEBUDDY_DESKTOP_HOME_DIR}/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
485
461
 
486
462
  const instructions = buildCodeBuddyManualInstructions({
487
463
  plugin: action.plugin,
488
464
  version: action.version,
489
465
  ref,
490
- cliHome,
491
466
  attestationDir,
492
467
  });
493
468
 
469
+ // 统一 requirement 结构:不再包含隔离目录信息
494
470
  const requirement = {
495
471
  kind: 'codebuddy-manual-install-requirement',
496
- consumer: 'codebuddy',
472
+ platform: 'codebuddy',
497
473
  plugin: action.plugin,
498
474
  version: action.version,
499
- entrySkill: action.entrySkill,
500
475
  repo: action.repo,
501
476
  ref,
502
- marketplace: CODEBUDDY_MARKETPLACE_NAME,
503
- marketplaceSource: CODEBUDDY_MARKETPLACE_SOURCE,
504
- installChannels: ['desktop', 'cli'],
505
- // (A) planDigest binds to the real frozen plan digest;
506
- // expectedPayloadDigest binds separately to the snapshot payload digest.
477
+ entrySkill: action.entrySkill,
507
478
  planDigest,
508
- expectedPayloadDigest: action.manifestDigest,
509
- isolatedHome: cliHome,
510
- codebuddyHome: cliHome,
511
- cliInstallRoot,
512
- desktopInstallRoot,
513
479
  attestationDir,
514
480
  attestationFile: CODEBUDDY_ATTESTATION_FILE,
515
481
  attestationTemplate: {
516
- consumer: 'codebuddy',
517
- plugin: action.plugin,
482
+ platform: 'codebuddy',
518
483
  version: action.version,
519
- entrySkill: action.entrySkill,
520
- repo: action.repo,
521
- ref,
522
- marketplace: CODEBUDDY_MARKETPLACE_NAME,
523
- installChannel: '<"desktop" or "cli">',
524
- installPath: '<actual installed plugin directory for the chosen channel>',
525
484
  planDigest,
526
- payloadDigest: action.manifestDigest,
527
- attestedBy: '<person responsible for the manual install>',
528
- attestedAt: '<ISO 8601 now; must not be in the future>',
529
- 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>',
530
489
  },
531
490
  instructions,
532
491
  };
533
492
 
534
- // Create ONLY the isolated cli home (which transitively creates the authority
535
- // dir); never pre-create the marketplace install subdir — the operator's CLI
536
- // install (cli channel) or the WorkBuddy desktop app (desktop channel) owns
537
- // the actual install directory.
493
+ // Ensure the authority directory exists (no isolated home creation needed).
494
+ const { mkdir } = await import('node:fs/promises');
538
495
  try {
539
- await mkdir(cliHome, { recursive: true, mode: 0o700 });
496
+ await mkdir(attestationDir, { recursive: true, mode: 0o700 });
540
497
  } catch (mkdirErr) {
541
- return createResult({
542
- actionType,
543
- status: ActionStatus.EXECUTE_FAILED,
544
- error: `cannot create codebuddy isolated home directory: ${mkdirErr.message}`,
545
- });
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
+ }
546
506
  }
547
507
 
548
508
  // Idempotent requirement write: an identical existing requirement is left
@@ -599,19 +559,12 @@ export async function executeCodeBuddyManualRequirement(action, context) {
599
559
  observation: {
600
560
  installed: false,
601
561
  manualInstallRequired: true,
602
- consumer: 'codebuddy',
562
+ platform: 'codebuddy',
603
563
  plugin: action.plugin,
604
564
  version: action.version,
605
- entrySkill: action.entrySkill,
606
- repo: action.repo,
607
565
  ref,
608
- marketplace: CODEBUDDY_MARKETPLACE_NAME,
609
- installChannels: ['desktop', 'cli'],
610
566
  planDigest,
611
567
  attestationDir,
612
- codebuddyHome: cliHome,
613
- cliInstallRoot,
614
- desktopInstallRoot,
615
568
  instructions,
616
569
  },
617
570
  });