release-skill 0.7.5 → 0.7.7
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 +37 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +22 -11
- package/README.zh-CN.md +17 -8
- package/SECURITY.md +12 -6
- 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 +1729 -1159
- package/adapters/claude/schemas/release-plan.schema.json +47 -0
- package/adapters/claude/schemas/release-project.schema.json +24 -0
- package/adapters/claude/skills/release-prepare/SKILL.md +5 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1729 -1159
- package/adapters/codex/schemas/release-plan.schema.json +47 -0
- package/adapters/codex/schemas/release-project.schema.json +24 -0
- package/adapters/codex/skills/release-assess/SKILL.md +1 -1
- package/adapters/codex/skills/release-config/SKILL.md +1 -1
- package/adapters/codex/skills/release-docs/SKILL.md +1 -1
- package/adapters/codex/skills/release-help/SKILL.md +1 -1
- package/adapters/codex/skills/release-marketplace/SKILL.md +1 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +6 -2
- package/adapters/codex/skills/release-publish/SKILL.md +1 -1
- package/adapters/codex/skills/release-reconcile/SKILL.md +1 -1
- package/adapters/codex/skills/release-setup/SKILL.md +1 -1
- package/adapters/codex/skills/release-verify/SKILL.md +1 -1
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1729 -1159
- package/adapters/kimi/schemas/release-plan.schema.json +47 -0
- package/adapters/kimi/schemas/release-project.schema.json +24 -0
- package/adapters/kimi/skills/release-prepare/SKILL.md +5 -1
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1729 -1159
- package/adapters/workbuddy/schemas/release-plan.schema.json +47 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +24 -0
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -1
- package/bin/release-skill-cli.mjs +116 -47
- package/bin/release-skill.bundle.mjs +1729 -1159
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +4 -4
- package/references/02-project-config.md +73 -29
- package/schemas/release-plan.schema.json +47 -0
- package/schemas/release-project.schema.json +24 -0
- package/skills/release-prepare/SKILL.md +5 -1
- package/skills-src/release-prepare/SKILL.md +5 -1
- package/src/adapters/git-github.mjs +139 -26
- package/src/commands/prepare.mjs +76 -6
- package/src/commands/publish.mjs +36 -0
- package/src/commands/verify.mjs +53 -1
- package/src/core/config.mjs +31 -0
- package/src/core/plan.mjs +78 -0
- package/src/core/release-assets.mjs +54 -0
- package/src/core/source-authority.mjs +127 -3
- package/src/producers/build-adapters.mjs +1 -1
package/src/commands/prepare.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { promisify } from 'node:util';
|
|
|
25
25
|
|
|
26
26
|
const execFile = promisify(execFileCb);
|
|
27
27
|
|
|
28
|
-
import { classifyPathInput } from 'skill-family-harness-node';
|
|
28
|
+
import { classifyPathInput, writeFileAtomic } from 'skill-family-harness-node';
|
|
29
29
|
import { loadProjectConfig } from '../core/config.mjs';
|
|
30
30
|
import { captureBaseline } from '../core/baseline.mjs';
|
|
31
31
|
import { runHook } from '../core/hooks.mjs';
|
|
@@ -62,6 +62,7 @@ import { PKG_ROOT } from '../core/pkg-root.mjs';
|
|
|
62
62
|
import { writeFrozenMarker, FROZEN_MARKER_FILENAME } from '../core/frozen-marker.mjs';
|
|
63
63
|
import {
|
|
64
64
|
SOURCE_INPUT_ALGORITHM_VERSION,
|
|
65
|
+
createPublicSourceAuthorityReceipt,
|
|
65
66
|
computeSourceInputClosure,
|
|
66
67
|
checkSourceInputDirty,
|
|
67
68
|
verifySnapshotSourcesMatchClosure,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
PAYLOAD_SOURCE_TAG_WORKTREE,
|
|
81
82
|
} from '../core/postpublish.mjs';
|
|
82
83
|
import { freezeExecutionBundle, bundleRootForAuthorityDir } from '../core/postpublish-bundle.mjs';
|
|
84
|
+
import { digestReleaseAssetIdentities } from '../core/release-assets.mjs';
|
|
83
85
|
|
|
84
86
|
// ---------------------------------------------------------------------------
|
|
85
87
|
// 安装契约常量
|
|
@@ -247,7 +249,7 @@ export {
|
|
|
247
249
|
import { boundedOutputTail } from '../core/bounded-output.mjs';
|
|
248
250
|
|
|
249
251
|
/**
|
|
250
|
-
* Run all declared project hooks in order: docs, build, test, typecheck.
|
|
252
|
+
* Run all declared project hooks in order: lint, docs, build, test, typecheck.
|
|
251
253
|
*
|
|
252
254
|
* Incremental cache (T3.2): a hook that opts in with `cacheable: true` and a
|
|
253
255
|
* non-empty `cacheInputs` is fingerprinted by its configuration plus the
|
|
@@ -286,13 +288,14 @@ import { boundedOutputTail } from '../core/bounded-output.mjs';
|
|
|
286
288
|
* @throws {ReleaseError} GATE_FAILED if any hook returns a non-zero exit code,
|
|
287
289
|
* throws, or declares a cacheInputs glob that matches no file.
|
|
288
290
|
*/
|
|
291
|
+
export const HOOK_EXECUTION_ORDER = Object.freeze(['lint', 'docs', 'build', 'test', 'typecheck']);
|
|
292
|
+
|
|
289
293
|
export async function runDeclaredHooks(config, root, evidence, hookFn = runHook, options = {}) {
|
|
290
|
-
const hookOrder = ['docs', 'build', 'test', 'typecheck'];
|
|
291
294
|
const hooks = config.hooks ?? {};
|
|
292
295
|
const cacheEnabled = options.hookCache !== false;
|
|
293
296
|
const records = [];
|
|
294
297
|
|
|
295
|
-
for (const name of
|
|
298
|
+
for (const name of HOOK_EXECUTION_ORDER) {
|
|
296
299
|
const hook = hooks[name];
|
|
297
300
|
if (!hook) continue;
|
|
298
301
|
|
|
@@ -1512,7 +1515,7 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1512
1515
|
* @param {string} realRoot - The project root for relative path calculation.
|
|
1513
1516
|
* @returns {object[]} Array of external action descriptors.
|
|
1514
1517
|
*/
|
|
1515
|
-
export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map(), frozenDistributions = null) {
|
|
1518
|
+
export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map(), frozenDistributions = null, publicSourceAuthorityReceipt = null) {
|
|
1516
1519
|
const actions = [];
|
|
1517
1520
|
|
|
1518
1521
|
if (!productionAssets) {
|
|
@@ -1784,6 +1787,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1784
1787
|
}
|
|
1785
1788
|
|
|
1786
1789
|
// GitHub release
|
|
1790
|
+
const releaseAssets = publicSourceAuthorityReceipt?.coordinatorUnitId === unit.id
|
|
1791
|
+
? [publicSourceAuthorityReceipt.asset]
|
|
1792
|
+
: null;
|
|
1787
1793
|
actions.push({
|
|
1788
1794
|
id: `github-release-${unit.id}`,
|
|
1789
1795
|
type: 'github-release',
|
|
@@ -1801,10 +1807,12 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1801
1807
|
.replaceAll('{version}', unitVersion)
|
|
1802
1808
|
.replaceAll('{unit}', unit.id),
|
|
1803
1809
|
notes: unit.production?.releaseNotes ?? `Release ${resolvedTag}`,
|
|
1810
|
+
...(releaseAssets ? { releaseAssets } : {}),
|
|
1804
1811
|
},
|
|
1805
1812
|
expected: {
|
|
1806
1813
|
tag: resolvedTag,
|
|
1807
1814
|
commit: asset.commit,
|
|
1815
|
+
...(releaseAssets ? { releaseAssetsDigest: digestReleaseAssetIdentities(releaseAssets) } : {}),
|
|
1808
1816
|
},
|
|
1809
1817
|
status: 'PENDING',
|
|
1810
1818
|
});
|
|
@@ -3647,7 +3655,68 @@ export async function prepareRelease(options) {
|
|
|
3647
3655
|
// 构建冻结分发映射:unitId -> frozen distributions
|
|
3648
3656
|
const frozenDistributionsMap = new Map(units.map((u) => [u.id, u.distributions]));
|
|
3649
3657
|
|
|
3650
|
-
let
|
|
3658
|
+
let publicSourceAuthorityReceipt = null;
|
|
3659
|
+
if (config.publicSourceAuthorityReceipt) {
|
|
3660
|
+
if (!production || !sourceAuthority) {
|
|
3661
|
+
throw new ReleaseError(
|
|
3662
|
+
CONFIG_INVALID,
|
|
3663
|
+
'publicSourceAuthorityReceipt requires a production prepare with source-authority closure enabled',
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
const unitsById = new Map(units.map((unit) => [unit.id, unit]));
|
|
3667
|
+
const subjects = config.publicSourceAuthorityReceipt.subjectUnitIds.map((unitId) => {
|
|
3668
|
+
const unit = unitsById.get(unitId);
|
|
3669
|
+
const npmDistribution = unit?.distributions?.find((distribution) => distribution.type === 'npm');
|
|
3670
|
+
const npm = unit?.frozenSnapshot?.npm;
|
|
3671
|
+
if (!unit || !npmDistribution || !npm) {
|
|
3672
|
+
throw new ReleaseError(
|
|
3673
|
+
GATE_FAILED,
|
|
3674
|
+
`public source-authority subject unit "${unitId}" is missing its frozen npm tarball`,
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
return {
|
|
3678
|
+
packageName: npmDistribution.package,
|
|
3679
|
+
version: unit.targetVersion,
|
|
3680
|
+
filename: basename(npm.tarballPath),
|
|
3681
|
+
sha256: npm.tarballSha256,
|
|
3682
|
+
};
|
|
3683
|
+
});
|
|
3684
|
+
const builtReceipt = createPublicSourceAuthorityReceipt({
|
|
3685
|
+
sourceRepository: sourceAuthority.sourceRepository,
|
|
3686
|
+
sourceBaseCommit: baseline.gitHead,
|
|
3687
|
+
subjects,
|
|
3688
|
+
});
|
|
3689
|
+
const assetName = 'source-authority-receipt.json';
|
|
3690
|
+
const assetDirectory = resolve(runDir, 'release-assets');
|
|
3691
|
+
await mkdir(assetDirectory, { recursive: true });
|
|
3692
|
+
await writeFileAtomic(assetDirectory, assetName, builtReceipt.bytes, { mode: 0o644 });
|
|
3693
|
+
publicSourceAuthorityReceipt = {
|
|
3694
|
+
coordinatorUnitId: config.publicSourceAuthorityReceipt.coordinatorUnitId,
|
|
3695
|
+
subjectUnitIds: [...config.publicSourceAuthorityReceipt.subjectUnitIds],
|
|
3696
|
+
asset: {
|
|
3697
|
+
name: assetName,
|
|
3698
|
+
path: relative(realRoot, resolve(assetDirectory, assetName)),
|
|
3699
|
+
sha256: builtReceipt.sha256,
|
|
3700
|
+
},
|
|
3701
|
+
};
|
|
3702
|
+
await evidence.append({
|
|
3703
|
+
phase: 'public-source-authority-receipt',
|
|
3704
|
+
status: 'frozen',
|
|
3705
|
+
coordinatorUnitId: publicSourceAuthorityReceipt.coordinatorUnitId,
|
|
3706
|
+
subjectUnitIds: publicSourceAuthorityReceipt.subjectUnitIds,
|
|
3707
|
+
assetPath: publicSourceAuthorityReceipt.asset.path,
|
|
3708
|
+
assetSha256: publicSourceAuthorityReceipt.asset.sha256,
|
|
3709
|
+
});
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
let externalActions = buildExternalActions(
|
|
3713
|
+
unitResults,
|
|
3714
|
+
resolvedVersions,
|
|
3715
|
+
productionAssets,
|
|
3716
|
+
externalMarketplaceFreezes,
|
|
3717
|
+
frozenDistributionsMap,
|
|
3718
|
+
publicSourceAuthorityReceipt,
|
|
3719
|
+
);
|
|
3651
3720
|
|
|
3652
3721
|
// Compute overall snapshot digest
|
|
3653
3722
|
const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
|
|
@@ -3822,6 +3891,7 @@ export async function prepareRelease(options) {
|
|
|
3822
3891
|
} : {}),
|
|
3823
3892
|
units,
|
|
3824
3893
|
externalActions,
|
|
3894
|
+
...(publicSourceAuthorityReceipt ? { publicSourceAuthorityReceipt } : {}),
|
|
3825
3895
|
...(frozenPostPublish ? { postPublish: frozenPostPublish } : {}),
|
|
3826
3896
|
...(sourceAuthority ? { sourceAuthority } : {}),
|
|
3827
3897
|
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
package/src/commands/publish.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
GATE_FAILED,
|
|
62
62
|
CONFIG_MISSING,
|
|
63
63
|
BASELINE_CHANGED,
|
|
64
|
+
CONTENT_MISMATCH,
|
|
64
65
|
PARTIAL_RELEASE,
|
|
65
66
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
66
67
|
} from '../core/errors.mjs';
|
|
@@ -78,6 +79,7 @@ import {
|
|
|
78
79
|
verifyFrozenSnapshot,
|
|
79
80
|
} from '../snapshot/frozen.mjs';
|
|
80
81
|
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
82
|
+
import { normalizeReleaseAssets } from '../core/release-assets.mjs';
|
|
81
83
|
|
|
82
84
|
function assertInsideAssetRoot(assetRoot, candidate, label) {
|
|
83
85
|
const rel = relative(assetRoot, candidate);
|
|
@@ -608,6 +610,17 @@ export async function publishRelease(options) {
|
|
|
608
610
|
}, root);
|
|
609
611
|
}
|
|
610
612
|
}
|
|
613
|
+
for (const action of plan.externalActions ?? []) {
|
|
614
|
+
for (const releaseAsset of normalizeReleaseAssets(action.parameters?.releaseAssets)) {
|
|
615
|
+
const verifiedAsset = await verifyFrozenFile({
|
|
616
|
+
root,
|
|
617
|
+
filePath: releaseAsset.path,
|
|
618
|
+
expectedSha256: releaseAsset.sha256,
|
|
619
|
+
label: `frozen GitHub Release asset "${releaseAsset.name}"`,
|
|
620
|
+
});
|
|
621
|
+
assertInsideAssetRoot(assetRoot, verifiedAsset.physical, 'frozen GitHub Release asset');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
611
624
|
await evidence.append({ phase: 'safety-gate', gate: 'frozen-artifacts', status: 'passed' });
|
|
612
625
|
}
|
|
613
626
|
|
|
@@ -987,6 +1000,29 @@ export async function publishRelease(options) {
|
|
|
987
1000
|
);
|
|
988
1001
|
}
|
|
989
1002
|
|
|
1003
|
+
if (
|
|
1004
|
+
plan.publicSourceAuthorityReceipt
|
|
1005
|
+
&& remoteResult.observation?.observedCommit !== plan.baseline.headCommit
|
|
1006
|
+
) {
|
|
1007
|
+
const observedCommit = remoteResult.observation?.observedCommit ?? null;
|
|
1008
|
+
await evidence.append({
|
|
1009
|
+
phase: 'safety-gate',
|
|
1010
|
+
gate: 'source-authority',
|
|
1011
|
+
status: 'failed',
|
|
1012
|
+
expectedCommit: plan.baseline.headCommit,
|
|
1013
|
+
observedCommit,
|
|
1014
|
+
});
|
|
1015
|
+
throw new ReleaseError(
|
|
1016
|
+
CONTENT_MISMATCH,
|
|
1017
|
+
'source authority commit gate failed: remote observedCommit does not match the public receipt sourceBaseCommit',
|
|
1018
|
+
{
|
|
1019
|
+
gate: 'source-authority',
|
|
1020
|
+
expectedCommit: plan.baseline.headCommit,
|
|
1021
|
+
observedCommit,
|
|
1022
|
+
},
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
990
1026
|
// Create receipt for successful verification
|
|
991
1027
|
sourceAuthorityReceipt = createSourceAuthorityReceipt({
|
|
992
1028
|
plan,
|
package/src/commands/verify.mjs
CHANGED
|
@@ -46,7 +46,11 @@ import {
|
|
|
46
46
|
POST_PUBLISH_VERIFY_FAILED,
|
|
47
47
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
48
48
|
} from '../core/errors.mjs';
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
consumePublicSourceAuthorityReceipt,
|
|
51
|
+
verifySourceAuthorityReceipt,
|
|
52
|
+
} from '../core/source-authority.mjs';
|
|
53
|
+
import { verifyFrozenFile } from '../snapshot/frozen.mjs';
|
|
50
54
|
import {
|
|
51
55
|
deriveBaselineAdvances,
|
|
52
56
|
applyBaselineAdvances,
|
|
@@ -61,6 +65,7 @@ import {
|
|
|
61
65
|
normalizeRegistry,
|
|
62
66
|
registryTokenKey,
|
|
63
67
|
resolveNpmRegistryAuthToken,
|
|
68
|
+
verifyFrozenNpmTarballContract,
|
|
64
69
|
} from '../adapters/npm.mjs';
|
|
65
70
|
import { runConsumerVerificationGates } from '../core/verification-gates.mjs';
|
|
66
71
|
import {
|
|
@@ -109,6 +114,40 @@ export const VERIFICATION_RESOLVED_TYPES = Object.freeze({
|
|
|
109
114
|
NOT_REQUIRED_UNCHANGED: 'NOT_REQUIRED_UNCHANGED',
|
|
110
115
|
});
|
|
111
116
|
|
|
117
|
+
/** Re-read every frozen subject tarball before consuming the public receipt. */
|
|
118
|
+
export async function verifyPublicSourceAuthorityReceiptOffline({ plan, root }) {
|
|
119
|
+
const descriptor = plan.publicSourceAuthorityReceipt;
|
|
120
|
+
const verifiedAsset = await verifyFrozenFile({
|
|
121
|
+
root,
|
|
122
|
+
filePath: descriptor.asset.path,
|
|
123
|
+
expectedSha256: descriptor.asset.sha256,
|
|
124
|
+
label: 'public source-authority receipt',
|
|
125
|
+
});
|
|
126
|
+
const actualSubjects = [];
|
|
127
|
+
for (const unitId of descriptor.subjectUnitIds) {
|
|
128
|
+
const unit = plan.units.find((candidate) => candidate.id === unitId);
|
|
129
|
+
const npmDistribution = unit?.distributions?.find((distribution) => distribution.type === 'npm');
|
|
130
|
+
const npm = unit?.frozenSnapshot?.npm;
|
|
131
|
+
await verifyFrozenNpmTarballContract({
|
|
132
|
+
package: npmDistribution?.package,
|
|
133
|
+
version: unit?.targetVersion,
|
|
134
|
+
tarballPath: npm?.tarballPath,
|
|
135
|
+
tarballSha256: npm?.tarballSha256,
|
|
136
|
+
integrity: npm?.integrity,
|
|
137
|
+
}, root);
|
|
138
|
+
actualSubjects.push({
|
|
139
|
+
packageName: npmDistribution.package,
|
|
140
|
+
version: unit.targetVersion,
|
|
141
|
+
filename: basename(npm.tarballPath),
|
|
142
|
+
sha256: npm.tarballSha256,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return consumePublicSourceAuthorityReceipt(
|
|
146
|
+
await readFile(verifiedAsset.physical),
|
|
147
|
+
actualSubjects,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
/**
|
|
113
152
|
* Map plan action type to adapter ActionType.
|
|
114
153
|
* Must match publish.mjs and reconcile.mjs.
|
|
@@ -1689,6 +1728,19 @@ export async function verifyRelease(options) {
|
|
|
1689
1728
|
|
|
1690
1729
|
await evidence.append({ phase: 'verify', step: 'adapter-verify', status: 'completed' });
|
|
1691
1730
|
|
|
1731
|
+
if (plan.publicSourceAuthorityReceipt) {
|
|
1732
|
+
const consumed = await verifyPublicSourceAuthorityReceiptOffline({ plan, root });
|
|
1733
|
+
await evidence.append({
|
|
1734
|
+
phase: 'public-source-authority-receipt',
|
|
1735
|
+
status: 'passed',
|
|
1736
|
+
verificationMode: 'offline-consumer',
|
|
1737
|
+
sourceRepository: consumed.sourceRepository,
|
|
1738
|
+
sourceBaseCommit: consumed.sourceBaseCommit,
|
|
1739
|
+
subjectCount: consumed.subjects.length,
|
|
1740
|
+
assetSha256: consumed.sha256,
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1692
1744
|
// =======================================================================
|
|
1693
1745
|
// Step 4: Installation smoke test
|
|
1694
1746
|
// =======================================================================
|
package/src/core/config.mjs
CHANGED
|
@@ -504,6 +504,37 @@ export async function loadProjectConfig({ root, configPath } = {}) {
|
|
|
504
504
|
// inferred at execution time. A gate must name one existing unit and, for
|
|
505
505
|
// consumer verification, one distribution that the unit actually ships.
|
|
506
506
|
const unitsById = new Map(config.releaseUnits.map((unit) => [unit.id, unit]));
|
|
507
|
+
const publicReceipt = config.publicSourceAuthorityReceipt;
|
|
508
|
+
if (publicReceipt) {
|
|
509
|
+
if (!config.project?.sourceRepository) {
|
|
510
|
+
throw new ReleaseError(
|
|
511
|
+
CONFIG_INVALID,
|
|
512
|
+
'publicSourceAuthorityReceipt requires project.sourceRepository',
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (!unitsById.has(publicReceipt.coordinatorUnitId)) {
|
|
516
|
+
throw new ReleaseError(
|
|
517
|
+
CONFIG_INVALID,
|
|
518
|
+
`publicSourceAuthorityReceipt references unknown coordinator unit "${publicReceipt.coordinatorUnitId}"`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
for (const unitId of publicReceipt.subjectUnitIds) {
|
|
522
|
+
const unit = unitsById.get(unitId);
|
|
523
|
+
if (!unit) {
|
|
524
|
+
throw new ReleaseError(
|
|
525
|
+
CONFIG_INVALID,
|
|
526
|
+
`publicSourceAuthorityReceipt references unknown subject unit "${unitId}"`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const npmDistributions = (unit.distributions ?? []).filter((distribution) => distribution.type === 'npm');
|
|
530
|
+
if (npmDistributions.length !== 1) {
|
|
531
|
+
throw new ReleaseError(
|
|
532
|
+
CONFIG_INVALID,
|
|
533
|
+
`publicSourceAuthorityReceipt subject unit "${unitId}" must declare exactly one npm distribution`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
507
538
|
const gateIds = new Set();
|
|
508
539
|
for (const gate of config.verificationGates ?? []) {
|
|
509
540
|
if (gateIds.has(gate.id)) {
|
package/src/core/plan.mjs
CHANGED
|
@@ -26,6 +26,8 @@ import { canonicalJson, sha256Hex } from './digest.mjs';
|
|
|
26
26
|
import { ReleaseError, GATE_FAILED } from './errors.mjs';
|
|
27
27
|
import { readTrustedPackageResource } from './trusted-resource.mjs';
|
|
28
28
|
import { computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from './installation-contract.mjs';
|
|
29
|
+
import { createPublicSourceAuthorityReceipt } from './source-authority.mjs';
|
|
30
|
+
import { digestReleaseAssetIdentities, normalizeReleaseAssets } from './release-assets.mjs';
|
|
29
31
|
// NOTE (T2.2 step 3): plan.mjs and the platform registry form a module cycle
|
|
30
32
|
// (plan -> registry -> platforms/kimi -> plan, because the kimi strategies
|
|
31
33
|
// bind computePlanDigest). PLATFORMS is therefore only ever read inside
|
|
@@ -450,6 +452,57 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
450
452
|
const units = Array.isArray(plan.units) ? plan.units : [];
|
|
451
453
|
const actions = Array.isArray(plan.externalActions) ? plan.externalActions : [];
|
|
452
454
|
const expectedAdapterMap = buildExpectedAdapterMap();
|
|
455
|
+
let publicReceiptBinding = null;
|
|
456
|
+
if (plan.publicSourceAuthorityReceipt) {
|
|
457
|
+
try {
|
|
458
|
+
if (plan.production?.mode !== 'github-npm-v1') {
|
|
459
|
+
failures.push('publicSourceAuthorityReceipt requires a production plan');
|
|
460
|
+
}
|
|
461
|
+
if (!plan.sourceAuthority?.sourceRepository || !plan.baseline?.headCommit) {
|
|
462
|
+
failures.push('publicSourceAuthorityReceipt requires sourceAuthority.sourceRepository and baseline.headCommit');
|
|
463
|
+
}
|
|
464
|
+
const descriptor = plan.publicSourceAuthorityReceipt;
|
|
465
|
+
const coordinator = units.find((unit) => unit.id === descriptor.coordinatorUnitId);
|
|
466
|
+
if (!coordinator) failures.push(`publicSourceAuthorityReceipt references unknown coordinator unit "${descriptor.coordinatorUnitId}"`);
|
|
467
|
+
const subjectIds = descriptor.subjectUnitIds ?? [];
|
|
468
|
+
if (new Set(subjectIds).size !== subjectIds.length) {
|
|
469
|
+
failures.push('publicSourceAuthorityReceipt subjectUnitIds must be unique');
|
|
470
|
+
}
|
|
471
|
+
const subjects = subjectIds.map((unitId) => {
|
|
472
|
+
const unit = units.find((candidate) => candidate.id === unitId);
|
|
473
|
+
if (!unit) throw new Error(`unknown subject unit "${unitId}"`);
|
|
474
|
+
const npmDistributions = (unit.distributions ?? []).filter((distribution) => distribution.type === 'npm');
|
|
475
|
+
if (npmDistributions.length !== 1 || !unit.frozenSnapshot?.npm) {
|
|
476
|
+
throw new Error(`subject unit "${unitId}" must bind exactly one frozen npm tarball`);
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
packageName: npmDistributions[0].package,
|
|
480
|
+
version: unit.targetVersion,
|
|
481
|
+
filename: basename(unit.frozenSnapshot.npm.tarballPath),
|
|
482
|
+
sha256: unit.frozenSnapshot.npm.tarballSha256,
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
const rebuilt = createPublicSourceAuthorityReceipt({
|
|
486
|
+
sourceRepository: plan.sourceAuthority?.sourceRepository,
|
|
487
|
+
sourceBaseCommit: plan.baseline?.headCommit,
|
|
488
|
+
subjects,
|
|
489
|
+
});
|
|
490
|
+
const assets = normalizeReleaseAssets([descriptor.asset]);
|
|
491
|
+
if (descriptor.asset.name !== 'source-authority-receipt.json') {
|
|
492
|
+
failures.push('publicSourceAuthorityReceipt asset name must be source-authority-receipt.json');
|
|
493
|
+
}
|
|
494
|
+
if (descriptor.asset.sha256 !== rebuilt.sha256) {
|
|
495
|
+
failures.push('publicSourceAuthorityReceipt asset SHA-256 does not match the mechanically derived receipt bytes');
|
|
496
|
+
}
|
|
497
|
+
publicReceiptBinding = {
|
|
498
|
+
coordinatorUnitId: descriptor.coordinatorUnitId,
|
|
499
|
+
assets,
|
|
500
|
+
assetsDigest: digestReleaseAssetIdentities(assets),
|
|
501
|
+
};
|
|
502
|
+
} catch (error) {
|
|
503
|
+
failures.push(`publicSourceAuthorityReceipt is invalid: ${error.message}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
453
506
|
|
|
454
507
|
// --- Must have at least one action ---
|
|
455
508
|
if (actions.length === 0) {
|
|
@@ -666,6 +719,31 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
666
719
|
_checkRequired(action, 'parameters.notes', action.parameters?.notes, productionConfig.releaseNotes ?? `Release ${expectedTag}`, unitId, failures);
|
|
667
720
|
_checkRequired(action, 'expected.tag', action.expected?.tag, expectedTag, unitId, failures);
|
|
668
721
|
_checkRequired(action, 'expected.commit', action.expected?.commit, frozen?.commit, unitId, failures);
|
|
722
|
+
if (publicReceiptBinding?.coordinatorUnitId === unitId) {
|
|
723
|
+
try {
|
|
724
|
+
const actualAssets = normalizeReleaseAssets(action.parameters?.releaseAssets);
|
|
725
|
+
if (canonicalJson(actualAssets) !== canonicalJson(publicReceiptBinding.assets)) {
|
|
726
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.releaseAssets do not match publicSourceAuthorityReceipt.asset`);
|
|
727
|
+
}
|
|
728
|
+
} catch (error) {
|
|
729
|
+
failures.push(`unit "${unitId}", action "${action.id}": invalid parameters.releaseAssets: ${error.message}`);
|
|
730
|
+
}
|
|
731
|
+
_checkRequired(
|
|
732
|
+
action,
|
|
733
|
+
'expected.releaseAssetsDigest',
|
|
734
|
+
action.expected?.releaseAssetsDigest,
|
|
735
|
+
publicReceiptBinding.assetsDigest,
|
|
736
|
+
unitId,
|
|
737
|
+
failures,
|
|
738
|
+
);
|
|
739
|
+
} else {
|
|
740
|
+
if (action.parameters?.releaseAssets !== undefined) {
|
|
741
|
+
failures.push(`unit "${unitId}", action "${action.id}": unexpected parameters.releaseAssets`);
|
|
742
|
+
}
|
|
743
|
+
if (action.expected?.releaseAssetsDigest !== undefined) {
|
|
744
|
+
failures.push(`unit "${unitId}", action "${action.id}": unexpected expected.releaseAssetsDigest`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
669
747
|
}
|
|
670
748
|
}
|
|
671
749
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen GitHub Release asset identities.
|
|
3
|
+
*
|
|
4
|
+
* This module owns only release-domain mapping: a plan freezes local asset
|
|
5
|
+
* paths and their already-computed SHA-256 values, while remote observation
|
|
6
|
+
* reduces the downloaded bytes to the same name/digest identity list.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { basename } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { canonicalJson, sha256Hex } from './digest.mjs';
|
|
12
|
+
|
|
13
|
+
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
14
|
+
const SAFE_ASSET_NAME_RE = /^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
15
|
+
|
|
16
|
+
/** Validate and normalize a plan's optional frozen Release assets. */
|
|
17
|
+
export function normalizeReleaseAssets(assets) {
|
|
18
|
+
if (assets === undefined) return [];
|
|
19
|
+
if (!Array.isArray(assets) || assets.length === 0) {
|
|
20
|
+
throw new TypeError('releaseAssets must be a non-empty array when present');
|
|
21
|
+
}
|
|
22
|
+
const names = new Set();
|
|
23
|
+
const normalized = assets.map((asset, index) => {
|
|
24
|
+
if (!asset || typeof asset !== 'object' || Array.isArray(asset)) {
|
|
25
|
+
throw new TypeError(`releaseAssets[${index}] must be an object`);
|
|
26
|
+
}
|
|
27
|
+
const keys = Object.keys(asset).sort();
|
|
28
|
+
if (canonicalJson(keys) !== canonicalJson(['name', 'path', 'sha256'])) {
|
|
29
|
+
throw new TypeError(`releaseAssets[${index}] must contain only name, path, and sha256`);
|
|
30
|
+
}
|
|
31
|
+
if (typeof asset.path !== 'string' || asset.path.length === 0) {
|
|
32
|
+
throw new TypeError(`releaseAssets[${index}].path must be a non-empty project-relative path`);
|
|
33
|
+
}
|
|
34
|
+
if (!SAFE_ASSET_NAME_RE.test(asset.name ?? '') || basename(asset.path) !== asset.name) {
|
|
35
|
+
throw new TypeError(`releaseAssets[${index}].name must be a safe basename matching path`);
|
|
36
|
+
}
|
|
37
|
+
if (!SHA256_RE.test(asset.sha256 ?? '')) {
|
|
38
|
+
throw new TypeError(`releaseAssets[${index}].sha256 must be a lowercase SHA-256 digest`);
|
|
39
|
+
}
|
|
40
|
+
if (names.has(asset.name)) {
|
|
41
|
+
throw new TypeError(`releaseAssets contains duplicate asset name "${asset.name}"`);
|
|
42
|
+
}
|
|
43
|
+
names.add(asset.name);
|
|
44
|
+
return Object.freeze({ name: asset.name, path: asset.path, sha256: asset.sha256 });
|
|
45
|
+
});
|
|
46
|
+
normalized.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
47
|
+
return Object.freeze(normalized);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Digest only public remote identity, never local paths. */
|
|
51
|
+
export function digestReleaseAssetIdentities(assets) {
|
|
52
|
+
const identities = normalizeReleaseAssets(assets).map(({ name, sha256 }) => ({ name, sha256 }));
|
|
53
|
+
return sha256Hex(canonicalJson(identities));
|
|
54
|
+
}
|
|
@@ -27,9 +27,115 @@ import {
|
|
|
27
27
|
const execFile = promisify(execFileCb);
|
|
28
28
|
const REPOSITORY_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u;
|
|
29
29
|
const GIT_MODE_RE = /^(?:100644|100755)$/u;
|
|
30
|
+
const COMMIT_RE = /^[a-f0-9]{40,64}$/u;
|
|
31
|
+
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
32
|
+
const NPM_PACKAGE_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/u;
|
|
33
|
+
const TARBALL_FILENAME_RE = /^(?!\.{1,2}$)[A-Za-z0-9@][A-Za-z0-9._@+-]*\.tgz$/u;
|
|
30
34
|
|
|
31
35
|
export const SOURCE_INPUT_ALGORITHM_VERSION = 1;
|
|
32
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Build the intentionally small public source-authority receipt.
|
|
39
|
+
*
|
|
40
|
+
* The receipt is an external release asset. It contains no plan/run fields
|
|
41
|
+
* and no self digest; the frozen action binds the canonical bytes instead.
|
|
42
|
+
*/
|
|
43
|
+
export function createPublicSourceAuthorityReceipt({
|
|
44
|
+
sourceRepository,
|
|
45
|
+
sourceBaseCommit,
|
|
46
|
+
subjects,
|
|
47
|
+
}) {
|
|
48
|
+
if (!REPOSITORY_RE.test(sourceRepository ?? '')) {
|
|
49
|
+
throw new ReleaseError(CONFIG_INVALID, 'public source-authority receipt requires an explicit GitHub owner/repo');
|
|
50
|
+
}
|
|
51
|
+
if (!COMMIT_RE.test(sourceBaseCommit ?? '')) {
|
|
52
|
+
throw new ReleaseError(CONFIG_INVALID, 'public source-authority receipt requires a full hexadecimal source base commit');
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(subjects) || subjects.length === 0) {
|
|
55
|
+
throw new ReleaseError(CONFIG_INVALID, 'public source-authority receipt requires at least one npm tarball subject');
|
|
56
|
+
}
|
|
57
|
+
const packageNames = new Set();
|
|
58
|
+
const normalizedSubjects = subjects.map((subject, index) => {
|
|
59
|
+
const keys = Object.keys(subject ?? {}).sort();
|
|
60
|
+
if (canonicalJson(keys) !== canonicalJson(['filename', 'packageName', 'sha256', 'version'])) {
|
|
61
|
+
throw new ReleaseError(CONFIG_INVALID, `public source-authority subject ${index} has an invalid field set`);
|
|
62
|
+
}
|
|
63
|
+
if (!NPM_PACKAGE_RE.test(subject.packageName ?? '')) {
|
|
64
|
+
throw new ReleaseError(CONFIG_INVALID, `public source-authority subject ${index} has an invalid packageName`);
|
|
65
|
+
}
|
|
66
|
+
if (packageNames.has(subject.packageName)) {
|
|
67
|
+
throw new ReleaseError(CONFIG_INVALID, `public source-authority receipt has duplicate packageName "${subject.packageName}"`);
|
|
68
|
+
}
|
|
69
|
+
packageNames.add(subject.packageName);
|
|
70
|
+
if (typeof subject.version !== 'string' || subject.version.length === 0) {
|
|
71
|
+
throw new ReleaseError(CONFIG_INVALID, `public source-authority subject "${subject.packageName}" has an invalid version`);
|
|
72
|
+
}
|
|
73
|
+
if (!TARBALL_FILENAME_RE.test(subject.filename ?? '')) {
|
|
74
|
+
throw new ReleaseError(CONFIG_INVALID, `public source-authority subject "${subject.packageName}" has an invalid tarball filename`);
|
|
75
|
+
}
|
|
76
|
+
if (!SHA256_RE.test(subject.sha256 ?? '')) {
|
|
77
|
+
throw new ReleaseError(CONFIG_INVALID, `public source-authority subject "${subject.packageName}" has an invalid SHA-256`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
packageName: subject.packageName,
|
|
81
|
+
version: subject.version,
|
|
82
|
+
filename: subject.filename,
|
|
83
|
+
sha256: subject.sha256,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
normalizedSubjects.sort((left, right) => (
|
|
87
|
+
left.packageName < right.packageName ? -1 : left.packageName > right.packageName ? 1 : 0
|
|
88
|
+
));
|
|
89
|
+
const receipt = {
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
kind: 'skill-family.source-authority-receipt',
|
|
92
|
+
sourceRepository,
|
|
93
|
+
sourceBaseCommit,
|
|
94
|
+
subjects: normalizedSubjects,
|
|
95
|
+
};
|
|
96
|
+
const bytes = Buffer.from(canonicalJson(receipt), 'utf8');
|
|
97
|
+
return Object.freeze({ receipt: Object.freeze(receipt), bytes, sha256: sha256Hex(bytes) });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Parse public bytes and verify them against actual offline tarball facts. */
|
|
101
|
+
export function consumePublicSourceAuthorityReceipt(bytes, actualSubjects) {
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(Buffer.isBuffer(bytes) ? bytes.toString('utf8') : String(bytes));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new ReleaseError(CONFIG_INVALID, `public source-authority receipt is not valid JSON: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
const topKeys = Object.keys(parsed ?? {}).sort();
|
|
109
|
+
if (canonicalJson(topKeys) !== canonicalJson([
|
|
110
|
+
'kind', 'schemaVersion', 'sourceBaseCommit', 'sourceRepository', 'subjects',
|
|
111
|
+
])) {
|
|
112
|
+
throw new ReleaseError(CONFIG_INVALID, 'public source-authority receipt has an invalid field set');
|
|
113
|
+
}
|
|
114
|
+
if (parsed.schemaVersion !== 1 || parsed.kind !== 'skill-family.source-authority-receipt') {
|
|
115
|
+
throw new ReleaseError(CONFIG_INVALID, 'public source-authority receipt kind or schemaVersion is unsupported');
|
|
116
|
+
}
|
|
117
|
+
const rebuilt = createPublicSourceAuthorityReceipt(parsed);
|
|
118
|
+
if (!Buffer.from(bytes).equals(rebuilt.bytes)) {
|
|
119
|
+
throw new ReleaseError(CONFIG_INVALID, 'public source-authority receipt bytes are not canonical JSON');
|
|
120
|
+
}
|
|
121
|
+
if (actualSubjects !== undefined) {
|
|
122
|
+
const actual = createPublicSourceAuthorityReceipt({
|
|
123
|
+
sourceRepository: parsed.sourceRepository,
|
|
124
|
+
sourceBaseCommit: parsed.sourceBaseCommit,
|
|
125
|
+
subjects: actualSubjects,
|
|
126
|
+
}).receipt.subjects;
|
|
127
|
+
if (canonicalJson(actual) !== canonicalJson(rebuilt.receipt.subjects)) {
|
|
128
|
+
throw new ReleaseError(CONTENT_MISMATCH, 'public source-authority receipt subjects do not match actual tarballs');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
sourceRepository: rebuilt.receipt.sourceRepository,
|
|
133
|
+
sourceBaseCommit: rebuilt.receipt.sourceBaseCommit,
|
|
134
|
+
subjects: rebuilt.receipt.subjects,
|
|
135
|
+
sha256: rebuilt.sha256,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
33
139
|
/**
|
|
34
140
|
* Compute the deterministic source-input closure for release units.
|
|
35
141
|
*
|
|
@@ -325,6 +431,7 @@ async function verifyWithInjectedReader({
|
|
|
325
431
|
sourceRepository,
|
|
326
432
|
}) {
|
|
327
433
|
const mismatchedPaths = [];
|
|
434
|
+
let observedCommit;
|
|
328
435
|
for (const entry of closure.entries) {
|
|
329
436
|
let result;
|
|
330
437
|
try {
|
|
@@ -334,6 +441,12 @@ async function verifyWithInjectedReader({
|
|
|
334
441
|
}
|
|
335
442
|
const classified = classifyRemoteStatus(result, sourceRepository, defaultBranch);
|
|
336
443
|
if (classified) return classified;
|
|
444
|
+
if (result.observedCommit !== undefined) {
|
|
445
|
+
if (observedCommit !== undefined && observedCommit !== result.observedCommit) {
|
|
446
|
+
return failure(REMOTE_UNAVAILABLE, 'remote source reader returned inconsistent observed commits');
|
|
447
|
+
}
|
|
448
|
+
observedCommit = result.observedCommit;
|
|
449
|
+
}
|
|
337
450
|
if (
|
|
338
451
|
sha256Hex(Buffer.isBuffer(result.content) ? result.content : Buffer.from(result.content))
|
|
339
452
|
!== entry.digest
|
|
@@ -349,6 +462,7 @@ async function verifyWithInjectedReader({
|
|
|
349
462
|
observation: {
|
|
350
463
|
defaultBranch,
|
|
351
464
|
entryCount: closure.entries.length,
|
|
465
|
+
...(observedCommit ? { observedCommit } : {}),
|
|
352
466
|
sourceRepository,
|
|
353
467
|
},
|
|
354
468
|
};
|
|
@@ -515,12 +629,22 @@ export function verifySourceAuthorityReceipt({ plan, run }) {
|
|
|
515
629
|
&& receipt.planDigest === plan.digest
|
|
516
630
|
&& receipt.result === 'CONSISTENT'
|
|
517
631
|
));
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
: {
|
|
632
|
+
if (!matching) {
|
|
633
|
+
return {
|
|
521
634
|
passed: false,
|
|
522
635
|
reason: 'publish run has no CONSISTENT source-authority receipt bound to this plan digest',
|
|
523
636
|
};
|
|
637
|
+
}
|
|
638
|
+
if (
|
|
639
|
+
plan.publicSourceAuthorityReceipt
|
|
640
|
+
&& matching.observedCommit !== plan.baseline?.headCommit
|
|
641
|
+
) {
|
|
642
|
+
return {
|
|
643
|
+
passed: false,
|
|
644
|
+
reason: 'publish source-authority receipt observedCommit does not match the public receipt sourceBaseCommit',
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
return { passed: true, receipt: matching };
|
|
524
648
|
}
|
|
525
649
|
|
|
526
650
|
/** Create the digest-bound receipt persisted by publish. */
|
|
@@ -317,7 +317,7 @@ async function collectSkills(srcDir, physicalRoot) {
|
|
|
317
317
|
const CODEX_PREAMBLE = `\
|
|
318
318
|
> **Codex 安装入口解析协议**:在调用 CLI 前,Agent 必须从宿主当前已加载技能的元数据中取得本 \`SKILL.md\` 的实际绝对路径,并将该字面量记为 \`SKILL_FILE\`。\n\
|
|
319
319
|
> \`SKILL_FILE\` 不是环境变量;禁止从工作目录、可执行搜索路径、源码仓库或 shell 调用上下文猜测。若宿主未提供该绝对路径,立即停止并报告安装定位失败。\n\
|
|
320
|
-
> 对 \`SKILL_FILE\` 执行 \`realpath\`,取其目录向上两级得到 \`PLUGIN_ROOT\`;校验真实技能路径匹配 \`PLUGIN_ROOT/skills/*/SKILL.md
|
|
320
|
+
> 对 \`SKILL_FILE\` 执行 \`realpath\`,取其目录向上两级得到 \`PLUGIN_ROOT\`;校验真实技能路径匹配 \`PLUGIN_ROOT/skills/*/SKILL.md\` 且仍位于插件根内(路径包含检查)。\n\
|
|
321
321
|
> 令 \`RELEASE_SKILL_ENTRY=PLUGIN_ROOT/bin/release-skill.mjs\`,对入口执行 \`realpath\` containment、\`lstat\` 非符号链接且为普通文件校验。\n\
|
|
322
322
|
> 每一次 shell 工具调用都必须在同一个调用中用上述已验证绝对值设置 \`RELEASE_SKILL_ENTRY\`,然后执行 \`node "$RELEASE_SKILL_ENTRY" ...\`;不得依赖前一次 shell 的变量。\n\
|
|
323
323
|
>\n\
|