release-skill 0.4.1 → 0.5.0
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 +41 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +27 -9
- package/README.zh-CN.md +23 -8
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/error-codes.json +154 -0
- package/adapters/claude/bin/kernel-protocol.json +60 -0
- package/adapters/claude/bin/registry.json +165 -0
- package/adapters/claude/bin/release-skill.bundle.mjs +3918 -1462
- package/adapters/claude/bin/rules.json +104 -0
- package/adapters/claude/schemas/release-plan.schema.json +5 -0
- package/adapters/claude/schemas/release-project.schema.json +5 -0
- package/adapters/claude/skills/release-assess/SKILL.md +2 -0
- package/adapters/claude/skills/release-help/SKILL.md +3 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +2 -2
- package/adapters/claude/skills/release-verify/SKILL.md +2 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/error-codes.json +154 -0
- package/adapters/codex/bin/kernel-protocol.json +60 -0
- package/adapters/codex/bin/registry.json +165 -0
- package/adapters/codex/bin/release-skill.bundle.mjs +3918 -1462
- package/adapters/codex/bin/rules.json +104 -0
- package/adapters/codex/schemas/release-plan.schema.json +5 -0
- package/adapters/codex/schemas/release-project.schema.json +5 -0
- package/adapters/codex/skills/release-assess/SKILL.md +2 -0
- package/adapters/codex/skills/release-help/SKILL.md +3 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +2 -2
- package/adapters/codex/skills/release-verify/SKILL.md +2 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/error-codes.json +154 -0
- package/adapters/kimi/bin/kernel-protocol.json +60 -0
- package/adapters/kimi/bin/registry.json +165 -0
- package/adapters/kimi/bin/release-skill.bundle.mjs +3918 -1462
- package/adapters/kimi/bin/rules.json +104 -0
- package/adapters/kimi/schemas/release-plan.schema.json +5 -0
- package/adapters/kimi/schemas/release-project.schema.json +5 -0
- package/adapters/kimi/skills/release-assess/SKILL.md +2 -0
- package/adapters/kimi/skills/release-help/SKILL.md +3 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +2 -2
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/error-codes.json +154 -0
- package/adapters/workbuddy/bin/kernel-protocol.json +60 -0
- package/adapters/workbuddy/bin/registry.json +165 -0
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +3918 -1462
- package/adapters/workbuddy/bin/rules.json +104 -0
- package/adapters/workbuddy/schemas/release-plan.schema.json +5 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +5 -0
- package/adapters/workbuddy/skills/release-assess/SKILL.md +2 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +3 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +2 -2
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
- package/bin/error-codes.json +154 -0
- package/bin/kernel-protocol.json +60 -0
- package/bin/registry.json +165 -0
- package/bin/release-skill.bundle.mjs +3918 -1462
- package/bin/rules.json +104 -0
- package/package.json +3 -1
- package/schemas/release-plan.schema.json +5 -0
- package/schemas/release-project.schema.json +5 -0
- package/scripts/build-bundle.mjs +159 -5
- package/scripts/sync-public-files.mjs +5 -0
- package/skills/release-assess/SKILL.md +2 -0
- package/skills/release-help/SKILL.md +3 -1
- package/skills/release-prepare/SKILL.md +2 -2
- package/skills/release-verify/SKILL.md +2 -0
- package/skills-src/release-assess/SKILL.md +2 -0
- package/skills-src/release-help/SKILL.md +3 -1
- package/skills-src/release-prepare/SKILL.md +2 -2
- package/skills-src/release-verify/SKILL.md +2 -0
- package/src/adapters/contract.mjs +10 -10
- package/src/adapters/plugin-marketplace.mjs +21 -5
- package/src/artifacts/entry.mjs +4 -1
- package/src/artifacts/producer-registry.mjs +9 -6
- package/src/artifacts/project-lock.mjs +324 -214
- package/src/commands/assess.mjs +60 -2
- package/src/commands/attest.mjs +11 -0
- package/src/commands/prepare.mjs +19 -13
- package/src/core/approval.mjs +5 -2
- package/src/core/digest.mjs +47 -12
- package/src/core/foundation-inflight.mjs +39 -0
- package/src/core/hook-cache.mjs +3 -0
- package/src/core/installation-contract.mjs +4 -2
- package/src/core/plan.mjs +34 -30
- package/src/core/run.mjs +22 -36
- package/src/core/skill-resource-closure.mjs +6 -4
- package/src/core/trusted-resource.mjs +64 -15
- package/src/platforms/codebuddy.mjs +92 -10
- package/src/platforms/registry.mjs +4 -3
- package/src/producers/build-adapters.mjs +27 -9
- package/src/producers/render-public-assets.mjs +12 -7
- package/src/producers/sync-skills.mjs +12 -7
- package/src/snapshot/public-map.mjs +10 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { lstat, readFile, readdir } from 'node:fs/promises';
|
|
11
11
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
12
12
|
|
|
13
|
-
import { canonicalJson,
|
|
13
|
+
import { canonicalJson, digestDocument } from 'skill-family-contracts';
|
|
14
14
|
import { GATE_FAILED, ReleaseError } from './errors.mjs';
|
|
15
15
|
import { computeFrozenSnapshot } from '../snapshot/frozen.mjs';
|
|
16
16
|
|
|
@@ -258,6 +258,8 @@ export function createSkillResourceClosureReceipt(result, identity = {}) {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
export function assertSkillResourceClosureReceipt(expected, observed, label = 'release unit') {
|
|
261
|
+
// 收据对象均为纯 JSON(JSON.parse 派生或固定字段投影),直接委托 Foundation
|
|
262
|
+
// contracts 严格 canonicalJson 比较(非 JSON 输入 fail-closed,抛 TypeError)。
|
|
261
263
|
if (canonicalJson(expected) !== canonicalJson(observed)) {
|
|
262
264
|
throw new ReleaseError(
|
|
263
265
|
GATE_FAILED,
|
|
@@ -319,9 +321,9 @@ export async function checkSkillResourceClosure({
|
|
|
319
321
|
})).digest;
|
|
320
322
|
} catch (error) {
|
|
321
323
|
snapshotError = error;
|
|
322
|
-
inputDigest =
|
|
324
|
+
inputDigest = digestDocument({
|
|
323
325
|
invalidTree: error.message,
|
|
324
|
-
})
|
|
326
|
+
});
|
|
325
327
|
}
|
|
326
328
|
const skillPaths = await discoverSkills(scanRoot);
|
|
327
329
|
const findings = [];
|
|
@@ -426,6 +428,6 @@ export async function checkSkillResourceClosure({
|
|
|
426
428
|
sourceOnlyCount,
|
|
427
429
|
findings,
|
|
428
430
|
};
|
|
429
|
-
result.receiptDigest =
|
|
431
|
+
result.receiptDigest = digestDocument(receiptProjection(result));
|
|
430
432
|
return result;
|
|
431
433
|
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read package-owned runtime resources without following untrusted links.
|
|
3
3
|
* Installed adapters use this for schemas and native manifests.
|
|
4
|
+
*
|
|
5
|
+
* 机制已委托 Foundation(skill-family-foundation-adoption-20260815 节点 1.6):
|
|
6
|
+
* - 异步变体 `readTrustedPackageResource`:路径包含三层校验(词法/符号链接/
|
|
7
|
+
* realpath 逃逸)委托 `resolveContained`,读取委托 `readFileContained`;
|
|
8
|
+
* 本地保留域策略前置校验 `assertStat`(拒绝任何符号链接、非常规文件与
|
|
9
|
+
* 硬链接),失败原因映射保持原语义。
|
|
10
|
+
* - 同步变体 `readTrustedPackageResourceSync`:Foundation 无同步路径 API,
|
|
11
|
+
* 词法分类委托同步纯函数 `classifyPathInput`,其余本地同步实现保留
|
|
12
|
+
* (残留,见 1.6 run-report 剩余风险)。
|
|
4
13
|
*/
|
|
5
14
|
|
|
6
15
|
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
7
|
-
import { lstat
|
|
16
|
+
import { lstat } from 'node:fs/promises';
|
|
8
17
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
9
18
|
|
|
19
|
+
import { classifyPathInput, readFileContained, resolveContained } from 'skill-family-harness-node';
|
|
20
|
+
|
|
10
21
|
import { CONFIG_INVALID, ReleaseError } from './errors.mjs';
|
|
11
22
|
import { PKG_ROOT } from './pkg-root.mjs';
|
|
12
23
|
|
|
@@ -18,6 +29,8 @@ function fail(code, resource, reason) {
|
|
|
18
29
|
);
|
|
19
30
|
}
|
|
20
31
|
|
|
32
|
+
// ---- 同步变体保留的本地实现(Foundation 无同步路径 API,文档化残留) ----
|
|
33
|
+
|
|
21
34
|
function lexicalPath(resource, code) {
|
|
22
35
|
if (typeof resource !== 'string' || resource.length === 0 || isAbsolute(resource)) {
|
|
23
36
|
fail(code, String(resource), 'INVALID_RESOURCE_PATH');
|
|
@@ -43,7 +56,44 @@ function assertPhysicalContainment(physicalRoot, physicalPath, resource, code) {
|
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
|
|
59
|
+
// ---- Foundation 机制错误 → 本地失败语义映射 ----
|
|
60
|
+
|
|
61
|
+
/** 把 Foundation HarnessError(SFC2004 + details.kind)映射为本地失败原因。 */
|
|
62
|
+
function harnessKindToReason(kind) {
|
|
63
|
+
switch (kind) {
|
|
64
|
+
case 'path-traversal':
|
|
65
|
+
return 'RESOURCE_PATH_ESCAPE';
|
|
66
|
+
case 'symlink-escape':
|
|
67
|
+
// 本地语义:lstat 前置校验对任何符号链接一律报 SYMLINK(不区分指向)。
|
|
68
|
+
// Foundation 对指向包外的符号链接先抛 symlink-escape,映射回 SYMLINK
|
|
69
|
+
// 保持原失败原因;包内符号链接由本地 assertStat 以 SYMLINK 拦截。
|
|
70
|
+
return 'SYMLINK';
|
|
71
|
+
case 'realpath-escape':
|
|
72
|
+
return 'PHYSICAL_PATH_ESCAPE';
|
|
73
|
+
case 'missing-resource':
|
|
74
|
+
return 'MISSING';
|
|
75
|
+
case 'read-failed':
|
|
76
|
+
return 'READ_FAILED';
|
|
77
|
+
default:
|
|
78
|
+
// invalid-path / absolute-path / windows-drive-path / unc-path /
|
|
79
|
+
// windows-path / invalid-root 等统一归入无效资源路径。
|
|
80
|
+
return 'INVALID_RESOURCE_PATH';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function mapHarnessError(cause, resource, code) {
|
|
85
|
+
if (cause && cause.code === 'SFC2004' && cause.details && typeof cause.details.kind === 'string') {
|
|
86
|
+
fail(code, resource, harnessKindToReason(cause.details.kind));
|
|
87
|
+
}
|
|
88
|
+
throw cause;
|
|
89
|
+
}
|
|
90
|
+
|
|
46
91
|
export function readTrustedPackageResourceSync(resource, { code = CONFIG_INVALID } = {}) {
|
|
92
|
+
// 词法分类委托 Foundation classifyPathInput(同步纯函数;比本地词法检查
|
|
93
|
+
// 更严格地拒绝反斜杠/NUL/UNC/盘符相对等跨平台歧义输入,fail-closed)。
|
|
94
|
+
const classification = classifyPathInput(resource, process.platform);
|
|
95
|
+
if (!classification.ok) fail(code, String(resource), 'INVALID_RESOURCE_PATH');
|
|
96
|
+
|
|
47
97
|
const path = lexicalPath(resource, code);
|
|
48
98
|
let stat;
|
|
49
99
|
try {
|
|
@@ -70,27 +120,26 @@ export function readTrustedPackageResourceSync(resource, { code = CONFIG_INVALID
|
|
|
70
120
|
}
|
|
71
121
|
|
|
72
122
|
export async function readTrustedPackageResource(resource, { code = CONFIG_INVALID } = {}) {
|
|
73
|
-
|
|
123
|
+
// 机制委托:resolveContained(词法/符号链接/realpath 三层包含校验)。
|
|
124
|
+
let target;
|
|
125
|
+
try {
|
|
126
|
+
target = await resolveContained(PKG_ROOT, resource);
|
|
127
|
+
} catch (cause) {
|
|
128
|
+
mapHarnessError(cause, resource, code);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 本地域策略前置:拒绝任何符号链接(含指向包内目标的)、非常规文件与硬链接。
|
|
74
132
|
let stat;
|
|
75
133
|
try {
|
|
76
|
-
stat = await lstat(
|
|
134
|
+
stat = await lstat(target);
|
|
77
135
|
} catch {
|
|
78
136
|
fail(code, resource, 'MISSING');
|
|
79
137
|
}
|
|
80
138
|
assertStat(stat, resource, code);
|
|
81
139
|
|
|
82
|
-
let physicalRoot;
|
|
83
|
-
let physicalPath;
|
|
84
140
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
fail(code, resource, 'REALPATH_FAILED');
|
|
89
|
-
}
|
|
90
|
-
assertPhysicalContainment(physicalRoot, physicalPath, resource, code);
|
|
91
|
-
try {
|
|
92
|
-
return await readFile(physicalPath);
|
|
93
|
-
} catch {
|
|
94
|
-
fail(code, resource, 'READ_FAILED');
|
|
141
|
+
return await readFileContained(PKG_ROOT, resource);
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
mapHarnessError(cause, resource, code);
|
|
95
144
|
}
|
|
96
145
|
}
|
|
@@ -88,15 +88,55 @@ export const CODEBUDDY_REQUIREMENT_FILE = 'release-skill-codebuddy-manual-instal
|
|
|
88
88
|
export const CODEBUDDY_ATTESTATION_FILE = 'release-skill-codebuddy-attestation.json';
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* segment in both channels
|
|
91
|
+
* Default unified marketplace the WorkBuddy desktop app and the codebuddy CLI
|
|
92
|
+
* install from (verified fact). A distribution may override this by declaring
|
|
93
|
+
* `marketplace` (see resolveCodeBuddyMarketplace); the install path is
|
|
94
|
+
* validated against the resolved marketplace segment in both channels, and a
|
|
95
|
+
* declared attestation `marketplace` field must equal the resolved name.
|
|
95
96
|
*/
|
|
96
97
|
export const CODEBUDDY_MARKETPLACE_NAME = 'artifact-skill-set';
|
|
97
|
-
/**
|
|
98
|
+
/** Default marketplace source URL (used only when no marketplace override is declared). */
|
|
98
99
|
export const CODEBUDDY_MARKETPLACE_SOURCE = 'https://github.com/ifoohoo/artifact-skill-set';
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the unified marketplace name for a codebuddy action.
|
|
103
|
+
*
|
|
104
|
+
* Falls back to CODEBUDDY_MARKETPLACE_NAME when the action carries no
|
|
105
|
+
* `marketplace` (legacy frozen plans, byte-identical behavior). A declared
|
|
106
|
+
* marketplace must be a non-empty string; anything else fails closed.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} [action] - expanded codebuddy action (may be undefined).
|
|
109
|
+
* @returns {string} the resolved marketplace name.
|
|
110
|
+
* @throws {Error} when a declared marketplace is not a non-empty string.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveCodeBuddyMarketplace(action) {
|
|
113
|
+
const marketplace = action?.marketplace ?? CODEBUDDY_MARKETPLACE_NAME;
|
|
114
|
+
if (typeof marketplace !== 'string' || marketplace.length === 0) {
|
|
115
|
+
throw new Error(`codebuddy marketplace must be a non-empty string when declared, got ${JSON.stringify(action?.marketplace)}`);
|
|
116
|
+
}
|
|
117
|
+
return marketplace;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the marketplace source URL for a codebuddy action, when knowable.
|
|
122
|
+
*
|
|
123
|
+
* The default marketplace has a verified source constant; a configured
|
|
124
|
+
* marketplace only has a source when the distribution declared
|
|
125
|
+
* `marketplaceSource`. When neither applies, null is returned and callers
|
|
126
|
+
* must NOT fabricate a URL.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} [action] - expanded codebuddy action (may be undefined).
|
|
129
|
+
* @returns {string|null} the source URL, or null when unknown.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveCodeBuddyMarketplaceSource(action) {
|
|
132
|
+
if (action?.marketplace === undefined || action?.marketplace === null) {
|
|
133
|
+
return CODEBUDDY_MARKETPLACE_SOURCE;
|
|
134
|
+
}
|
|
135
|
+
return typeof action.marketplaceSource === 'string' && action.marketplaceSource.length > 0
|
|
136
|
+
? action.marketplaceSource
|
|
137
|
+
: null;
|
|
138
|
+
}
|
|
139
|
+
|
|
100
140
|
/** Authoritative codebuddy plugin manifest (single candidate, no precedence). */
|
|
101
141
|
export const CODEBUDDY_PLUGIN_MANIFEST_RELATIVE = join('.codebuddy-plugin', 'plugin.json');
|
|
102
142
|
|
|
@@ -185,7 +225,7 @@ export function codebuddyAuthorityDir(context, planDigest, plugin) {
|
|
|
185
225
|
/**
|
|
186
226
|
* 统一人工安装说明:面向 CodeBuddy / WorkBuddy 的人工结果流程。
|
|
187
227
|
*
|
|
188
|
-
* @param {{plugin:string, version:string, ref:string, attestationDir:string, requiresInstalledClosure:boolean}} p
|
|
228
|
+
* @param {{plugin:string, version:string, ref:string, attestationDir:string, requiresInstalledClosure:boolean, marketplace:string, marketplaceSource:string|null}} p
|
|
189
229
|
* @returns {string[]}
|
|
190
230
|
*/
|
|
191
231
|
function buildCodeBuddyManualInstructions({
|
|
@@ -194,11 +234,13 @@ function buildCodeBuddyManualInstructions({
|
|
|
194
234
|
ref,
|
|
195
235
|
attestationDir,
|
|
196
236
|
requiresInstalledClosure,
|
|
237
|
+
marketplace,
|
|
238
|
+
marketplaceSource,
|
|
197
239
|
}) {
|
|
198
240
|
return [
|
|
199
241
|
`CodeBuddy/WorkBuddy 插件安装无法锁定冻结 ref(codebuddy CLI marketplace add/install 没有 ref 选项,跟踪默认分支),因此安装是需要人工结果证明的手动步骤。`,
|
|
200
242
|
`1) publish 完成所有远端写入后进入 PUBLISHED 状态(自动化 Git 分支/标签、npm 和 GitHub Release 写入已完成)。此 codebuddy 检查点标记为需要人工安装。`,
|
|
201
|
-
`2) 从统一市场 "${
|
|
243
|
+
`2) 从统一市场 "${marketplace}"${marketplaceSource ? ` (${marketplaceSource})` : ''} 安装 ${plugin}。确认安装的插件版本等于冻结版本 ${version}。`,
|
|
202
244
|
`3) 将人工结果 JSON 写入: ${attestationDir}/${CODEBUDDY_ATTESTATION_FILE}`,
|
|
203
245
|
` 必填字段: platform="codebuddy", version, planDigest(冻结计划摘要), result("passed" 或 "failed"), actor(确认人), confirmedAt(ISO 8601 时间戳)${requiresInstalledClosure ? ',installChannel("desktop" 或 "cli"),installPath(实际安装后的插件目录)' : ''}`,
|
|
204
246
|
` 可选字段: note(备注)`,
|
|
@@ -312,15 +354,35 @@ export function validateCodeBuddyAttestation(attestation, action, isoNow, boundP
|
|
|
312
354
|
if (normalized.installPath.includes('..')) {
|
|
313
355
|
return { valid: false, error: 'codebuddy attestation installPath must not contain path traversal ("..")', normalized: null };
|
|
314
356
|
}
|
|
315
|
-
// CLI 通道:installPath
|
|
357
|
+
// CLI 通道:installPath 必须以预期后缀结尾(市场名解析自冻结动作,
|
|
358
|
+
// 未声明时回落到默认统一市场常量)
|
|
316
359
|
if (normalized.installChannel === 'cli') {
|
|
317
|
-
|
|
360
|
+
let resolvedMarketplace;
|
|
361
|
+
try {
|
|
362
|
+
resolvedMarketplace = resolveCodeBuddyMarketplace(action);
|
|
363
|
+
} catch (resolveErr) {
|
|
364
|
+
return { valid: false, error: resolveErr.message, normalized: null };
|
|
365
|
+
}
|
|
366
|
+
const expectedSuffix = `.workbuddy/plugins/marketplaces/${resolvedMarketplace}/plugins/${action.plugin}`;
|
|
318
367
|
if (!normalized.installPath.endsWith(expectedSuffix)) {
|
|
319
368
|
return { valid: false, error: `codebuddy CLI attestation installPath must end with "${expectedSuffix}", got "${normalized.installPath}"`, normalized: null };
|
|
320
369
|
}
|
|
321
370
|
}
|
|
322
371
|
}
|
|
323
372
|
|
|
373
|
+
// 可选 marketplace 字段:一旦声明必须等于冻结动作解析出的市场名
|
|
374
|
+
if (normalized.marketplace !== undefined) {
|
|
375
|
+
let resolvedMarketplace;
|
|
376
|
+
try {
|
|
377
|
+
resolvedMarketplace = resolveCodeBuddyMarketplace(action);
|
|
378
|
+
} catch (resolveErr) {
|
|
379
|
+
return { valid: false, error: resolveErr.message, normalized: null };
|
|
380
|
+
}
|
|
381
|
+
if (normalized.marketplace !== resolvedMarketplace) {
|
|
382
|
+
return { valid: false, error: `codebuddy attestation marketplace "${normalized.marketplace}" does not match the resolved marketplace "${resolvedMarketplace}"`, normalized: null };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
324
386
|
// result 只接受 passed 或 failed
|
|
325
387
|
if (normalized.result !== 'passed' && normalized.result !== 'failed') {
|
|
326
388
|
return { valid: false, error: `codebuddy attestation result "${normalized.result}" must be "passed" or "failed"`, normalized: null };
|
|
@@ -454,6 +516,22 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
454
516
|
|
|
455
517
|
const ref = action.ref ?? `v${action.version}`;
|
|
456
518
|
|
|
519
|
+
// Unified marketplace identity: resolved from the frozen action (declared
|
|
520
|
+
// `marketplace`/`marketplaceSource`), falling back to the default constants
|
|
521
|
+
// for legacy plans. A fabricated source URL is never emitted for a
|
|
522
|
+
// configured marketplace without a declared marketplaceSource.
|
|
523
|
+
let marketplace;
|
|
524
|
+
try {
|
|
525
|
+
marketplace = resolveCodeBuddyMarketplace(action);
|
|
526
|
+
} catch (marketplaceErr) {
|
|
527
|
+
return createResult({
|
|
528
|
+
actionType,
|
|
529
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
530
|
+
error: marketplaceErr.message,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const marketplaceSource = resolveCodeBuddyMarketplaceSource(action);
|
|
534
|
+
|
|
457
535
|
// (B) Stable plugin-level authority dir, shared across
|
|
458
536
|
// publish/reconcile/verify run dirs.
|
|
459
537
|
let attestationDir;
|
|
@@ -473,6 +551,8 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
473
551
|
ref,
|
|
474
552
|
attestationDir,
|
|
475
553
|
requiresInstalledClosure: Boolean(context.plan?.skillResourceClosure),
|
|
554
|
+
marketplace,
|
|
555
|
+
marketplaceSource,
|
|
476
556
|
});
|
|
477
557
|
|
|
478
558
|
// 统一 requirement 结构:不再包含隔离目录信息
|
|
@@ -484,6 +564,8 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
484
564
|
repo: action.repo,
|
|
485
565
|
ref,
|
|
486
566
|
entrySkill: action.entrySkill,
|
|
567
|
+
marketplace,
|
|
568
|
+
...(marketplaceSource ? { marketplaceSource } : {}),
|
|
487
569
|
planDigest,
|
|
488
570
|
attestationDir,
|
|
489
571
|
attestationFile: CODEBUDDY_ATTESTATION_FILE,
|
|
@@ -497,7 +579,7 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
497
579
|
...(context.plan?.skillResourceClosure
|
|
498
580
|
? {
|
|
499
581
|
installChannel: '<"desktop" or "cli">',
|
|
500
|
-
installPath: `<actual .workbuddy/plugins/marketplaces/${
|
|
582
|
+
installPath: `<actual .workbuddy/plugins/marketplaces/${marketplace}/plugins/${action.plugin} directory>`,
|
|
501
583
|
}
|
|
502
584
|
: {}),
|
|
503
585
|
note: '<optional note>',
|
|
@@ -359,9 +359,10 @@ const CODEBUDDY = Object.freeze({
|
|
|
359
359
|
marketplace: '.claude-plugin/marketplace.json',
|
|
360
360
|
}),
|
|
361
361
|
marketplaceSourceForm: null,
|
|
362
|
-
// codebuddy installs from a unified marketplace
|
|
363
|
-
//
|
|
364
|
-
// ./codebuddy.mjs); the
|
|
362
|
+
// codebuddy installs from a unified marketplace whose identity is optional
|
|
363
|
+
// on the action: a distribution may declare `marketplace` to override the
|
|
364
|
+
// default constant in ./codebuddy.mjs (resolveCodeBuddyMarketplace); the
|
|
365
|
+
// entry-version binding does not apply either way (null = N/A).
|
|
365
366
|
marketplaceEntryCarriesVersion: null,
|
|
366
367
|
// codebuddy has no marketplace add that can pin a frozen ref (attestation
|
|
367
368
|
// loop instead), so the marketplace ref form does not apply (null = N/A).
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
import { readdir, mkdir, lstat, realpath, open, rm } from 'node:fs/promises';
|
|
22
22
|
import { constants as fsConstants } from 'node:fs';
|
|
23
23
|
import { join, dirname, relative, isAbsolute, sep, resolve, posix as pathPosix } from 'node:path';
|
|
24
|
-
import { createHash } from 'node:crypto';
|
|
25
24
|
|
|
26
25
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
26
|
+
import { digestDocument } from 'skill-family-contracts';
|
|
27
|
+
import { digestBytes } from 'skill-family-harness-node';
|
|
27
28
|
import { PLATFORMS as PLATFORM_REGISTRY } from '../platforms/registry.mjs';
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -186,11 +187,14 @@ async function ensureTrustedOutputDirectory(rootPath, rootIdentity, relativeDir,
|
|
|
186
187
|
* @returns {string}
|
|
187
188
|
*/
|
|
188
189
|
export function computeBuildAdaptersDigest(sourceBytes) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
// 多段哈希(源字节 + node 版本 + 固定 locale/timezone)等价于对拼接字节
|
|
191
|
+
// 的 sha256,委托 Foundation digestBytes 实现,字节逐位一致。
|
|
192
|
+
const bytes = Buffer.concat([
|
|
193
|
+
...sourceBytes,
|
|
194
|
+
Buffer.from(`node:${process.version}`),
|
|
195
|
+
Buffer.from('locale:en-US timezone:UTC'),
|
|
196
|
+
]);
|
|
197
|
+
return `sha256:${digestBytes(bytes)}`;
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
// Platform definitions, derived from the platform registry (the single source
|
|
@@ -488,8 +492,21 @@ async function generateAdapterFiles(platform, skills, templateJson, root, market
|
|
|
488
492
|
});
|
|
489
493
|
}
|
|
490
494
|
|
|
491
|
-
// Copy bin/ entry point and bundle (text files)
|
|
492
|
-
|
|
495
|
+
// Copy bin/ entry point and bundle (text files). The self-contained
|
|
496
|
+
// bundle ships runtime sidecars next to it (JSON resources read via
|
|
497
|
+
// new URL(<rel>, import.meta.url) by inlined Foundation modules, e.g.
|
|
498
|
+
// error-codes.json); enumerate the directory so every sidecar is
|
|
499
|
+
// replicated deterministically. The source CLI (release-skill-cli.mjs)
|
|
500
|
+
// is never shipped into adapters. The wrapper and the bundle are
|
|
501
|
+
// required entries: a missing one fails closed before any write.
|
|
502
|
+
const binFiles = (await readdir(join(root, 'bin')))
|
|
503
|
+
.filter((name) => name !== 'release-skill-cli.mjs')
|
|
504
|
+
.sort();
|
|
505
|
+
for (const required of ['release-skill.mjs', 'release-skill.bundle.mjs']) {
|
|
506
|
+
if (!binFiles.includes(required)) {
|
|
507
|
+
throw new Error(`SECURITY: required bundle entry is missing: bin/${required}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
493
510
|
for (const name of binFiles) {
|
|
494
511
|
const srcPath = join(root, 'bin', name);
|
|
495
512
|
const content = await readTrustedFile(srcPath, root, `bin/${name}`);
|
|
@@ -752,8 +769,9 @@ export async function produceBuildAdapters({ inputs, output, outputRoots, platfo
|
|
|
752
769
|
* @returns {string}
|
|
753
770
|
*/
|
|
754
771
|
export function digestEntryOutputs(entries) {
|
|
772
|
+
// 纯 JSON 投影(platform 条件展开 + 定字段),直接委托 Foundation digestDocument。
|
|
755
773
|
const canonical = entries.map(({ platform, path, type, mode, sha256, size }) => ({
|
|
756
774
|
...(platform ? { platform } : {}), path, type, mode, size, sha256,
|
|
757
775
|
}));
|
|
758
|
-
return `sha256:${
|
|
776
|
+
return `sha256:${digestDocument(canonical)}`;
|
|
759
777
|
}
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
import { readFile, writeFile, readdir, stat, mkdir } from 'node:fs/promises';
|
|
15
15
|
import { join, relative } from 'node:path';
|
|
16
|
-
import { createHash } from 'node:crypto';
|
|
17
16
|
|
|
18
17
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
18
|
+
import { digestDocument } from 'skill-family-contracts';
|
|
19
|
+
import { digestBytes } from 'skill-family-harness-node';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Compute an implementation digest from the source bytes of this module.
|
|
@@ -24,11 +25,14 @@ import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
|
24
25
|
* @returns {string}
|
|
25
26
|
*/
|
|
26
27
|
export function computeRenderDigest(sourceBytes) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
// 多段哈希(源字节 + node 版本 + 固定 locale/timezone)等价于对拼接字节
|
|
29
|
+
// 的 sha256,委托 Foundation digestBytes 实现,字节逐位一致。
|
|
30
|
+
const bytes = Buffer.concat([
|
|
31
|
+
...sourceBytes,
|
|
32
|
+
Buffer.from(`node:${process.version}`),
|
|
33
|
+
Buffer.from('locale:en-US timezone:UTC'),
|
|
34
|
+
]);
|
|
35
|
+
return `sha256:${digestBytes(bytes)}`;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -145,8 +149,9 @@ export async function produceRenderPublicAssets({ inputs, output } = {}) {
|
|
|
145
149
|
* @returns {string}
|
|
146
150
|
*/
|
|
147
151
|
export function digestEntryOutputs(entries) {
|
|
152
|
+
// 纯 JSON 投影(定字段),直接委托 Foundation digestDocument。
|
|
148
153
|
const canonical = entries.map(({ path, type, mode, sha256, size }) => ({
|
|
149
154
|
path, type, mode, size, sha256,
|
|
150
155
|
}));
|
|
151
|
-
return `sha256:${
|
|
156
|
+
return `sha256:${digestDocument(canonical)}`;
|
|
152
157
|
}
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
-
import { createHash } from 'node:crypto';
|
|
15
14
|
|
|
16
15
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
16
|
+
import { digestDocument } from 'skill-family-contracts';
|
|
17
|
+
import { digestBytes } from 'skill-family-harness-node';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Compute an implementation digest from the source bytes of this module.
|
|
@@ -22,11 +23,14 @@ import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
|
22
23
|
* @returns {string}
|
|
23
24
|
*/
|
|
24
25
|
export function computeSyncSkillsDigest(sourceBytes) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// 多段哈希(源字节 + node 版本 + 固定 locale/timezone)等价于对拼接字节
|
|
27
|
+
// 的 sha256,委托 Foundation digestBytes 实现,字节逐位一致。
|
|
28
|
+
const bytes = Buffer.concat([
|
|
29
|
+
...sourceBytes,
|
|
30
|
+
Buffer.from(`node:${process.version}`),
|
|
31
|
+
Buffer.from('locale:en-US timezone:UTC'),
|
|
32
|
+
]);
|
|
33
|
+
return `sha256:${digestBytes(bytes)}`;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/**
|
|
@@ -89,8 +93,9 @@ export async function produceSyncSkills({ inputs, output } = {}) {
|
|
|
89
93
|
* @returns {string}
|
|
90
94
|
*/
|
|
91
95
|
export function digestEntryOutputs(entries) {
|
|
96
|
+
// 纯 JSON 投影(定字段),直接委托 Foundation digestDocument。
|
|
92
97
|
const canonical = entries.map(({ path, type, mode, sha256, size }) => ({
|
|
93
98
|
path, type, mode, size, sha256,
|
|
94
99
|
}));
|
|
95
|
-
return `sha256:${
|
|
100
|
+
return `sha256:${digestDocument(canonical)}`;
|
|
96
101
|
}
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
import { lstat, readFile, mkdir, readdir, realpath, chmod as fsChmod } from 'node:fs/promises';
|
|
25
25
|
import { open as fsOpen } from 'node:fs/promises';
|
|
26
26
|
import { relative, resolve, dirname, sep as pathSep, isAbsolute } from 'node:path';
|
|
27
|
-
import { posix } from 'node:path';
|
|
27
|
+
import { posix, win32 } from 'node:path';
|
|
28
28
|
import { constants as fsConstants } from 'node:fs';
|
|
29
29
|
import { createHash } from 'node:crypto';
|
|
30
|
+
import { classifyPathInput } from 'skill-family-harness-node';
|
|
30
31
|
import {
|
|
31
32
|
ReleaseError,
|
|
32
33
|
PUBLIC_FILE_MISSING,
|
|
@@ -81,7 +82,14 @@ const MAX_PATH_LENGTH = 4096;
|
|
|
81
82
|
*/
|
|
82
83
|
export function _isContainedWith(relativeFn, isAbsoluteFn, root, candidate, relPathSep = pathSep) {
|
|
83
84
|
const rel = relativeFn(root, candidate);
|
|
84
|
-
|
|
85
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${relPathSep}`) || isAbsoluteFn(rel)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// 词法分类委托 Foundation classifyPathInput(同步纯函数):对相对路径做
|
|
89
|
+
// 跨平台歧义输入 fail-closed(反斜杠分隔符/NUL/UNC/盘符相对/绝对路径)。
|
|
90
|
+
// 平台从注入的 relativeFn 推导,保持纯同步、无文件系统访问的既有契约。
|
|
91
|
+
const platform = relativeFn === win32.relative ? 'win32' : 'posix';
|
|
92
|
+
return classifyPathInput(rel, platform).ok === true;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
/**
|