release-skill 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +46 -0
- package/CONTRIBUTING.md +27 -0
- package/INSTALL.md +95 -139
- package/INSTALL.zh-CN.md +70 -121
- package/README.md +266 -913
- package/README.zh-CN.md +224 -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 +19805 -17556
- package/adapters/claude/schemas/release-plan.schema.json +228 -0
- package/adapters/claude/schemas/release-project.schema.json +93 -0
- package/adapters/claude/schemas/release-run.schema.json +155 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/codex/schemas/release-plan.schema.json +228 -0
- package/adapters/codex/schemas/release-project.schema.json +93 -0
- package/adapters/codex/schemas/release-run.schema.json +155 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/kimi/schemas/release-plan.schema.json +228 -0
- package/adapters/kimi/schemas/release-project.schema.json +93 -0
- package/adapters/kimi/schemas/release-run.schema.json +155 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/workbuddy/schemas/release-plan.schema.json +228 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +155 -2
- package/bin/release-skill.bundle.mjs +19805 -17556
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +228 -0
- package/schemas/release-project.schema.json +93 -0
- package/schemas/release-run.schema.json +155 -2
- package/scripts/sync-public-files.mjs +20 -9
- package/src/adapters/plugin-marketplace.mjs +1237 -403
- package/src/commands/prepare.mjs +454 -40
- package/src/commands/publish.mjs +169 -75
- package/src/commands/reconcile.mjs +92 -327
- package/src/commands/setup.mjs +148 -20
- package/src/commands/verify.mjs +454 -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 +158 -6
- package/src/core/skill-resource-closure.mjs +425 -0
- package/src/platforms/codebuddy.mjs +206 -280
- package/src/platforms/codex.mjs +369 -0
- package/src/platforms/kimi.mjs +191 -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,26 @@ 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, requiresInstalledClosure:boolean}} p
|
|
239
187
|
* @returns {string[]}
|
|
240
188
|
*/
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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({
|
|
190
|
+
plugin,
|
|
191
|
+
version,
|
|
192
|
+
ref,
|
|
193
|
+
attestationDir,
|
|
194
|
+
requiresInstalledClosure,
|
|
195
|
+
}) {
|
|
306
196
|
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.`,
|
|
197
|
+
`CodeBuddy/WorkBuddy 插件安装无法锁定冻结 ref(codebuddy CLI marketplace add/install 没有 ref 选项,跟踪默认分支),因此安装是需要人工结果证明的手动步骤。`,
|
|
198
|
+
`1) publish 完成所有远端写入后进入 PUBLISHED 状态(自动化 Git 分支/标签、npm 和 GitHub Release 写入已完成)。此 codebuddy 检查点标记为需要人工安装。`,
|
|
199
|
+
`2) 从统一市场 "${CODEBUDDY_MARKETPLACE_NAME}" (${CODEBUDDY_MARKETPLACE_SOURCE}) 安装 release-skill。确认安装的插件版本等于冻结版本 ${version}。`,
|
|
200
|
+
`3) 将人工结果 JSON 写入: ${attestationDir}/${CODEBUDDY_ATTESTATION_FILE}`,
|
|
201
|
+
` 必填字段: platform="codebuddy", version, planDigest(冻结计划摘要), result("passed" 或 "failed"), actor(确认人), confirmedAt(ISO 8601 时间戳)${requiresInstalledClosure ? ',installChannel("desktop" 或 "cli"),installPath(实际安装后的插件目录)' : ''}`,
|
|
202
|
+
` 可选字段: note(备注)`,
|
|
203
|
+
`4) 运行 release-skill verify(从同一个计划摘要索引的权威目录读取结果,成功后 -> VERIFIED)。`,
|
|
315
204
|
];
|
|
316
205
|
}
|
|
317
206
|
|
|
@@ -347,129 +236,185 @@ export async function readCodeBuddyManifest(pluginRootReal) {
|
|
|
347
236
|
}
|
|
348
237
|
|
|
349
238
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
239
|
+
* 统一人工结果验证:验证 codebuddy 人工结果是否匹配冻结计划。
|
|
240
|
+
*
|
|
241
|
+
* 统一后的结果只需:
|
|
242
|
+
* - 必填:platform, version, planDigest, result(passed|failed), actor, confirmedAt
|
|
243
|
+
* - 可选:note
|
|
244
|
+
*
|
|
245
|
+
* 旧格式兼容(0.2.3 及更早):
|
|
246
|
+
* - consumer → platform (必须是 'codebuddy')
|
|
247
|
+
* - attestedBy → actor
|
|
248
|
+
* - attestedAt → confirmedAt
|
|
249
|
+
* - payloadDigest → 载荷绑定验证(如果存在)
|
|
352
250
|
*
|
|
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).
|
|
251
|
+
* 绑定验证(任何不匹配都失败):
|
|
252
|
+
* - planDigest 绑定到真正的冻结计划摘要(boundPlanDigest)
|
|
253
|
+
* - version 静态一致性检查
|
|
254
|
+
* - result 只接受 passed 或 failed
|
|
369
255
|
*
|
|
370
|
-
* @param {object} attestation -
|
|
371
|
-
* @param {object} action -
|
|
372
|
-
* @param {string} isoNow -
|
|
373
|
-
* @param {string} boundPlanDigest -
|
|
374
|
-
* @returns {{valid:boolean, error:string|null}}
|
|
256
|
+
* @param {object} attestation - 解析后的人工结果 JSON。
|
|
257
|
+
* @param {object} action - 展开的 codebuddy 动作(顶层字段)。
|
|
258
|
+
* @param {string} isoNow - 当前 ISO 时间戳(保留签名兼容,不再用于过期检查)。
|
|
259
|
+
* @param {string} boundPlanDigest - 验证过的冻结计划摘要。
|
|
260
|
+
* @returns {{valid:boolean, error:string|null, normalized:object|null}}
|
|
375
261
|
*/
|
|
376
262
|
export function validateCodeBuddyAttestation(attestation, action, isoNow, boundPlanDigest) {
|
|
377
263
|
if (!attestation || typeof attestation !== 'object' || Array.isArray(attestation)) {
|
|
378
|
-
return { valid: false, error: 'codebuddy attestation is not an object' };
|
|
264
|
+
return { valid: false, error: 'codebuddy attestation is not an object', normalized: null };
|
|
379
265
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
266
|
+
|
|
267
|
+
// 旧格式归一化:将旧字段映射到新字段
|
|
268
|
+
const normalized = { ...attestation };
|
|
269
|
+
|
|
270
|
+
// 旧格式识别:完整旧标识组(consumer, attestedBy, attestedAt)全部存在,
|
|
271
|
+
// 且新格式字段组(result, actor, confirmedAt)未混入。
|
|
272
|
+
const hasCompleteOldMarkers = !!(normalized.consumer && normalized.attestedBy && normalized.attestedAt);
|
|
273
|
+
const hasNewFormatFields = !!(normalized.result || normalized.actor || normalized.confirmedAt);
|
|
274
|
+
const isOldFormat = hasCompleteOldMarkers && !hasNewFormatFields;
|
|
275
|
+
|
|
276
|
+
// 新格式严格要求 result 字段;仅当确认为旧格式时才允许缺省
|
|
277
|
+
if (!normalized.result && isOldFormat) {
|
|
278
|
+
normalized.result = 'passed';
|
|
385
279
|
}
|
|
386
|
-
|
|
387
|
-
|
|
280
|
+
|
|
281
|
+
// consumer → platform (旧格式使用 consumer)
|
|
282
|
+
if (!normalized.platform && normalized.consumer) {
|
|
283
|
+
normalized.platform = normalized.consumer;
|
|
388
284
|
}
|
|
389
|
-
|
|
390
|
-
|
|
285
|
+
// attestedBy → actor (旧格式使用 attestedBy)
|
|
286
|
+
if (!normalized.actor && normalized.attestedBy) {
|
|
287
|
+
normalized.actor = normalized.attestedBy;
|
|
391
288
|
}
|
|
392
|
-
|
|
393
|
-
|
|
289
|
+
// attestedAt → confirmedAt (旧格式使用 attestedAt)
|
|
290
|
+
if (!normalized.confirmedAt && normalized.attestedAt) {
|
|
291
|
+
normalized.confirmedAt = normalized.attestedAt;
|
|
394
292
|
}
|
|
395
|
-
|
|
396
|
-
|
|
293
|
+
|
|
294
|
+
// 统一必填字段
|
|
295
|
+
const requiredStrings = ['platform', 'version', 'planDigest', 'result', 'actor', 'confirmedAt'];
|
|
296
|
+
for (const field of requiredStrings) {
|
|
297
|
+
if (typeof normalized[field] !== 'string' || normalized[field].length === 0) {
|
|
298
|
+
return { valid: false, error: `codebuddy attestation missing required field "${field}"`, normalized: null };
|
|
299
|
+
}
|
|
397
300
|
}
|
|
398
|
-
if (
|
|
399
|
-
return { valid: false, error:
|
|
301
|
+
if (normalized.platform !== 'codebuddy') {
|
|
302
|
+
return { valid: false, error: `codebuddy attestation platform "${normalized.platform}" must be "codebuddy"`, normalized: null };
|
|
400
303
|
}
|
|
401
|
-
|
|
402
|
-
|
|
304
|
+
|
|
305
|
+
// installPath 路径安全校验:CLI 通道必须在预期目录内
|
|
306
|
+
if (normalized.installPath !== undefined) {
|
|
307
|
+
if (typeof normalized.installPath !== 'string' || normalized.installPath.length === 0) {
|
|
308
|
+
return { valid: false, error: 'codebuddy attestation installPath must be a non-empty string when present', normalized: null };
|
|
309
|
+
}
|
|
310
|
+
if (normalized.installPath.includes('..')) {
|
|
311
|
+
return { valid: false, error: 'codebuddy attestation installPath must not contain path traversal ("..")', normalized: null };
|
|
312
|
+
}
|
|
313
|
+
// CLI 通道:installPath 必须以预期后缀结尾
|
|
314
|
+
if (normalized.installChannel === 'cli') {
|
|
315
|
+
const expectedSuffix = `.workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
|
|
316
|
+
if (!normalized.installPath.endsWith(expectedSuffix)) {
|
|
317
|
+
return { valid: false, error: `codebuddy CLI attestation installPath must end with "${expectedSuffix}", got "${normalized.installPath}"`, normalized: null };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
403
320
|
}
|
|
404
|
-
|
|
405
|
-
|
|
321
|
+
|
|
322
|
+
// result 只接受 passed 或 failed
|
|
323
|
+
if (normalized.result !== 'passed' && normalized.result !== 'failed') {
|
|
324
|
+
return { valid: false, error: `codebuddy attestation result "${normalized.result}" must be "passed" or "failed"`, normalized: null };
|
|
325
|
+
}
|
|
326
|
+
// planDigest 绑定验证
|
|
327
|
+
if (!HEX_DIGEST_RE.test(normalized.planDigest)) {
|
|
328
|
+
return { valid: false, error: 'codebuddy attestation planDigest must be a 64-char lowercase hex digest', normalized: null };
|
|
406
329
|
}
|
|
407
|
-
if (
|
|
408
|
-
return { valid: false, error:
|
|
330
|
+
if (normalized.planDigest !== boundPlanDigest) {
|
|
331
|
+
return { valid: false, error: 'codebuddy attestation planDigest does not match the frozen plan digest', normalized: null };
|
|
409
332
|
}
|
|
410
|
-
|
|
411
|
-
|
|
333
|
+
// version 静态一致性检查
|
|
334
|
+
if (normalized.version !== action.version) {
|
|
335
|
+
return { valid: false, error: `codebuddy attestation version "${normalized.version}" does not match action version "${action.version}"`, normalized: null };
|
|
412
336
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
337
|
+
|
|
338
|
+
// 旧格式额外绑定字段校验:plugin, repo, ref, entrySkill, payloadDigest
|
|
339
|
+
if (isOldFormat) {
|
|
340
|
+
if (normalized.plugin !== action.plugin) {
|
|
341
|
+
return { valid: false, error: `codebuddy attestation plugin "${normalized.plugin}" does not match action plugin "${action.plugin}"`, normalized: null };
|
|
342
|
+
}
|
|
343
|
+
if (normalized.repo !== action.repo) {
|
|
344
|
+
return { valid: false, error: `codebuddy attestation repo "${normalized.repo}" does not match action repo "${action.repo}"`, normalized: null };
|
|
345
|
+
}
|
|
346
|
+
const expectedRef = action.ref ?? `v${action.version}`;
|
|
347
|
+
if (normalized.ref !== expectedRef) {
|
|
348
|
+
return { valid: false, error: `codebuddy attestation ref "${normalized.ref}" does not match action ref "${expectedRef}"`, normalized: null };
|
|
349
|
+
}
|
|
350
|
+
if (normalized.entrySkill !== action.entrySkill) {
|
|
351
|
+
return { valid: false, error: `codebuddy attestation entrySkill "${normalized.entrySkill}" does not match action entrySkill "${action.entrySkill}"`, normalized: null };
|
|
352
|
+
}
|
|
353
|
+
// payloadDigest 旧格式必填、格式合法且等于冻结动作 manifestDigest
|
|
354
|
+
if (typeof normalized.payloadDigest !== 'string' || normalized.payloadDigest.length === 0) {
|
|
355
|
+
return { valid: false, error: 'codebuddy attestation payloadDigest is required for old-format receipts', normalized: null };
|
|
356
|
+
}
|
|
357
|
+
if (!HEX_DIGEST_RE.test(normalized.payloadDigest)) {
|
|
358
|
+
return { valid: false, error: 'codebuddy attestation payloadDigest must be a 64-char lowercase hex digest', normalized: null };
|
|
359
|
+
}
|
|
360
|
+
if (action.manifestDigest && normalized.payloadDigest !== action.manifestDigest) {
|
|
361
|
+
return { valid: false, error: 'codebuddy attestation payloadDigest does not match the frozen manifest digest', normalized: null };
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
// 新格式 payloadDigest 可选,但如果存在则必须合法
|
|
365
|
+
if (normalized.payloadDigest !== undefined) {
|
|
366
|
+
if (!HEX_DIGEST_RE.test(normalized.payloadDigest)) {
|
|
367
|
+
return { valid: false, error: 'codebuddy attestation payloadDigest must be a 64-char lowercase hex digest when present', normalized: null };
|
|
368
|
+
}
|
|
369
|
+
if (action.manifestDigest && normalized.payloadDigest !== action.manifestDigest) {
|
|
370
|
+
return { valid: false, error: 'codebuddy attestation payloadDigest does not match the frozen manifest digest', normalized: null };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
416
373
|
}
|
|
417
|
-
|
|
418
|
-
|
|
374
|
+
|
|
375
|
+
// confirmedAt 必须是有效的时间戳
|
|
376
|
+
const confirmedMs = Date.parse(normalized.confirmedAt);
|
|
377
|
+
if (!Number.isFinite(confirmedMs)) {
|
|
378
|
+
return { valid: false, error: 'codebuddy attestation confirmedAt must be a valid ISO timestamp', normalized: null };
|
|
419
379
|
}
|
|
420
|
-
|
|
421
|
-
const expiresMs = Date.parse(attestation.expiresAt);
|
|
380
|
+
// confirmedAt 不得在未来
|
|
422
381
|
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 };
|
|
382
|
+
if (Number.isFinite(nowMs) && confirmedMs > nowMs) {
|
|
383
|
+
return { valid: false, error: 'codebuddy attestation confirmedAt must not be in the future', normalized: null };
|
|
384
|
+
}
|
|
385
|
+
// expiresAt 校验(可选字段)
|
|
386
|
+
if (normalized.expiresAt !== undefined) {
|
|
387
|
+
const expiresMs = Date.parse(normalized.expiresAt);
|
|
388
|
+
if (!Number.isFinite(expiresMs)) {
|
|
389
|
+
return { valid: false, error: 'codebuddy attestation expiresAt must be a valid ISO timestamp', normalized: null };
|
|
390
|
+
}
|
|
391
|
+
// 已过期
|
|
392
|
+
if (Number.isFinite(nowMs) && expiresMs < nowMs) {
|
|
393
|
+
return { valid: false, error: 'codebuddy attestation has expired', normalized: null };
|
|
394
|
+
}
|
|
395
|
+
// 有效期不得超过 24 小时
|
|
396
|
+
if (expiresMs - confirmedMs > CODEBUDDY_MAX_ATTESTATION_VALIDITY_MS) {
|
|
397
|
+
return { valid: false, error: 'codebuddy attestation validity exceeds 24 hours', normalized: null };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// 可选字段 note 如果存在必须是字符串
|
|
401
|
+
if (normalized.note !== undefined && typeof normalized.note !== 'string') {
|
|
402
|
+
return { valid: false, error: 'codebuddy attestation note must be a string when present', normalized: null };
|
|
403
|
+
}
|
|
404
|
+
return { valid: true, error: null, normalized };
|
|
455
405
|
}
|
|
456
406
|
|
|
457
407
|
/**
|
|
458
408
|
* CodeBuddy protocol capability gap: the codebuddy CLI cannot pin a frozen ref,
|
|
459
409
|
* so there is NO trustworthy automated install checkpoint. execute NEVER execs a
|
|
460
410
|
* codebuddy command. Instead it emits an actionable manual-install requirement
|
|
461
|
-
* bound to the real frozen plan digest + identity
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
411
|
+
* bound to the real frozen plan digest + identity, and leaves success to
|
|
412
|
+
* observe/verify, which consume only a trusted human attestation plus read-only
|
|
413
|
+
* verification. Without that proof the checkpoint fails closed and can never
|
|
414
|
+
* reach VERIFIED.
|
|
465
415
|
*
|
|
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.
|
|
416
|
+
* 统一人工判定:不再创建隔离目录,不再要求隔离 HOME。
|
|
417
|
+
* 人工结果只需 platform, version, planDigest, result, actor, confirmedAt。
|
|
473
418
|
*
|
|
474
419
|
* Referenced from the registry as the codebuddy strategy.buildManualRequirement
|
|
475
420
|
* — the automatable=false manual-requirement path.
|
|
@@ -519,70 +464,58 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
519
464
|
error: dirErr.message,
|
|
520
465
|
});
|
|
521
466
|
}
|
|
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
467
|
|
|
526
468
|
const instructions = buildCodeBuddyManualInstructions({
|
|
527
469
|
plugin: action.plugin,
|
|
528
470
|
version: action.version,
|
|
529
471
|
ref,
|
|
530
|
-
cliHome,
|
|
531
472
|
attestationDir,
|
|
473
|
+
requiresInstalledClosure: Boolean(context.plan?.skillResourceClosure),
|
|
532
474
|
});
|
|
533
475
|
|
|
476
|
+
// 统一 requirement 结构:不再包含隔离目录信息
|
|
534
477
|
const requirement = {
|
|
535
478
|
kind: 'codebuddy-manual-install-requirement',
|
|
536
|
-
|
|
479
|
+
platform: 'codebuddy',
|
|
537
480
|
plugin: action.plugin,
|
|
538
481
|
version: action.version,
|
|
539
|
-
entrySkill: action.entrySkill,
|
|
540
482
|
repo: action.repo,
|
|
541
483
|
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.
|
|
484
|
+
entrySkill: action.entrySkill,
|
|
547
485
|
planDigest,
|
|
548
|
-
expectedPayloadDigest: action.manifestDigest,
|
|
549
|
-
isolatedHome: cliHome,
|
|
550
|
-
codebuddyHome: cliHome,
|
|
551
|
-
cliInstallRoot,
|
|
552
|
-
desktopInstallRoot,
|
|
553
486
|
attestationDir,
|
|
554
487
|
attestationFile: CODEBUDDY_ATTESTATION_FILE,
|
|
555
488
|
attestationTemplate: {
|
|
556
|
-
|
|
557
|
-
plugin: action.plugin,
|
|
489
|
+
platform: 'codebuddy',
|
|
558
490
|
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
491
|
planDigest,
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
492
|
+
result: '<"passed" or "failed">',
|
|
493
|
+
actor: '<person who confirmed the install>',
|
|
494
|
+
confirmedAt: '<ISO 8601 timestamp>',
|
|
495
|
+
...(context.plan?.skillResourceClosure
|
|
496
|
+
? {
|
|
497
|
+
installChannel: '<"desktop" or "cli">',
|
|
498
|
+
installPath: `<actual .workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin} directory>`,
|
|
499
|
+
}
|
|
500
|
+
: {}),
|
|
501
|
+
note: '<optional note>',
|
|
570
502
|
},
|
|
571
503
|
instructions,
|
|
572
504
|
};
|
|
573
505
|
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
// install (cli channel) or the WorkBuddy desktop app (desktop channel) owns
|
|
577
|
-
// the actual install directory.
|
|
506
|
+
// Ensure the authority directory exists (no isolated home creation needed).
|
|
507
|
+
const { mkdir } = await import('node:fs/promises');
|
|
578
508
|
try {
|
|
579
|
-
await mkdir(
|
|
509
|
+
await mkdir(attestationDir, { recursive: true, mode: 0o700 });
|
|
580
510
|
} catch (mkdirErr) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
511
|
+
// EEXIST is fine; other errors are fatal.
|
|
512
|
+
if (mkdirErr?.code !== 'EEXIST') {
|
|
513
|
+
return createResult({
|
|
514
|
+
actionType,
|
|
515
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
516
|
+
error: `cannot create codebuddy attestation directory: ${mkdirErr.message}`,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
586
519
|
}
|
|
587
520
|
|
|
588
521
|
// Idempotent requirement write: an identical existing requirement is left
|
|
@@ -639,19 +572,12 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
639
572
|
observation: {
|
|
640
573
|
installed: false,
|
|
641
574
|
manualInstallRequired: true,
|
|
642
|
-
|
|
575
|
+
platform: 'codebuddy',
|
|
643
576
|
plugin: action.plugin,
|
|
644
577
|
version: action.version,
|
|
645
|
-
entrySkill: action.entrySkill,
|
|
646
|
-
repo: action.repo,
|
|
647
578
|
ref,
|
|
648
|
-
marketplace: CODEBUDDY_MARKETPLACE_NAME,
|
|
649
|
-
installChannels: ['desktop', 'cli'],
|
|
650
579
|
planDigest,
|
|
651
580
|
attestationDir,
|
|
652
|
-
codebuddyHome: cliHome,
|
|
653
|
-
cliInstallRoot,
|
|
654
|
-
desktopInstallRoot,
|
|
655
581
|
instructions,
|
|
656
582
|
},
|
|
657
583
|
});
|