release-skill 0.2.4 → 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 +24 -0
- package/INSTALL.md +4 -4
- package/INSTALL.zh-CN.md +4 -4
- package/README.md +16 -14
- package/README.zh-CN.md +16 -14
- 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 +1375 -607
- package/adapters/claude/schemas/release-plan.schema.json +91 -0
- package/adapters/claude/schemas/release-run.schema.json +85 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1375 -607
- package/adapters/codex/schemas/release-plan.schema.json +91 -0
- package/adapters/codex/schemas/release-run.schema.json +85 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1375 -607
- package/adapters/kimi/schemas/release-plan.schema.json +91 -0
- package/adapters/kimi/schemas/release-run.schema.json +85 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1375 -607
- package/adapters/workbuddy/schemas/release-plan.schema.json +91 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +85 -0
- package/bin/release-skill.bundle.mjs +1375 -607
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +91 -0
- package/schemas/release-run.schema.json +85 -0
- package/scripts/sync-public-files.mjs +20 -9
- package/src/adapters/plugin-marketplace.mjs +82 -3
- package/src/commands/prepare.mjs +74 -0
- package/src/commands/publish.mjs +62 -0
- package/src/commands/verify.mjs +151 -1
- package/src/core/plan.mjs +29 -0
- package/src/core/skill-resource-closure.mjs +425 -0
- package/src/platforms/codebuddy.mjs +16 -3
- package/src/platforms/kimi.mjs +36 -9
package/package.json
CHANGED
|
@@ -815,6 +815,97 @@
|
|
|
815
815
|
"minLength": 1,
|
|
816
816
|
"description": "Digest of the project configuration at plan freeze time"
|
|
817
817
|
},
|
|
818
|
+
"skillResourceClosure": {
|
|
819
|
+
"type": "object",
|
|
820
|
+
"description": "Skill resource closure gate receipts from prepare phase (per-unit bound)",
|
|
821
|
+
"additionalProperties": false,
|
|
822
|
+
"properties": {
|
|
823
|
+
"unitReceipts": {
|
|
824
|
+
"type": "array",
|
|
825
|
+
"minItems": 1,
|
|
826
|
+
"items": {
|
|
827
|
+
"type": "object",
|
|
828
|
+
"additionalProperties": false,
|
|
829
|
+
"properties": {
|
|
830
|
+
"unitId": {
|
|
831
|
+
"type": "string",
|
|
832
|
+
"minLength": 1
|
|
833
|
+
},
|
|
834
|
+
"checkerVersion": {
|
|
835
|
+
"type": "string",
|
|
836
|
+
"minLength": 1
|
|
837
|
+
},
|
|
838
|
+
"inputDigest": {
|
|
839
|
+
"type": "string",
|
|
840
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
841
|
+
},
|
|
842
|
+
"surfaceCount": {
|
|
843
|
+
"type": "integer",
|
|
844
|
+
"minimum": 0
|
|
845
|
+
},
|
|
846
|
+
"skillCount": {
|
|
847
|
+
"type": "integer",
|
|
848
|
+
"minimum": 0
|
|
849
|
+
},
|
|
850
|
+
"referenceCount": {
|
|
851
|
+
"type": "integer",
|
|
852
|
+
"minimum": 0
|
|
853
|
+
},
|
|
854
|
+
"sourceOnlyCount": {
|
|
855
|
+
"type": "integer",
|
|
856
|
+
"minimum": 0
|
|
857
|
+
},
|
|
858
|
+
"findingCount": {
|
|
859
|
+
"const": 0
|
|
860
|
+
},
|
|
861
|
+
"receiptDigest": {
|
|
862
|
+
"type": "string",
|
|
863
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
"required": [
|
|
867
|
+
"unitId",
|
|
868
|
+
"checkerVersion",
|
|
869
|
+
"inputDigest",
|
|
870
|
+
"surfaceCount",
|
|
871
|
+
"skillCount",
|
|
872
|
+
"referenceCount",
|
|
873
|
+
"sourceOnlyCount",
|
|
874
|
+
"findingCount",
|
|
875
|
+
"receiptDigest"
|
|
876
|
+
]
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
"checkerVersion": {
|
|
880
|
+
"type": "string",
|
|
881
|
+
"minLength": 1
|
|
882
|
+
},
|
|
883
|
+
"totalSkillCount": {
|
|
884
|
+
"type": "integer",
|
|
885
|
+
"minimum": 0
|
|
886
|
+
},
|
|
887
|
+
"totalReferenceCount": {
|
|
888
|
+
"type": "integer",
|
|
889
|
+
"minimum": 0
|
|
890
|
+
},
|
|
891
|
+
"totalSourceOnlyCount": {
|
|
892
|
+
"type": "integer",
|
|
893
|
+
"minimum": 0
|
|
894
|
+
},
|
|
895
|
+
"totalFindingCount": {
|
|
896
|
+
"type": "integer",
|
|
897
|
+
"minimum": 0
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
"required": [
|
|
901
|
+
"unitReceipts",
|
|
902
|
+
"checkerVersion",
|
|
903
|
+
"totalSkillCount",
|
|
904
|
+
"totalReferenceCount",
|
|
905
|
+
"totalSourceOnlyCount",
|
|
906
|
+
"totalFindingCount"
|
|
907
|
+
]
|
|
908
|
+
},
|
|
818
909
|
"verificationGates": {
|
|
819
910
|
"type": "array",
|
|
820
911
|
"items": {
|
|
@@ -366,6 +366,91 @@
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
+
},
|
|
370
|
+
"skillResourceClosureReceipts": {
|
|
371
|
+
"type": "array",
|
|
372
|
+
"description": "Per-surface skill resource closure verification receipts from verify phase",
|
|
373
|
+
"items": {
|
|
374
|
+
"type": "object",
|
|
375
|
+
"required": [
|
|
376
|
+
"host",
|
|
377
|
+
"distribution",
|
|
378
|
+
"unitId",
|
|
379
|
+
"checkerVersion",
|
|
380
|
+
"inputDigest",
|
|
381
|
+
"surfaceCount",
|
|
382
|
+
"skillCount",
|
|
383
|
+
"referenceCount",
|
|
384
|
+
"sourceOnlyCount",
|
|
385
|
+
"findingCount",
|
|
386
|
+
"receiptDigest",
|
|
387
|
+
"checkedAt",
|
|
388
|
+
"exitCode"
|
|
389
|
+
],
|
|
390
|
+
"additionalProperties": false,
|
|
391
|
+
"properties": {
|
|
392
|
+
"host": {
|
|
393
|
+
"type": "string",
|
|
394
|
+
"minLength": 1
|
|
395
|
+
},
|
|
396
|
+
"unitId": {
|
|
397
|
+
"type": "string",
|
|
398
|
+
"minLength": 1
|
|
399
|
+
},
|
|
400
|
+
"distribution": {
|
|
401
|
+
"type": "string",
|
|
402
|
+
"enum": [
|
|
403
|
+
"npm",
|
|
404
|
+
"claude-plugin",
|
|
405
|
+
"codex-plugin",
|
|
406
|
+
"kimi-plugin",
|
|
407
|
+
"codebuddy-plugin"
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
"actionId": {
|
|
411
|
+
"type": "string",
|
|
412
|
+
"minLength": 1
|
|
413
|
+
},
|
|
414
|
+
"checkerVersion": {
|
|
415
|
+
"type": "string",
|
|
416
|
+
"minLength": 1
|
|
417
|
+
},
|
|
418
|
+
"inputDigest": {
|
|
419
|
+
"type": "string",
|
|
420
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
421
|
+
},
|
|
422
|
+
"surfaceCount": {
|
|
423
|
+
"type": "integer",
|
|
424
|
+
"minimum": 0
|
|
425
|
+
},
|
|
426
|
+
"skillCount": {
|
|
427
|
+
"type": "integer",
|
|
428
|
+
"minimum": 0
|
|
429
|
+
},
|
|
430
|
+
"referenceCount": {
|
|
431
|
+
"type": "integer",
|
|
432
|
+
"minimum": 0
|
|
433
|
+
},
|
|
434
|
+
"sourceOnlyCount": {
|
|
435
|
+
"type": "integer",
|
|
436
|
+
"minimum": 0
|
|
437
|
+
},
|
|
438
|
+
"findingCount": {
|
|
439
|
+
"const": 0
|
|
440
|
+
},
|
|
441
|
+
"receiptDigest": {
|
|
442
|
+
"type": "string",
|
|
443
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
444
|
+
},
|
|
445
|
+
"checkedAt": {
|
|
446
|
+
"type": "string",
|
|
447
|
+
"format": "date-time"
|
|
448
|
+
},
|
|
449
|
+
"exitCode": {
|
|
450
|
+
"const": 0
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
369
454
|
}
|
|
370
455
|
},
|
|
371
456
|
"allOf": [
|
|
@@ -208,9 +208,9 @@ function buildRequiredPublicFilesYaml(entries, indent = 6) {
|
|
|
208
208
|
// Entry collection
|
|
209
209
|
// ---------------------------------------------------------------------------
|
|
210
210
|
|
|
211
|
-
async function collectEntries() {
|
|
211
|
+
async function collectEntries(packageRoot) {
|
|
212
212
|
// 1. Scan all files in the package directory
|
|
213
|
-
const allFiles = await scanFiles(
|
|
213
|
+
const allFiles = await scanFiles(packageRoot);
|
|
214
214
|
const allSet = new Set(allFiles);
|
|
215
215
|
|
|
216
216
|
// 2. Build the adapter exclusion set (all files under adapters/)
|
|
@@ -303,8 +303,8 @@ async function collectEntries() {
|
|
|
303
303
|
// Check / write
|
|
304
304
|
// ---------------------------------------------------------------------------
|
|
305
305
|
|
|
306
|
-
async function readProjectYaml() {
|
|
307
|
-
return await readFile(
|
|
306
|
+
async function readProjectYaml(projectPath) {
|
|
307
|
+
return await readFile(projectPath, 'utf8');
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
function extractExistingEntries(yamlText, key) {
|
|
@@ -369,18 +369,29 @@ function replaceArrayInYaml(yamlText, key, newContent) {
|
|
|
369
369
|
*
|
|
370
370
|
* @param {object} options
|
|
371
371
|
* @param {string} options.packageDir - Package directory.
|
|
372
|
+
* @param {string} [options.projectPath] - Project config path. Defaults to the
|
|
373
|
+
* real workspace config; tests may provide an isolated copy.
|
|
372
374
|
* @param {boolean} [options.check] - Read-only drift detection.
|
|
373
375
|
* @returns {Promise<{mode: string, clean?: boolean, diff?: object}>}
|
|
374
376
|
*/
|
|
375
|
-
export async function syncPublicFiles({
|
|
377
|
+
export async function syncPublicFiles({
|
|
378
|
+
packageDir,
|
|
379
|
+
projectPath = PROJECT_YAML,
|
|
380
|
+
check = false,
|
|
381
|
+
} = {}) {
|
|
376
382
|
if (typeof packageDir !== 'string' || packageDir.length === 0) {
|
|
377
383
|
const err = new Error('sync-public-files: packageDir is required');
|
|
378
384
|
err.code = 'SYNC_PUBLIC_FILES_INVALID_ARGUMENT';
|
|
379
385
|
throw err;
|
|
380
386
|
}
|
|
387
|
+
if (typeof projectPath !== 'string' || projectPath.length === 0) {
|
|
388
|
+
const err = new Error('sync-public-files: projectPath must be a non-empty string');
|
|
389
|
+
err.code = 'SYNC_PUBLIC_FILES_INVALID_ARGUMENT';
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
381
392
|
|
|
382
|
-
const entries = await collectEntries();
|
|
383
|
-
const yamlText = await readProjectYaml();
|
|
393
|
+
const entries = await collectEntries(packageDir);
|
|
394
|
+
const yamlText = await readProjectYaml(projectPath);
|
|
384
395
|
|
|
385
396
|
const existingPublic = extractExistingEntries(yamlText, 'publicFiles');
|
|
386
397
|
const existingRequired = extractExistingEntries(yamlText, 'requiredPublicFiles');
|
|
@@ -422,10 +433,10 @@ export async function syncPublicFiles({ packageDir, check = false } = {}) {
|
|
|
422
433
|
let newYaml = replaceArrayInYaml(yamlText, 'publicFiles', publicYaml);
|
|
423
434
|
newYaml = replaceArrayInYaml(newYaml, 'requiredPublicFiles', requiredYaml);
|
|
424
435
|
|
|
425
|
-
await writeFile(
|
|
436
|
+
await writeFile(projectPath, newYaml, 'utf8');
|
|
426
437
|
|
|
427
438
|
// Verify write
|
|
428
|
-
const reread = await readFile(
|
|
439
|
+
const reread = await readFile(projectPath, 'utf8');
|
|
429
440
|
if (reread !== newYaml) {
|
|
430
441
|
const err = new Error('sync-public-files: post-write verification failed');
|
|
431
442
|
err.code = 'SYNC_PUBLIC_FILES_WRITE_VERIFY_FAILED';
|
|
@@ -2882,6 +2882,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2882
2882
|
|
|
2883
2883
|
// 使用归一化后的 attestation(兼容旧格式)
|
|
2884
2884
|
const normalizedAttestation = attestationCheck.normalized;
|
|
2885
|
+
const requiresInstalledClosure = Boolean(context.plan?.skillResourceClosure);
|
|
2885
2886
|
|
|
2886
2887
|
// 统一人工判定:如果结果是 failed,直接返回失败
|
|
2887
2888
|
if (normalizedAttestation.result === 'failed') {
|
|
@@ -2901,7 +2902,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2901
2902
|
});
|
|
2902
2903
|
}
|
|
2903
2904
|
|
|
2905
|
+
if (requiresInstalledClosure && !normalizedAttestation.installPath) {
|
|
2906
|
+
return createResult({
|
|
2907
|
+
actionType,
|
|
2908
|
+
status: ActionStatus.OBSERVED,
|
|
2909
|
+
observation: {
|
|
2910
|
+
installed: false,
|
|
2911
|
+
error: 'kimi attestation installPath is required by the skill resource closure gate',
|
|
2912
|
+
},
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2904
2916
|
// installPath 验证:必须在 managed root 内,不得逃逸,不得是符号链接
|
|
2917
|
+
let verifiedInstallPath = null;
|
|
2905
2918
|
if (normalizedAttestation.installPath) {
|
|
2906
2919
|
const managedRoot = resolve(attestationDir, 'kimi-home', 'plugins', 'managed');
|
|
2907
2920
|
const installPathAbs = resolve(normalizedAttestation.installPath);
|
|
@@ -2967,18 +2980,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2967
2980
|
},
|
|
2968
2981
|
});
|
|
2969
2982
|
}
|
|
2983
|
+
verifiedInstallPath = installPathReal;
|
|
2970
2984
|
}
|
|
2971
2985
|
|
|
2972
2986
|
// 载荷绑定验证:旧格式含 installPath 时必须绑定冻结载荷并失败关闭。
|
|
2973
2987
|
// 身份、版本、载荷、市场来源不一致绝不能被人工 passed 覆盖。
|
|
2974
2988
|
let manifestDigest = action.manifestDigest;
|
|
2975
2989
|
let payloadBinding = null;
|
|
2976
|
-
if (
|
|
2990
|
+
if (verifiedInstallPath) {
|
|
2977
2991
|
try {
|
|
2978
2992
|
payloadBinding = await verifyInstalledMarketplacePayload(
|
|
2979
2993
|
action,
|
|
2980
2994
|
context,
|
|
2981
|
-
|
|
2995
|
+
verifiedInstallPath,
|
|
2982
2996
|
consumer,
|
|
2983
2997
|
);
|
|
2984
2998
|
manifestDigest = payloadBinding.manifestDigest;
|
|
@@ -3019,6 +3033,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3019
3033
|
entrySkillFound: true,
|
|
3020
3034
|
manifestDigest,
|
|
3021
3035
|
planDigest: boundPlanDigest,
|
|
3036
|
+
...(verifiedInstallPath ? { installPath: verifiedInstallPath } : {}),
|
|
3022
3037
|
...extraInstalledPathsAudit(payloadBinding),
|
|
3023
3038
|
},
|
|
3024
3039
|
});
|
|
@@ -3129,6 +3144,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3129
3144
|
|
|
3130
3145
|
// 使用归一化后的 attestation(兼容旧格式)
|
|
3131
3146
|
const normalizedAttestation = attestationCheck.normalized;
|
|
3147
|
+
const requiresInstalledClosure = Boolean(context.plan?.skillResourceClosure);
|
|
3132
3148
|
|
|
3133
3149
|
// 统一人工判定:如果结果是 failed,直接返回失败
|
|
3134
3150
|
if (normalizedAttestation.result === 'failed') {
|
|
@@ -3148,7 +3164,69 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3148
3164
|
});
|
|
3149
3165
|
}
|
|
3150
3166
|
|
|
3151
|
-
|
|
3167
|
+
let verifiedInstallPath = null;
|
|
3168
|
+
if (requiresInstalledClosure) {
|
|
3169
|
+
if (!normalizedAttestation.installPath) {
|
|
3170
|
+
return createResult({
|
|
3171
|
+
actionType,
|
|
3172
|
+
status: ActionStatus.OBSERVED,
|
|
3173
|
+
observation: {
|
|
3174
|
+
installed: false,
|
|
3175
|
+
error: 'codebuddy attestation installPath is required by the skill resource closure gate',
|
|
3176
|
+
},
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
if (!['desktop', 'cli'].includes(normalizedAttestation.installChannel)) {
|
|
3180
|
+
return createResult({
|
|
3181
|
+
actionType,
|
|
3182
|
+
status: ActionStatus.OBSERVED,
|
|
3183
|
+
observation: {
|
|
3184
|
+
installed: false,
|
|
3185
|
+
error: 'codebuddy attestation installChannel must be "desktop" or "cli" when the skill resource closure gate is active',
|
|
3186
|
+
},
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
const normalizedPath = normalizedAttestation.installPath.replaceAll('\\', '/');
|
|
3190
|
+
const expectedSuffix = `.workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
|
|
3191
|
+
if (!normalizedPath.endsWith(expectedSuffix)) {
|
|
3192
|
+
return createResult({
|
|
3193
|
+
actionType,
|
|
3194
|
+
status: ActionStatus.OBSERVED,
|
|
3195
|
+
observation: {
|
|
3196
|
+
installed: false,
|
|
3197
|
+
error: `codebuddy attestation installPath must end with "${expectedSuffix}"`,
|
|
3198
|
+
},
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
const installPathAbs = resolve(normalizedAttestation.installPath);
|
|
3202
|
+
let lexicalStat;
|
|
3203
|
+
try {
|
|
3204
|
+
lexicalStat = await lstat(installPathAbs);
|
|
3205
|
+
} catch {
|
|
3206
|
+
return createResult({
|
|
3207
|
+
actionType,
|
|
3208
|
+
status: ActionStatus.OBSERVED,
|
|
3209
|
+
observation: {
|
|
3210
|
+
installed: false,
|
|
3211
|
+
error: `codebuddy attestation installPath does not exist: ${normalizedAttestation.installPath}`,
|
|
3212
|
+
},
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
if (lexicalStat.isSymbolicLink() || !lexicalStat.isDirectory()) {
|
|
3216
|
+
return createResult({
|
|
3217
|
+
actionType,
|
|
3218
|
+
status: ActionStatus.OBSERVED,
|
|
3219
|
+
observation: {
|
|
3220
|
+
installed: false,
|
|
3221
|
+
error: 'codebuddy attestation installPath must be a real directory, not a symlink',
|
|
3222
|
+
},
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
verifiedInstallPath = await realpath(installPathAbs);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
// 人工结果仍裁决不可自动化的安装步骤;新资源闭包计划另外
|
|
3229
|
+
// 暴露已校验的真实安装目录,供 verify 的内置只读门禁扫描。
|
|
3152
3230
|
return createResult({
|
|
3153
3231
|
actionType,
|
|
3154
3232
|
status: ActionStatus.OBSERVED,
|
|
@@ -3167,6 +3245,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3167
3245
|
entrySkillFound: true,
|
|
3168
3246
|
manifestDigest: action.manifestDigest,
|
|
3169
3247
|
planDigest: boundPlanDigest,
|
|
3248
|
+
...(verifiedInstallPath ? { installPath: verifiedInstallPath } : {}),
|
|
3170
3249
|
},
|
|
3171
3250
|
});
|
|
3172
3251
|
}
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -33,6 +33,11 @@ import { runSnapshotVerificationGates } from '../core/verification-gates.mjs';
|
|
|
33
33
|
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
34
34
|
import { computePlanDigest, writePlanAtomic, writePlanImmutable } from '../core/plan.mjs';
|
|
35
35
|
import { sha256Hex } from '../core/digest.mjs';
|
|
36
|
+
import {
|
|
37
|
+
CHECKER_VERSION as SKILL_RESOURCE_CHECKER_VERSION,
|
|
38
|
+
checkSkillResourceClosure,
|
|
39
|
+
createSkillResourceClosureReceipt,
|
|
40
|
+
} from '../core/skill-resource-closure.mjs';
|
|
36
41
|
import { buildPublicStaging } from '../snapshot/public-map.mjs';
|
|
37
42
|
import { resolveUnitScopedPath } from '../snapshot/public-path.mjs';
|
|
38
43
|
import { scanSnapshot } from '../snapshot/scan.mjs';
|
|
@@ -2234,6 +2239,67 @@ export async function prepareRelease(options) {
|
|
|
2234
2239
|
)
|
|
2235
2240
|
: null;
|
|
2236
2241
|
|
|
2242
|
+
// --- Step 7b: Skill resource closure gate ---
|
|
2243
|
+
// Production snapshots are sealed inside buildProductionAssets. Scan only
|
|
2244
|
+
// after that transition so the receipt binds the exact byte/mode identity
|
|
2245
|
+
// publish will re-verify, rather than the writable pre-freeze staging tree.
|
|
2246
|
+
// Non-production plans scan the final staging tree at the same point.
|
|
2247
|
+
// This gate is built in, read-only, and cannot be disabled by overlays.
|
|
2248
|
+
const skillResourceClosureResults = [];
|
|
2249
|
+
for (const { unit, manifest } of unitResults) {
|
|
2250
|
+
await evidence.append({
|
|
2251
|
+
phase: 'skill-resource-closure',
|
|
2252
|
+
status: 'started',
|
|
2253
|
+
unitId: unit.id,
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
const closureResult = await checkSkillResourceClosure({
|
|
2257
|
+
snapshotDir: manifest.outputDir,
|
|
2258
|
+
host: 'root',
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
const receipt = createSkillResourceClosureReceipt(closureResult, { unitId: unit.id });
|
|
2262
|
+
skillResourceClosureResults.push(receipt);
|
|
2263
|
+
|
|
2264
|
+
if (closureResult.findings.length > 0) {
|
|
2265
|
+
await evidence.append({
|
|
2266
|
+
phase: 'skill-resource-closure',
|
|
2267
|
+
status: 'blocking',
|
|
2268
|
+
unitId: unit.id,
|
|
2269
|
+
findingCount: closureResult.findings.length,
|
|
2270
|
+
findings: closureResult.findings.map((f) => ({
|
|
2271
|
+
skill: f.skill,
|
|
2272
|
+
line: f.line,
|
|
2273
|
+
reference: f.reference,
|
|
2274
|
+
classification: f.classification,
|
|
2275
|
+
code: f.code,
|
|
2276
|
+
})),
|
|
2277
|
+
});
|
|
2278
|
+
throw new ReleaseError(
|
|
2279
|
+
GATE_FAILED,
|
|
2280
|
+
`skill resource closure gate failed for unit "${unit.id}": ${closureResult.findings.length} finding(s)`,
|
|
2281
|
+
{
|
|
2282
|
+
unitId: unit.id,
|
|
2283
|
+
findingCount: closureResult.findings.length,
|
|
2284
|
+
findings: closureResult.findings,
|
|
2285
|
+
},
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
await evidence.append({
|
|
2290
|
+
phase: 'skill-resource-closure',
|
|
2291
|
+
status: 'completed',
|
|
2292
|
+
unitId: unit.id,
|
|
2293
|
+
checkerVersion: closureResult.checkerVersion,
|
|
2294
|
+
surfaceCount: receipt.surfaceCount,
|
|
2295
|
+
skillCount: receipt.skillCount,
|
|
2296
|
+
referenceCount: closureResult.referenceCount,
|
|
2297
|
+
sourceOnlyCount: closureResult.sourceOnlyCount,
|
|
2298
|
+
findingCount: 0,
|
|
2299
|
+
receiptDigest: closureResult.receiptDigest,
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2237
2303
|
// Freeze external independent marketplace HEADs (production + online only):
|
|
2238
2304
|
// for each claude/codex/codebuddy distribution declaring marketplaceRepo,
|
|
2239
2305
|
// resolve the external repo's HEAD sha + default branch and validate the
|
|
@@ -2581,6 +2647,14 @@ export async function prepareRelease(options) {
|
|
|
2581
2647
|
capturedAt: baseline.capturedAt,
|
|
2582
2648
|
},
|
|
2583
2649
|
configDigest,
|
|
2650
|
+
skillResourceClosure: {
|
|
2651
|
+
checkerVersion: SKILL_RESOURCE_CHECKER_VERSION,
|
|
2652
|
+
unitReceipts: skillResourceClosureResults,
|
|
2653
|
+
totalSkillCount: skillResourceClosureResults.reduce((sum, item) => sum + item.skillCount, 0),
|
|
2654
|
+
totalReferenceCount: skillResourceClosureResults.reduce((sum, item) => sum + item.referenceCount, 0),
|
|
2655
|
+
totalSourceOnlyCount: skillResourceClosureResults.reduce((sum, item) => sum + item.sourceOnlyCount, 0),
|
|
2656
|
+
totalFindingCount: 0,
|
|
2657
|
+
},
|
|
2584
2658
|
verificationGates: config.verificationGates ?? [],
|
|
2585
2659
|
snapshotDigest: overallSnapshotDigest,
|
|
2586
2660
|
...(production ? {
|
package/src/commands/publish.mjs
CHANGED
|
@@ -41,6 +41,12 @@ import {
|
|
|
41
41
|
reObservePreviousPublicBaseline,
|
|
42
42
|
} from '../core/previous-public-baseline.mjs';
|
|
43
43
|
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
44
|
+
import {
|
|
45
|
+
CHECKER_VERSION as SKILL_RESOURCE_CHECKER_VERSION,
|
|
46
|
+
assertSkillResourceClosureReceipt,
|
|
47
|
+
checkSkillResourceClosure,
|
|
48
|
+
createSkillResourceClosureReceipt,
|
|
49
|
+
} from '../core/skill-resource-closure.mjs';
|
|
44
50
|
import {
|
|
45
51
|
ADAPTER_ACTION_TYPE_MAP,
|
|
46
52
|
TIER_TABLE,
|
|
@@ -521,6 +527,7 @@ export async function publishRelease(options) {
|
|
|
521
527
|
if (productionMode && !isProductionPlan) {
|
|
522
528
|
throw new ReleaseError(GATE_FAILED, 'production publish requires a github-npm-v1 frozen plan');
|
|
523
529
|
}
|
|
530
|
+
const verifiedFrozenSnapshots = new Map();
|
|
524
531
|
if (isProductionPlan) {
|
|
525
532
|
if (!plan.production.assetRoot || plan.production.assetRoot === '.') {
|
|
526
533
|
throw new ReleaseError(GATE_FAILED, 'production plan requires a dedicated assetRoot');
|
|
@@ -566,6 +573,7 @@ export async function publishRelease(options) {
|
|
|
566
573
|
snapshotPath: frozen.path,
|
|
567
574
|
expectedDigest: frozen.manifestDigest,
|
|
568
575
|
});
|
|
576
|
+
verifiedFrozenSnapshots.set(unit.id, snapshot.snapshotDir);
|
|
569
577
|
assertInsideAssetRoot(assetRoot, snapshot.snapshotDir, 'frozen snapshot');
|
|
570
578
|
const git = await verifyFrozenGitRepository({
|
|
571
579
|
root,
|
|
@@ -599,6 +607,60 @@ export async function publishRelease(options) {
|
|
|
599
607
|
await evidence.append({ phase: 'safety-gate', gate: 'frozen-artifacts', status: 'passed' });
|
|
600
608
|
}
|
|
601
609
|
|
|
610
|
+
// =======================================================================
|
|
611
|
+
// Safety Gate 2c: Skill resource closure recheck
|
|
612
|
+
// Re-run the skill resource closure check on each unit's frozen snapshot.
|
|
613
|
+
// Requires findingCount=0 and receipt matches the plan.
|
|
614
|
+
// =======================================================================
|
|
615
|
+
if (plan.skillResourceClosure) {
|
|
616
|
+
await evidence.append({ phase: 'safety-gate', gate: 'skill-resource-closure', status: 'started' });
|
|
617
|
+
const expectedReceipts = plan.skillResourceClosure.unitReceipts ?? [];
|
|
618
|
+
if (plan.skillResourceClosure.checkerVersion !== SKILL_RESOURCE_CHECKER_VERSION) {
|
|
619
|
+
throw new ReleaseError(
|
|
620
|
+
GATE_FAILED,
|
|
621
|
+
`skill resource closure checker version mismatch: plan=${plan.skillResourceClosure.checkerVersion}, current=${SKILL_RESOURCE_CHECKER_VERSION}`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (expectedReceipts.length !== plan.units.length) {
|
|
625
|
+
throw new ReleaseError(GATE_FAILED, 'skill resource closure receipt set does not cover every release unit');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
for (const [unitId, snapshotDir] of verifiedFrozenSnapshots) {
|
|
629
|
+
const expected = expectedReceipts.find((item) => item.unitId === unitId);
|
|
630
|
+
if (!expected) {
|
|
631
|
+
throw new ReleaseError(GATE_FAILED, `skill resource closure receipt missing for unit "${unitId}"`);
|
|
632
|
+
}
|
|
633
|
+
const closureResult = await checkSkillResourceClosure({
|
|
634
|
+
snapshotDir,
|
|
635
|
+
host: 'root',
|
|
636
|
+
});
|
|
637
|
+
if (closureResult.findings.length > 0) {
|
|
638
|
+
await evidence.append({
|
|
639
|
+
phase: 'safety-gate',
|
|
640
|
+
gate: 'skill-resource-closure',
|
|
641
|
+
status: 'failed',
|
|
642
|
+
unitId,
|
|
643
|
+
findingCount: closureResult.findings.length,
|
|
644
|
+
});
|
|
645
|
+
throw new ReleaseError(
|
|
646
|
+
GATE_FAILED,
|
|
647
|
+
`skill resource closure recheck failed for unit "${unitId}": ${closureResult.findings.length} finding(s)`,
|
|
648
|
+
{ unitId, findings: closureResult.findings },
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const observed = createSkillResourceClosureReceipt(closureResult, { unitId });
|
|
652
|
+
assertSkillResourceClosureReceipt(expected, observed, `unit "${unitId}"`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
await evidence.append({
|
|
656
|
+
phase: 'safety-gate',
|
|
657
|
+
gate: 'skill-resource-closure',
|
|
658
|
+
status: 'passed',
|
|
659
|
+
receiptCount: expectedReceipts.length,
|
|
660
|
+
recheckedReceiptCount: verifiedFrozenSnapshots.size,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
602
664
|
// =======================================================================
|
|
603
665
|
// Safety Gates 3-7: Load and validate approval record (shared)
|
|
604
666
|
// =======================================================================
|