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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +27 -0
- package/INSTALL.md +95 -139
- package/INSTALL.zh-CN.md +70 -121
- package/README.md +264 -913
- package/README.zh-CN.md +222 -535
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +19054 -17573
- package/adapters/claude/schemas/release-plan.schema.json +137 -0
- package/adapters/claude/schemas/release-project.schema.json +93 -0
- package/adapters/claude/schemas/release-run.schema.json +70 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +19054 -17573
- package/adapters/codex/schemas/release-plan.schema.json +137 -0
- package/adapters/codex/schemas/release-project.schema.json +93 -0
- package/adapters/codex/schemas/release-run.schema.json +70 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +19054 -17573
- package/adapters/kimi/schemas/release-plan.schema.json +137 -0
- package/adapters/kimi/schemas/release-project.schema.json +93 -0
- package/adapters/kimi/schemas/release-run.schema.json +70 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +19054 -17573
- package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
- package/bin/release-skill.bundle.mjs +19054 -17573
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +137 -0
- package/schemas/release-project.schema.json +93 -0
- package/schemas/release-run.schema.json +70 -2
- package/src/adapters/plugin-marketplace.mjs +1190 -435
- package/src/commands/prepare.mjs +380 -40
- package/src/commands/publish.mjs +107 -75
- package/src/commands/reconcile.mjs +92 -327
- package/src/commands/setup.mjs +148 -20
- package/src/commands/verify.mjs +304 -20
- package/src/core/baseline.mjs +8 -1
- package/src/core/checkpoints.mjs +50 -7
- package/src/core/config.mjs +15 -0
- package/src/core/errors.mjs +2 -0
- package/src/core/installation-contract.mjs +341 -0
- package/src/core/plan.mjs +129 -6
- package/src/platforms/codebuddy.mjs +193 -280
- package/src/platforms/codex.mjs +369 -0
- package/src/platforms/kimi.mjs +164 -119
- 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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
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
|
-
*
|
|
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}
|
|
238
|
-
* @param {string} plugin
|
|
186
|
+
* @param {{plugin:string, version:string, ref:string, attestationDir:string}} p
|
|
239
187
|
* @returns {string[]}
|
|
240
188
|
*/
|
|
241
|
-
function
|
|
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
|
|
308
|
-
`1) publish
|
|
309
|
-
`2)
|
|
310
|
-
`3)
|
|
311
|
-
`
|
|
312
|
-
`
|
|
313
|
-
`
|
|
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 插件安装无法锁定冻结 ref(codebuddy 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
|
-
*
|
|
351
|
-
*
|
|
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
|
-
*
|
|
354
|
-
* -
|
|
355
|
-
* -
|
|
356
|
-
* -
|
|
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 -
|
|
371
|
-
* @param {object} action -
|
|
372
|
-
* @param {string} isoNow -
|
|
373
|
-
* @param {string} boundPlanDigest -
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
|
|
274
|
+
|
|
275
|
+
// consumer → platform (旧格式使用 consumer)
|
|
276
|
+
if (!normalized.platform && normalized.consumer) {
|
|
277
|
+
normalized.platform = normalized.consumer;
|
|
388
278
|
}
|
|
389
|
-
|
|
390
|
-
|
|
279
|
+
// attestedBy → actor (旧格式使用 attestedBy)
|
|
280
|
+
if (!normalized.actor && normalized.attestedBy) {
|
|
281
|
+
normalized.actor = normalized.attestedBy;
|
|
391
282
|
}
|
|
392
|
-
|
|
393
|
-
|
|
283
|
+
// attestedAt → confirmedAt (旧格式使用 attestedAt)
|
|
284
|
+
if (!normalized.confirmedAt && normalized.attestedAt) {
|
|
285
|
+
normalized.confirmedAt = normalized.attestedAt;
|
|
394
286
|
}
|
|
395
|
-
|
|
396
|
-
|
|
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 (
|
|
399
|
-
return { valid: false, error:
|
|
295
|
+
if (normalized.platform !== 'codebuddy') {
|
|
296
|
+
return { valid: false, error: `codebuddy attestation platform "${normalized.platform}" must be "codebuddy"`, normalized: null };
|
|
400
297
|
}
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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 (
|
|
408
|
-
return { valid: false, error:
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
421
|
-
const expiresMs = Date.parse(attestation.expiresAt);
|
|
374
|
+
// confirmedAt 不得在未来
|
|
422
375
|
const nowMs = Date.parse(isoNow);
|
|
423
|
-
if (
|
|
424
|
-
return { valid: false, error: 'codebuddy attestation
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
//
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
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
|
-
*
|
|
467
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
//
|
|
575
|
-
|
|
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(
|
|
496
|
+
await mkdir(attestationDir, { recursive: true, mode: 0o700 });
|
|
580
497
|
} catch (mkdirErr) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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
|
});
|