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
package/src/commands/verify.mjs
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* @module commands/verify
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { readFile, writeFile, mkdtemp, rm, mkdir, lstat, realpath } from 'node:fs/promises';
|
|
16
|
-
import {
|
|
15
|
+
import { readFile, writeFile, mkdtemp, rm, mkdir, lstat, realpath, readdir } from 'node:fs/promises';
|
|
16
|
+
import { realpathSync } from 'node:fs';
|
|
17
|
+
import { dirname, join, relative, isAbsolute, resolve, basename } from 'node:path';
|
|
17
18
|
import { tmpdir } from 'node:os';
|
|
18
19
|
import { execFile as execFileCb } from 'node:child_process';
|
|
19
20
|
import { promisify } from 'node:util';
|
|
@@ -50,11 +51,34 @@ import {
|
|
|
50
51
|
resolveNpmRegistryAuthToken,
|
|
51
52
|
} from '../adapters/npm.mjs';
|
|
52
53
|
import { runConsumerVerificationGates } from '../core/verification-gates.mjs';
|
|
54
|
+
import {
|
|
55
|
+
checkSkillResourceClosure,
|
|
56
|
+
createSkillResourceClosureReceipt,
|
|
57
|
+
evaluateConsumerSkillResourceClosureReceipts,
|
|
58
|
+
} from '../core/skill-resource-closure.mjs';
|
|
59
|
+
import { isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
|
|
60
|
+
import {
|
|
61
|
+
shouldSkipVerification,
|
|
62
|
+
INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
63
|
+
} from '../core/installation-contract.mjs';
|
|
53
64
|
|
|
54
65
|
// ---------------------------------------------------------------------------
|
|
55
66
|
// Constants
|
|
56
67
|
// ---------------------------------------------------------------------------
|
|
57
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 验证已解决结果类型。
|
|
71
|
+
*
|
|
72
|
+
* - PASSED_AUTOMATIC: 自动验证通过(adapter.verify 返回 VERIFIED)
|
|
73
|
+
* - PASSED_MANUAL: 人工验证通过(用户手动确认)
|
|
74
|
+
* - NOT_REQUIRED_UNCHANGED: 无需验证(远端状态未变化,跳过验证)
|
|
75
|
+
*/
|
|
76
|
+
export const VERIFICATION_RESOLVED_TYPES = Object.freeze({
|
|
77
|
+
PASSED_AUTOMATIC: 'PASSED_AUTOMATIC',
|
|
78
|
+
PASSED_MANUAL: 'PASSED_MANUAL',
|
|
79
|
+
NOT_REQUIRED_UNCHANGED: 'NOT_REQUIRED_UNCHANGED',
|
|
80
|
+
});
|
|
81
|
+
|
|
58
82
|
/**
|
|
59
83
|
* Map plan action type to adapter ActionType.
|
|
60
84
|
* Must match publish.mjs and reconcile.mjs.
|
|
@@ -80,6 +104,16 @@ function defaultClock() {
|
|
|
80
104
|
return new Date().toISOString();
|
|
81
105
|
}
|
|
82
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 验证摘要格式是否为合法的 64 位十六进制字符串。
|
|
109
|
+
*
|
|
110
|
+
* @param {string} digest - 摘要
|
|
111
|
+
* @returns {boolean} 是否合法
|
|
112
|
+
*/
|
|
113
|
+
function isValidDigest(digest) {
|
|
114
|
+
return typeof digest === 'string' && /^[a-f0-9]{64}$/.test(digest);
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
// ---------------------------------------------------------------------------
|
|
84
118
|
// Smoke test
|
|
85
119
|
// ---------------------------------------------------------------------------
|
|
@@ -187,11 +221,13 @@ export async function runSmokeTest(plan, root, options = {}) {
|
|
|
187
221
|
skipped: true,
|
|
188
222
|
details: { message: 'No npm distributions in plan; smoke test skipped' },
|
|
189
223
|
gateResults: [],
|
|
224
|
+
skillResourceClosureReceipts: [],
|
|
190
225
|
};
|
|
191
226
|
}
|
|
192
227
|
|
|
193
228
|
const results = [];
|
|
194
229
|
const gateResults = [];
|
|
230
|
+
const skillResourceClosureReceipts = [];
|
|
195
231
|
|
|
196
232
|
for (const { package: pkgName, registry, targetVersion, unitId, smokeBin, smokeArgs, smokeExpectedJson } of npmDistributions) {
|
|
197
233
|
const packageAtVersion = `${pkgName}@${targetVersion}`;
|
|
@@ -256,6 +292,47 @@ export async function runSmokeTest(plan, root, options = {}) {
|
|
|
256
292
|
}
|
|
257
293
|
|
|
258
294
|
const pkgRoot = join(installDir, 'node_modules', pkgName);
|
|
295
|
+
if (plan.skillResourceClosure) {
|
|
296
|
+
const expectedUnitReceipt = plan.skillResourceClosure.unitReceipts
|
|
297
|
+
.find((item) => item.unitId === unitId);
|
|
298
|
+
if (!expectedUnitReceipt) {
|
|
299
|
+
return {
|
|
300
|
+
passed: false,
|
|
301
|
+
details: { error: `Skill resource closure receipt missing for npm unit "${unitId}"` },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const closureResult = await checkSkillResourceClosure({
|
|
305
|
+
snapshotDir: pkgRoot,
|
|
306
|
+
host: 'npm',
|
|
307
|
+
});
|
|
308
|
+
if (closureResult.findings.length > 0) {
|
|
309
|
+
return {
|
|
310
|
+
passed: false,
|
|
311
|
+
details: {
|
|
312
|
+
error: `Skill resource closure failed for ${packageAtVersion}`,
|
|
313
|
+
findings: closureResult.findings,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (expectedUnitReceipt.skillCount > 0 && closureResult.skillCount === 0) {
|
|
318
|
+
return {
|
|
319
|
+
passed: false,
|
|
320
|
+
details: {
|
|
321
|
+
error: `Installed npm package ${packageAtVersion} contains no skills`,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
skillResourceClosureReceipts.push(createSkillResourceClosureReceipt(
|
|
326
|
+
closureResult,
|
|
327
|
+
{
|
|
328
|
+
unitId,
|
|
329
|
+
distribution: 'npm',
|
|
330
|
+
host: 'npm',
|
|
331
|
+
checkedAt: new Date().toISOString(),
|
|
332
|
+
exitCode: 0,
|
|
333
|
+
},
|
|
334
|
+
));
|
|
335
|
+
}
|
|
259
336
|
gateResults.push(...await runConsumerVerificationGates({
|
|
260
337
|
plan,
|
|
261
338
|
unitId,
|
|
@@ -442,6 +519,7 @@ export async function runSmokeTest(plan, root, options = {}) {
|
|
|
442
519
|
count: results.length,
|
|
443
520
|
},
|
|
444
521
|
gateResults,
|
|
522
|
+
skillResourceClosureReceipts,
|
|
445
523
|
};
|
|
446
524
|
} finally {
|
|
447
525
|
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
@@ -519,6 +597,7 @@ const defaultNpmExecutor = {
|
|
|
519
597
|
* @param {string} [options.root] - Project root for source access.
|
|
520
598
|
* @param {string} [options.runDir] - Evidence directory.
|
|
521
599
|
* @param {() => string} [options.clock] - Clock function returning ISO-8601 strings.
|
|
600
|
+
* @param {Object} [options.previousVerifyRun] - 上一次验证成功的 verify run 记录,用于安装契约摘要免验。
|
|
522
601
|
*
|
|
523
602
|
* @returns {Promise<{ planPath: string, status: string, adapterChecks: Object[], smokeTest: Object }>}
|
|
524
603
|
*
|
|
@@ -536,6 +615,7 @@ export async function verifyRelease(options) {
|
|
|
536
615
|
npmExecutor,
|
|
537
616
|
verificationGatesAuthorized,
|
|
538
617
|
gateEnv,
|
|
618
|
+
previousVerifyRun,
|
|
539
619
|
} = options ?? {};
|
|
540
620
|
|
|
541
621
|
const clockFn = typeof clockOpt === 'function' ? clockOpt : defaultClock;
|
|
@@ -697,9 +777,13 @@ export async function verifyRelease(options) {
|
|
|
697
777
|
// Validate checkpoint mapping
|
|
698
778
|
validateRunCheckpointMapping(sourceRun, plan.externalActions ?? []);
|
|
699
779
|
|
|
700
|
-
// All checkpoints must be succeeded or skipped (no failed/pending)
|
|
780
|
+
// All checkpoints must be succeeded or skipped (no failed/pending),
|
|
781
|
+
// except marketplace install checkpoints which are re-verified by verify
|
|
782
|
+
// itself via consumer verification (human attestation or automatic).
|
|
783
|
+
// Deferred marketplace checkpoints from publish are also allowed through.
|
|
701
784
|
const incompleteCheckpoints = sourceRun.checkpoints.filter(
|
|
702
|
-
(cp) => cp.status !== 'succeeded' && cp.status !== 'skipped'
|
|
785
|
+
(cp) => cp.status !== 'succeeded' && cp.status !== 'skipped'
|
|
786
|
+
&& !((cp.status === 'failed' || cp.status === 'deferred') && isMarketplaceAction(cp.actionType)),
|
|
703
787
|
);
|
|
704
788
|
if (incompleteCheckpoints.length > 0) {
|
|
705
789
|
throw new ReleaseError(
|
|
@@ -731,13 +815,130 @@ export async function verifyRelease(options) {
|
|
|
731
815
|
|
|
732
816
|
const adapterChecks = [];
|
|
733
817
|
const consumerGateResults = [];
|
|
818
|
+
const consumerVerificationReceipts = [];
|
|
734
819
|
const actions = plan.externalActions ?? [];
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
820
|
+
|
|
821
|
+
// --- 自动发现可信消费端验证收据 ---
|
|
822
|
+
// 收据选择逻辑(per-action 从所有候选中选最新匹配):
|
|
823
|
+
// - 只读取同一权威 .release-skill/runs 的真实直接子目录
|
|
824
|
+
// - runs 根或候选目录不得是符号链接,不得物理越界
|
|
825
|
+
// - 候选必须经 loadRun(..., { requireDigest: true })、状态 VERIFIED、合法 finishedAt
|
|
826
|
+
// - 对当前 action 收集所有可信匹配收据,按 finishedAt 最新选择
|
|
827
|
+
// - 收据必须是 consumerVerificationReceipts,严格匹配 actionId/unitId/platform
|
|
828
|
+
// - 收据 planDigest 等于候选 run 自身 planDigest
|
|
829
|
+
// - 显式注入的 previousVerifyRun 也必须经过同等语义校验
|
|
830
|
+
|
|
831
|
+
/** @type {Array<Object>} 所有可信的验证 run 候选 */
|
|
832
|
+
const trustedVerifyRuns = [];
|
|
833
|
+
|
|
834
|
+
// 计算权威 runs 目录路径(基于 plan 的物理位置)
|
|
835
|
+
const planDir = dirname(planPath);
|
|
836
|
+
const releaseDir = basename(planDir) === 'plans' ? dirname(planDir) : planDir;
|
|
837
|
+
const runsDir = resolve(releaseDir, 'runs');
|
|
838
|
+
let runsDirReal = null;
|
|
839
|
+
let authorityDirReal = null;
|
|
840
|
+
|
|
841
|
+
// 校验并纳入显式注入的 previousVerifyRun
|
|
842
|
+
// 必须经过完整语义校验:status=VERIFIED、合法 runDigest、合法 finishedAt、
|
|
843
|
+
// 合法 planDigest、收据身份绑定。不满足则不注入(不参与复用),但不阻断。
|
|
844
|
+
if (previousVerifyRun) {
|
|
845
|
+
if (
|
|
846
|
+
previousVerifyRun.status === 'VERIFIED'
|
|
847
|
+
&& isValidDigest(previousVerifyRun.planDigest)
|
|
848
|
+
&& previousVerifyRun.finishedAt
|
|
849
|
+
&& typeof previousVerifyRun.finishedAt === 'string'
|
|
850
|
+
&& !isNaN(Date.parse(previousVerifyRun.finishedAt))
|
|
851
|
+
) {
|
|
852
|
+
const computedRunDigest = computeRunDigest(previousVerifyRun);
|
|
853
|
+
if (
|
|
854
|
+
typeof previousVerifyRun.runDigest === 'string'
|
|
855
|
+
&& previousVerifyRun.runDigest.length > 0
|
|
856
|
+
&& previousVerifyRun.runDigest === computedRunDigest
|
|
857
|
+
) {
|
|
858
|
+
trustedVerifyRuns.push(previousVerifyRun);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// 自动发现:从同一 .release-skill/runs 权威目录中发现
|
|
864
|
+
{
|
|
865
|
+
try {
|
|
866
|
+
const runsDirStat = await lstat(runsDir);
|
|
867
|
+
if (runsDirStat.isSymbolicLink()) {
|
|
868
|
+
// runs 根是符号链接:权威目录身份错误,失败关闭
|
|
869
|
+
throw new ReleaseError(
|
|
870
|
+
GATE_FAILED,
|
|
871
|
+
'runs directory is a symbolic link; authority identity compromised',
|
|
872
|
+
{ runsDir },
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (!runsDirStat.isDirectory()) {
|
|
876
|
+
throw new ReleaseError(
|
|
877
|
+
GATE_FAILED,
|
|
878
|
+
'runs path is not a directory',
|
|
879
|
+
{ runsDir },
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
// 以 plan 的物理权威目录为基准,验证 runs 是其中真实 runs 子目录
|
|
883
|
+
runsDirReal = realpathSync(runsDir);
|
|
884
|
+
authorityDirReal = realpathSync(releaseDir);
|
|
885
|
+
if (!runsDirReal.startsWith(authorityDirReal + '/') && runsDirReal !== authorityDirReal) {
|
|
886
|
+
throw new ReleaseError(
|
|
887
|
+
GATE_FAILED,
|
|
888
|
+
'runs directory is not a real child of the plan authority directory',
|
|
889
|
+
{ runsDir, runsDirReal, authorityDirReal },
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
} catch (err) {
|
|
893
|
+
if (err instanceof ReleaseError) throw err;
|
|
894
|
+
// runs 目录不存在等非致命情况:跳过自动发现
|
|
895
|
+
runsDirReal = null;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (runsDirReal) {
|
|
899
|
+
const entries = await readdir(runsDirReal, { withFileTypes: true });
|
|
900
|
+
for (const entry of entries) {
|
|
901
|
+
if (!entry.name.startsWith('verify-')) continue;
|
|
902
|
+
// 非目录或符号链接:失败关闭
|
|
903
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
904
|
+
throw new ReleaseError(
|
|
905
|
+
GATE_FAILED,
|
|
906
|
+
'verify-* candidate is a symbolic link or not a directory; authority identity compromised',
|
|
907
|
+
{ entry: entry.name, runsDir: runsDirReal },
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
const candidateDir = resolve(runsDirReal, entry.name);
|
|
911
|
+
const candidateStat = await lstat(candidateDir).catch(() => null);
|
|
912
|
+
if (!candidateStat || candidateStat.isSymbolicLink()) {
|
|
913
|
+
throw new ReleaseError(
|
|
914
|
+
GATE_FAILED,
|
|
915
|
+
'verify-* candidate is a symbolic link; authority identity compromised',
|
|
916
|
+
{ candidateDir, runsDir: runsDirReal },
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
// realpath 包含性校验:候选必须在权威 runs 目录内
|
|
920
|
+
const candidateReal = realpathSync(candidateDir);
|
|
921
|
+
if (!candidateReal.startsWith(runsDirReal + '/') && candidateReal !== runsDirReal) {
|
|
922
|
+
throw new ReleaseError(
|
|
923
|
+
GATE_FAILED,
|
|
924
|
+
'verify-* candidate real path is not contained in authority runs directory',
|
|
925
|
+
{ candidateDir, candidateReal, runsDir: runsDirReal },
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const candidatePath = resolve(candidateDir, 'release-run.json');
|
|
929
|
+
try {
|
|
930
|
+
const candidate = await loadRun(candidatePath, { requireDigest: true });
|
|
931
|
+
if (candidate.status !== 'VERIFIED') continue;
|
|
932
|
+
if (!candidate.planDigest) continue;
|
|
933
|
+
if (!candidate.finishedAt || typeof candidate.finishedAt !== 'string') continue;
|
|
934
|
+
trustedVerifyRuns.push(candidate);
|
|
935
|
+
} catch {
|
|
936
|
+
// 真实直接目录里的坏/缺 release-run.json 可忽略
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
741
942
|
|
|
742
943
|
for (const action of actions) {
|
|
743
944
|
const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
|
|
@@ -765,7 +966,7 @@ export async function verifyRelease(options) {
|
|
|
765
966
|
);
|
|
766
967
|
}
|
|
767
968
|
|
|
768
|
-
if (
|
|
969
|
+
if (isMarketplaceAction(action.type)) {
|
|
769
970
|
// --- Marketplace: fresh consumer verification in verify's own runDir ---
|
|
770
971
|
// Context: isolatedConsumerWritesAuthorized allows writing to verify's
|
|
771
972
|
// runDir/consumers/ directory; externalWritesAuthorized stays false.
|
|
@@ -783,22 +984,108 @@ export async function verifyRelease(options) {
|
|
|
783
984
|
...action.parameters,
|
|
784
985
|
};
|
|
785
986
|
|
|
786
|
-
//
|
|
987
|
+
// --- 安装契约摘要免验检查 ---
|
|
988
|
+
// 从计划中读取冻结的安装契约摘要。
|
|
989
|
+
// 新计划在 prepare 阶段计算并冻结 installationContractDigest;
|
|
990
|
+
// 旧计划(无此字段)跳过免验检查,强制重新验证。
|
|
991
|
+
const unit = (plan.units ?? []).find((u) => u.id === action.unitId);
|
|
992
|
+
const typeToDist = {
|
|
993
|
+
'claude-marketplace-install': 'claude-plugin',
|
|
994
|
+
'codex-marketplace-install': 'codex-plugin',
|
|
995
|
+
'kimi-marketplace-install': 'kimi-plugin',
|
|
996
|
+
'codebuddy-marketplace-install': 'codebuddy-plugin',
|
|
997
|
+
};
|
|
998
|
+
const dist = unit?.distributions?.find((d) => d.type === typeToDist[action.type]);
|
|
999
|
+
|
|
1000
|
+
// Step 3a: Preflight(始终先执行 adapter preflight,完成静态身份、版本、
|
|
1001
|
+
// tag/ref/sha、来源和 payload 校验;只有 preflight 通过后才允许免验)
|
|
1002
|
+
// 必须明确要求 PREFLIGHT_PASSED,其他状态全部失败关闭。
|
|
787
1003
|
const preflightResult = await adapter.preflight(actionInput, marketplaceContext);
|
|
788
|
-
if (preflightResult.status
|
|
1004
|
+
if (preflightResult.status !== 'PREFLIGHT_PASSED') {
|
|
789
1005
|
adapterChecks.push({
|
|
790
1006
|
actionId: action.id,
|
|
791
1007
|
actionType: action.type,
|
|
792
1008
|
status: 'FAILED',
|
|
793
|
-
error: `preflight
|
|
1009
|
+
error: `preflight did not pass: status=${preflightResult.status}, error=${preflightResult.error}`,
|
|
794
1010
|
});
|
|
795
1011
|
throw new ReleaseError(
|
|
796
1012
|
POST_PUBLISH_VERIFY_FAILED,
|
|
797
|
-
`marketplace preflight
|
|
1013
|
+
`marketplace preflight did not pass for action "${action.id}": status=${preflightResult.status}, error=${preflightResult.error}`,
|
|
798
1014
|
{ actionId: action.id },
|
|
799
1015
|
);
|
|
800
1016
|
}
|
|
801
1017
|
|
|
1018
|
+
// Preflight 通过后,检查是否可以免验
|
|
1019
|
+
if (dist?.installationContractDigest) {
|
|
1020
|
+
const currentDigest = dist.installationContractDigest;
|
|
1021
|
+
const currentPlatform = action.parameters?.consumer ?? action.type.replace('-marketplace-install', '');
|
|
1022
|
+
|
|
1023
|
+
// 紧邻可信公开基线:只检查最近一个 VERIFIED run 的收据。
|
|
1024
|
+
// 不能从任意更老历史中捞出相同摘要来免验。
|
|
1025
|
+
// 若最近 run 无该 action/platform 的收据,则 REQUIRE_VERIFICATION。
|
|
1026
|
+
let previousActionCheck = null;
|
|
1027
|
+
if (trustedVerifyRuns.length > 0) {
|
|
1028
|
+
// 按 finishedAt 降序排序,取最近一个
|
|
1029
|
+
const sorted = [...trustedVerifyRuns].sort((a, b) => {
|
|
1030
|
+
const ta = Date.parse(a.finishedAt) || 0;
|
|
1031
|
+
const tb = Date.parse(b.finishedAt) || 0;
|
|
1032
|
+
return tb - ta;
|
|
1033
|
+
});
|
|
1034
|
+
const latest = sorted[0];
|
|
1035
|
+
const matchingReceipt = (latest.consumerVerificationReceipts ?? []).find(
|
|
1036
|
+
(r) => r.actionId === action.id
|
|
1037
|
+
&& r.unitId === action.unitId
|
|
1038
|
+
&& r.platform === currentPlatform
|
|
1039
|
+
&& r.planDigest === latest.planDigest,
|
|
1040
|
+
);
|
|
1041
|
+
if (matchingReceipt) {
|
|
1042
|
+
previousActionCheck = { ...matchingReceipt, _runFinishedAtTime: Date.parse(latest.finishedAt) };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const previousDigest = previousActionCheck?.installationContractDigest ?? null;
|
|
1046
|
+
|
|
1047
|
+
const skipDecision = shouldSkipVerification({
|
|
1048
|
+
currentDigest,
|
|
1049
|
+
previousDigest,
|
|
1050
|
+
previousReceipt: previousActionCheck,
|
|
1051
|
+
algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// A current-run resource-closure receipt requires a fresh isolated
|
|
1055
|
+
// install. Installation-contract reuse alone cannot prove the
|
|
1056
|
+
// installed Skill resources are reachable.
|
|
1057
|
+
if (skipDecision === 'NOT_REQUIRED_UNCHANGED' && !plan.skillResourceClosure) {
|
|
1058
|
+
adapterChecks.push({
|
|
1059
|
+
actionId: action.id,
|
|
1060
|
+
actionType: action.type,
|
|
1061
|
+
status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
|
|
1062
|
+
installationContractDigest: currentDigest,
|
|
1063
|
+
reason: '安装契约摘要未变化,跳过验证',
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
consumerVerificationReceipts.push({
|
|
1067
|
+
actionId: action.id,
|
|
1068
|
+
unitId: action.unitId,
|
|
1069
|
+
platform: action.parameters?.consumer ?? action.type.replace('-marketplace-install', ''),
|
|
1070
|
+
result: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
|
|
1071
|
+
installationContractDigest: currentDigest,
|
|
1072
|
+
algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
1073
|
+
planDigest: plan.digest,
|
|
1074
|
+
verifiedAt: clockFn(),
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
await evidence.append({
|
|
1078
|
+
phase: 'verify-marketplace',
|
|
1079
|
+
actionId: action.id,
|
|
1080
|
+
actionType: action.type,
|
|
1081
|
+
status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
|
|
1082
|
+
installationContractDigest: currentDigest,
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
802
1089
|
// Step 3b: Execute (install to isolated consumer directory)
|
|
803
1090
|
const executeResult = await adapter.execute(actionInput, marketplaceContext);
|
|
804
1091
|
if (executeResult.status !== 'EXECUTED') {
|
|
@@ -821,15 +1108,39 @@ export async function verifyRelease(options) {
|
|
|
821
1108
|
marketplaceContext,
|
|
822
1109
|
);
|
|
823
1110
|
|
|
1111
|
+
// 正确分类:人工确认 -> PASSED_MANUAL,其他自动通路 -> PASSED_AUTOMATIC
|
|
1112
|
+
const isHumanConfirmed = verifyResult.observation?.humanConfirmed === true;
|
|
1113
|
+
const resolvedStatus = verifyResult.status === 'VERIFIED'
|
|
1114
|
+
? (isHumanConfirmed
|
|
1115
|
+
? VERIFICATION_RESOLVED_TYPES.PASSED_MANUAL
|
|
1116
|
+
: VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC)
|
|
1117
|
+
: 'FAILED';
|
|
1118
|
+
|
|
824
1119
|
const check = {
|
|
825
1120
|
actionId: action.id,
|
|
826
1121
|
actionType: action.type,
|
|
827
|
-
status:
|
|
1122
|
+
status: resolvedStatus,
|
|
828
1123
|
observation: verifyResult.observation,
|
|
829
1124
|
error: verifyResult.error,
|
|
1125
|
+
...(dist?.installationContractDigest ? { installationContractDigest: dist.installationContractDigest } : {}),
|
|
830
1126
|
};
|
|
831
1127
|
adapterChecks.push(check);
|
|
832
1128
|
|
|
1129
|
+
// 持久化消费端验证收据
|
|
1130
|
+
// 旧计划无摘要时可不生成"安装契约复用收据",但不得写非法空字符串
|
|
1131
|
+
if (resolvedStatus !== 'FAILED' && dist?.installationContractDigest && /^[a-f0-9]{64}$/.test(dist.installationContractDigest)) {
|
|
1132
|
+
consumerVerificationReceipts.push({
|
|
1133
|
+
actionId: action.id,
|
|
1134
|
+
unitId: action.unitId,
|
|
1135
|
+
platform: action.parameters?.consumer ?? action.type.replace('-marketplace-install', ''),
|
|
1136
|
+
result: resolvedStatus,
|
|
1137
|
+
installationContractDigest: dist.installationContractDigest,
|
|
1138
|
+
algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
1139
|
+
planDigest: plan.digest,
|
|
1140
|
+
verifiedAt: clockFn(),
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
833
1144
|
await evidence.append({
|
|
834
1145
|
phase: 'verify-marketplace',
|
|
835
1146
|
actionId: action.id,
|
|
@@ -841,7 +1152,13 @@ export async function verifyRelease(options) {
|
|
|
841
1152
|
throw new ReleaseError(
|
|
842
1153
|
POST_PUBLISH_VERIFY_FAILED,
|
|
843
1154
|
`marketplace verification failed for action "${action.id}": ${verifyResult.error}`,
|
|
844
|
-
{
|
|
1155
|
+
{
|
|
1156
|
+
actionId: action.id,
|
|
1157
|
+
actionType: action.type,
|
|
1158
|
+
verificationResult: check.status,
|
|
1159
|
+
observation: verifyResult.observation,
|
|
1160
|
+
expected: action.expected,
|
|
1161
|
+
},
|
|
845
1162
|
);
|
|
846
1163
|
}
|
|
847
1164
|
|
|
@@ -901,7 +1218,7 @@ export async function verifyRelease(options) {
|
|
|
901
1218
|
const check = {
|
|
902
1219
|
actionId: action.id,
|
|
903
1220
|
actionType: action.type,
|
|
904
|
-
status: verifyResult.status === 'VERIFIED' ?
|
|
1221
|
+
status: verifyResult.status === 'VERIFIED' ? VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC : 'FAILED',
|
|
905
1222
|
observation: verifyResult.observation,
|
|
906
1223
|
error: verifyResult.error,
|
|
907
1224
|
};
|
|
@@ -919,7 +1236,13 @@ export async function verifyRelease(options) {
|
|
|
919
1236
|
throw new ReleaseError(
|
|
920
1237
|
POST_PUBLISH_VERIFY_FAILED,
|
|
921
1238
|
`adapter verification failed for action "${action.id}": ${verifyResult.error}`,
|
|
922
|
-
{
|
|
1239
|
+
{
|
|
1240
|
+
actionId: action.id,
|
|
1241
|
+
actionType: action.type,
|
|
1242
|
+
verificationResult: check.status,
|
|
1243
|
+
observation: verifyResult.observation,
|
|
1244
|
+
expected: action.expected,
|
|
1245
|
+
},
|
|
923
1246
|
);
|
|
924
1247
|
}
|
|
925
1248
|
}
|
|
@@ -970,6 +1293,103 @@ export async function verifyRelease(options) {
|
|
|
970
1293
|
);
|
|
971
1294
|
}
|
|
972
1295
|
|
|
1296
|
+
// =======================================================================
|
|
1297
|
+
// Step 4b: Skill resource closure check on consumer install surfaces
|
|
1298
|
+
// Re-run the closure check on each declared consumer install surface.
|
|
1299
|
+
// If the plan declares skillResourceClosure, every declared host surface
|
|
1300
|
+
// must be checked and pass; undeclared or failed surfaces block VERIFIED.
|
|
1301
|
+
// =======================================================================
|
|
1302
|
+
const skillResourceClosureReceipts = [
|
|
1303
|
+
...(smokeTest.skillResourceClosureReceipts ?? []),
|
|
1304
|
+
];
|
|
1305
|
+
if (plan.skillResourceClosure) {
|
|
1306
|
+
await evidence.append({ phase: 'verify', step: 'skill-resource-closure', status: 'started' });
|
|
1307
|
+
|
|
1308
|
+
// Collect install paths from marketplace adapter checks
|
|
1309
|
+
const installSurfaces = [];
|
|
1310
|
+
const actionDistribution = {
|
|
1311
|
+
'claude-marketplace-install': 'claude-plugin',
|
|
1312
|
+
'codex-marketplace-install': 'codex-plugin',
|
|
1313
|
+
'kimi-marketplace-install': 'kimi-plugin',
|
|
1314
|
+
'codebuddy-marketplace-install': 'codebuddy-plugin',
|
|
1315
|
+
};
|
|
1316
|
+
for (const check of adapterChecks) {
|
|
1317
|
+
const distribution = actionDistribution[check.actionType];
|
|
1318
|
+
if (distribution && check.observation?.installPath) {
|
|
1319
|
+
installSurfaces.push({
|
|
1320
|
+
host: distribution.replace('-plugin', ''),
|
|
1321
|
+
distribution,
|
|
1322
|
+
installPath: check.observation.installPath,
|
|
1323
|
+
actionId: check.actionId,
|
|
1324
|
+
unitId: actions.find((a) => a.id === check.actionId)?.unitId,
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
for (const surface of installSurfaces) {
|
|
1330
|
+
const closureResult = await checkSkillResourceClosure({
|
|
1331
|
+
snapshotDir: surface.installPath,
|
|
1332
|
+
host: surface.host,
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
if (closureResult.findings.length > 0) {
|
|
1336
|
+
await evidence.append({
|
|
1337
|
+
phase: 'verify',
|
|
1338
|
+
step: 'skill-resource-closure',
|
|
1339
|
+
status: 'failed',
|
|
1340
|
+
host: surface.host,
|
|
1341
|
+
findingCount: closureResult.findings.length,
|
|
1342
|
+
findings: closureResult.findings,
|
|
1343
|
+
});
|
|
1344
|
+
throw new ReleaseError(
|
|
1345
|
+
POST_PUBLISH_VERIFY_FAILED,
|
|
1346
|
+
`skill resource closure check failed for ${surface.host} consumer install: ${closureResult.findings.length} finding(s)`,
|
|
1347
|
+
{ host: surface.host, findings: closureResult.findings },
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
const expectedUnitReceipt = plan.skillResourceClosure.unitReceipts
|
|
1351
|
+
.find((item) => item.unitId === surface.unitId);
|
|
1352
|
+
if (!expectedUnitReceipt || closureResult.skillCount === 0) {
|
|
1353
|
+
throw new ReleaseError(
|
|
1354
|
+
POST_PUBLISH_VERIFY_FAILED,
|
|
1355
|
+
`skill resource closure installed surface is empty or unbound for ${surface.distribution}`,
|
|
1356
|
+
{ unitId: surface.unitId, distribution: surface.distribution },
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
skillResourceClosureReceipts.push(createSkillResourceClosureReceipt(
|
|
1360
|
+
closureResult,
|
|
1361
|
+
{
|
|
1362
|
+
host: surface.host,
|
|
1363
|
+
distribution: surface.distribution,
|
|
1364
|
+
actionId: surface.actionId,
|
|
1365
|
+
unitId: surface.unitId,
|
|
1366
|
+
checkedAt: clockFn(),
|
|
1367
|
+
exitCode: 0,
|
|
1368
|
+
},
|
|
1369
|
+
));
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const receiptCoverage = evaluateConsumerSkillResourceClosureReceipts(
|
|
1373
|
+
plan,
|
|
1374
|
+
skillResourceClosureReceipts,
|
|
1375
|
+
);
|
|
1376
|
+
if (!receiptCoverage.passed) {
|
|
1377
|
+
throw new ReleaseError(
|
|
1378
|
+
POST_PUBLISH_VERIFY_FAILED,
|
|
1379
|
+
'skill resource closure receipt set does not match declared consumer distributions',
|
|
1380
|
+
receiptCoverage,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
await evidence.append({
|
|
1385
|
+
phase: 'verify',
|
|
1386
|
+
step: 'skill-resource-closure',
|
|
1387
|
+
status: 'completed',
|
|
1388
|
+
surfaceCount: installSurfaces.length,
|
|
1389
|
+
receipts: skillResourceClosureReceipts,
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
973
1393
|
// =======================================================================
|
|
974
1394
|
// All verifications passed — write verify run
|
|
975
1395
|
// =======================================================================
|
|
@@ -994,13 +1414,27 @@ export async function verifyRelease(options) {
|
|
|
994
1414
|
status: VERIFIED,
|
|
995
1415
|
checkpoints: actions.map((a) => {
|
|
996
1416
|
const check = adapterChecks.find((c) => c.actionId === a.id);
|
|
1417
|
+
let status;
|
|
1418
|
+
if (check?.status === 'SKIPPED') {
|
|
1419
|
+
status = 'skipped';
|
|
1420
|
+
} else if (check?.status === VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC) {
|
|
1421
|
+
status = 'succeeded';
|
|
1422
|
+
} else if (check?.status === VERIFICATION_RESOLVED_TYPES.PASSED_MANUAL) {
|
|
1423
|
+
status = 'succeeded';
|
|
1424
|
+
} else if (check?.status === VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED) {
|
|
1425
|
+
status = 'skipped';
|
|
1426
|
+
} else {
|
|
1427
|
+
status = 'succeeded';
|
|
1428
|
+
}
|
|
997
1429
|
return {
|
|
998
1430
|
actionId: a.id,
|
|
999
1431
|
actionType: a.type,
|
|
1000
|
-
status
|
|
1432
|
+
status,
|
|
1001
1433
|
};
|
|
1002
1434
|
}),
|
|
1003
1435
|
gateResults: consumerGateResults,
|
|
1436
|
+
consumerVerificationReceipts,
|
|
1437
|
+
skillResourceClosureReceipts,
|
|
1004
1438
|
startedAt: clockFn(),
|
|
1005
1439
|
finishedAt: clockFn(),
|
|
1006
1440
|
};
|
package/src/core/baseline.mjs
CHANGED
|
@@ -172,12 +172,19 @@ async function computeWorkspaceDigest(root) {
|
|
|
172
172
|
|
|
173
173
|
// Ask Git for unambiguous NUL-delimited names, then request the patch for
|
|
174
174
|
// each exact argv path. This avoids parsing C-quoted `diff --git` headers.
|
|
175
|
+
//
|
|
176
|
+
// Per-file `git diff --binary` can exceed Node.js default ~1 MiB maxBuffer
|
|
177
|
+
// when generated bundles (e.g. 3.8 MiB) are modified. We use an explicit
|
|
178
|
+
// 64 MiB upper bound: large enough for any realistic single-file diff, small
|
|
179
|
+
// enough to fail closed before exhausting memory.
|
|
180
|
+
const DIFF_MAX_BUFFER = 64 * 1024 * 1024; // 64 MiB
|
|
181
|
+
const diffOpts = { ...opts, maxBuffer: DIFF_MAX_BUFFER };
|
|
175
182
|
const changedPaths = splitNul(changedOut).filter((p) => !isControlPlanePath(p));
|
|
176
183
|
for (const changedPath of changedPaths) {
|
|
177
184
|
const { stdout: patch } = await execFile(
|
|
178
185
|
'git',
|
|
179
186
|
['diff', '--no-ext-diff', '--no-textconv', '--binary', '--no-color', '--', changedPath],
|
|
180
|
-
|
|
187
|
+
diffOpts,
|
|
181
188
|
);
|
|
182
189
|
parts.push(`UNSTAGED:${changedPath}\0${patch}`);
|
|
183
190
|
}
|