release-skill 0.4.2 → 0.5.1
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 +44 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +10 -7
- package/README.zh-CN.md +10 -7
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/error-codes.json +154 -0
- package/adapters/claude/bin/kernel-protocol.json +60 -0
- package/adapters/claude/bin/registry.json +179 -0
- package/adapters/claude/bin/release-skill.bundle.mjs +37609 -23014
- package/adapters/claude/bin/rules.json +104 -0
- package/adapters/claude/skills/release-assess/SKILL.md +2 -0
- package/adapters/claude/skills/release-help/SKILL.md +3 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +2 -2
- package/adapters/claude/skills/release-verify/SKILL.md +2 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/error-codes.json +154 -0
- package/adapters/codex/bin/kernel-protocol.json +60 -0
- package/adapters/codex/bin/registry.json +179 -0
- package/adapters/codex/bin/release-skill.bundle.mjs +37609 -23014
- package/adapters/codex/bin/rules.json +104 -0
- package/adapters/codex/skills/release-assess/SKILL.md +2 -0
- package/adapters/codex/skills/release-help/SKILL.md +3 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +2 -2
- package/adapters/codex/skills/release-verify/SKILL.md +2 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/error-codes.json +154 -0
- package/adapters/kimi/bin/kernel-protocol.json +60 -0
- package/adapters/kimi/bin/registry.json +179 -0
- package/adapters/kimi/bin/release-skill.bundle.mjs +37609 -23014
- package/adapters/kimi/bin/rules.json +104 -0
- package/adapters/kimi/skills/release-assess/SKILL.md +2 -0
- package/adapters/kimi/skills/release-help/SKILL.md +3 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +2 -2
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/error-codes.json +154 -0
- package/adapters/workbuddy/bin/kernel-protocol.json +60 -0
- package/adapters/workbuddy/bin/registry.json +179 -0
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +37609 -23014
- package/adapters/workbuddy/bin/rules.json +104 -0
- package/adapters/workbuddy/skills/release-assess/SKILL.md +2 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +3 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +2 -2
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
- package/bin/error-codes.json +154 -0
- package/bin/kernel-protocol.json +60 -0
- package/bin/registry.json +179 -0
- package/bin/release-skill.bundle.mjs +37609 -23014
- package/bin/rules.json +104 -0
- package/package.json +3 -1
- package/scripts/build-bundle.mjs +159 -5
- package/scripts/sync-public-files.mjs +5 -0
- package/skills/release-assess/SKILL.md +2 -0
- package/skills/release-help/SKILL.md +3 -1
- package/skills/release-prepare/SKILL.md +2 -2
- package/skills/release-verify/SKILL.md +2 -0
- package/skills-src/release-assess/SKILL.md +2 -0
- package/skills-src/release-help/SKILL.md +3 -1
- package/skills-src/release-prepare/SKILL.md +2 -2
- package/skills-src/release-verify/SKILL.md +2 -0
- package/src/adapters/contract.mjs +10 -10
- package/src/adapters/plugin-marketplace.mjs +48 -4
- package/src/adapters/push-snapshot.mjs +12 -4
- package/src/artifacts/entry.mjs +4 -1
- package/src/artifacts/producer-registry.mjs +9 -6
- package/src/artifacts/project-lock.mjs +324 -214
- package/src/commands/assess.mjs +231 -2
- package/src/commands/attest.mjs +11 -0
- package/src/commands/hooks.mjs +6 -1
- package/src/commands/prepare.mjs +83 -1
- package/src/core/approval.mjs +5 -2
- package/src/core/baseline.mjs +11 -1
- package/src/core/digest.mjs +47 -12
- package/src/core/foundation-inflight.mjs +46 -0
- package/src/core/hook-cache.mjs +3 -0
- package/src/core/installation-contract.mjs +4 -2
- package/src/core/plan.mjs +42 -29
- package/src/core/run.mjs +22 -36
- package/src/core/skill-resource-closure.mjs +6 -4
- package/src/core/trusted-resource.mjs +64 -15
- package/src/producers/build-adapters.mjs +27 -9
- package/src/producers/render-public-assets.mjs +12 -7
- package/src/producers/sync-skills.mjs +12 -7
- package/src/snapshot/public-map.mjs +10 -2
package/src/core/plan.mjs
CHANGED
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* @module core/plan
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { readFile,
|
|
18
|
+
import { readFile, mkdir, lstat } from 'node:fs/promises';
|
|
19
19
|
import { basename, dirname, join, resolve, parse, sep } from 'node:path';
|
|
20
20
|
import Ajv from 'ajv';
|
|
21
21
|
import addFormats from 'ajv-formats';
|
|
22
|
+
import { digestDocument } from 'skill-family-contracts';
|
|
23
|
+
import { writeFileAtomic } from 'skill-family-harness-node';
|
|
24
|
+
import { publishFileExclusive, HARNESS_ERROR_KINDS as INFLIGHT_KINDS } from './foundation-inflight.mjs';
|
|
22
25
|
import { canonicalJson, sha256Hex } from './digest.mjs';
|
|
23
26
|
import { ReleaseError, GATE_FAILED } from './errors.mjs';
|
|
24
27
|
import { readTrustedPackageResource } from './trusted-resource.mjs';
|
|
@@ -248,6 +251,11 @@ function stripRecordLayerV2(plan) {
|
|
|
248
251
|
* @returns {string} Lowercase 64-char hex SHA-256 digest.
|
|
249
252
|
*/
|
|
250
253
|
export function computePlanDigest(plan) {
|
|
254
|
+
// 输入域适配:计划绑定层可能含条件置 undefined 的字段(如 codebuddy 的
|
|
255
|
+
// marketplace),本地语义为省略该字段。因此不直接用严格 digestDocument,
|
|
256
|
+
// 而是经 digest.mjs 的宽松输入域包装(contracts 权威序列化 + 本地归一化)
|
|
257
|
+
// 后由 Foundation digestBytes 摘要 —— 对纯 JSON 计划与 digestDocument 字节
|
|
258
|
+
// 完全一致,对含 undefined 的计划保持迁移前字节语义。
|
|
251
259
|
if (plan.planVersion === 2) {
|
|
252
260
|
return sha256Hex(canonicalJson(stripRecordLayerV2(plan)));
|
|
253
261
|
}
|
|
@@ -286,15 +294,11 @@ export async function writePlanAtomic(planPath, plan) {
|
|
|
286
294
|
// 4. Serialise
|
|
287
295
|
const json = JSON.stringify(augmented, null, 2);
|
|
288
296
|
|
|
289
|
-
// 5. Write
|
|
297
|
+
// 5. Write atomically via Foundation (contained, exclusive temp, fsync, rename)
|
|
290
298
|
const dir = dirname(planPath);
|
|
291
299
|
await prepareAuthorityDirectory(dir);
|
|
292
300
|
await assertAuthorityFileTarget(planPath);
|
|
293
|
-
|
|
294
|
-
await writeFile(tmpPath, json, 'utf8');
|
|
295
|
-
|
|
296
|
-
// 6. Atomic rename
|
|
297
|
-
await rename(tmpPath, planPath);
|
|
301
|
+
await writeFileAtomic(dir, basename(planPath), json);
|
|
298
302
|
|
|
299
303
|
return { planPath, planDigest };
|
|
300
304
|
}
|
|
@@ -303,9 +307,11 @@ export async function writePlanAtomic(planPath, plan) {
|
|
|
303
307
|
* Create a digest-addressed plan authority exactly once.
|
|
304
308
|
*
|
|
305
309
|
* The target must be named by the computed digest. An existing byte-identical
|
|
306
|
-
* authority is reused; an existing divergent file fails closed.
|
|
307
|
-
*
|
|
308
|
-
*
|
|
310
|
+
* authority is reused; an existing divergent file fails closed. The write
|
|
311
|
+
* delegates to Foundation `publishFileExclusive` (temp + fsync + exclusive
|
|
312
|
+
* link + byte/mode/identity verification + directory fsync); this wrapper
|
|
313
|
+
* supplies the idempotent-same-bytes branch that the in-flight Foundation
|
|
314
|
+
* version does not have (G4 conflict-surface.md 第一节).
|
|
309
315
|
*/
|
|
310
316
|
export async function writePlanImmutable(planPath, plan) {
|
|
311
317
|
const planDigest = computePlanDigest(plan);
|
|
@@ -324,21 +330,15 @@ export async function writePlanImmutable(planPath, plan) {
|
|
|
324
330
|
const dir = dirname(planPath);
|
|
325
331
|
await prepareAuthorityDirectory(dir);
|
|
326
332
|
await assertAuthorityFileTarget(planPath);
|
|
327
|
-
const tmpPath = join(dir, `.release-plan-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
|
|
328
|
-
const handle = await open(tmpPath, 'wx', 0o600);
|
|
329
|
-
try {
|
|
330
|
-
await handle.writeFile(json, 'utf8');
|
|
331
|
-
await handle.sync();
|
|
332
|
-
} finally {
|
|
333
|
-
await handle.close();
|
|
334
|
-
}
|
|
335
333
|
|
|
336
334
|
try {
|
|
337
|
-
await
|
|
338
|
-
} catch (
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
335
|
+
await publishFileExclusive(dir, basename(planPath), json, { mode: 0o600 });
|
|
336
|
+
} catch (cause) {
|
|
337
|
+
if (cause?.details?.kind === INFLIGHT_KINDS.EXCLUSIVE_PUBLISH_CONFLICT) {
|
|
338
|
+
// 同字节幂等:目标已存在且字节相同 → 幂等成功(Foundation 在途版
|
|
339
|
+
// EEXIST 直接抛冲突,无幂等分支;wrapper 补齐,fail-closed 无双路径)。
|
|
340
|
+
const existing = await readFile(planPath, 'utf8').catch(() => null);
|
|
341
|
+
if (existing === json) return { planPath, planDigest };
|
|
342
342
|
// planVersion 2 (design: t1-2-digest-decoupling.md §4.1): record-layer
|
|
343
343
|
// fields (status/createdAt/baseline) are excluded from the digest, so
|
|
344
344
|
// re-preparing over identical binding content yields the SAME digest
|
|
@@ -346,7 +346,7 @@ export async function writePlanImmutable(planPath, plan) {
|
|
|
346
346
|
// it is genuinely self-consistent (its embedded digest and recomputed
|
|
347
347
|
// digest both equal this digest) -- the authority is never replaced.
|
|
348
348
|
// Any other divergence (tampering, malformed file) still fails closed.
|
|
349
|
-
if (plan.planVersion === 2) {
|
|
349
|
+
if (plan.planVersion === 2 && existing !== null) {
|
|
350
350
|
let existingPlan = null;
|
|
351
351
|
try {
|
|
352
352
|
existingPlan = JSON.parse(existing);
|
|
@@ -367,8 +367,11 @@ export async function writePlanImmutable(planPath, plan) {
|
|
|
367
367
|
{ planPath, planDigest },
|
|
368
368
|
);
|
|
369
369
|
}
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
throw new ReleaseError(
|
|
371
|
+
GATE_FAILED,
|
|
372
|
+
`immutable plan authority write failed: ${cause?.message ?? String(cause)}`,
|
|
373
|
+
{ planPath, planDigest },
|
|
374
|
+
);
|
|
372
375
|
}
|
|
373
376
|
|
|
374
377
|
return { planPath, planDigest };
|
|
@@ -531,8 +534,18 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
531
534
|
if (!frozen.parentCommit || frozen.parentCommit !== unit.previousPublicBaseline?.commit) {
|
|
532
535
|
failures.push(`unit "${unitId}" frozenSnapshot.parentCommit does not match previous public baseline commit`);
|
|
533
536
|
}
|
|
534
|
-
} else if (
|
|
535
|
-
|
|
537
|
+
} else if (branchStrategy === 'create-release-branch') {
|
|
538
|
+
// Chain-integrity gate (0.5.1): create-release-branch creates an
|
|
539
|
+
// orphan root commit (no parent). It is only legitimate for the
|
|
540
|
+
// FIRST release of a repository (previousPublicBaseline.mode=none).
|
|
541
|
+
// A later release using it would start a fresh lineage disconnected
|
|
542
|
+
// from the published history — the historical orphan-root channel.
|
|
543
|
+
if (unit.previousPublicBaseline?.mode === 'bound') {
|
|
544
|
+
failures.push(`unit "${unitId}" create-release-branch is only valid for first releases (previousPublicBaseline.mode=none), but a bound previous public baseline exists`);
|
|
545
|
+
}
|
|
546
|
+
if (frozen.parentCommit) {
|
|
547
|
+
failures.push(`unit "${unitId}" create-release-branch must not freeze a parentCommit`);
|
|
548
|
+
}
|
|
536
549
|
}
|
|
537
550
|
if (
|
|
538
551
|
branchStrategy === 'advance-existing-branch' &&
|
|
@@ -1149,7 +1162,7 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
1149
1162
|
// 不能只比较两个可一起篡改的字符串;必须重新由 distribution 的 installationContract 计算并验证
|
|
1150
1163
|
if (hasFieldGroup && dist.installationContract) {
|
|
1151
1164
|
// 从 distribution 的 installationContract 重新计算摘要
|
|
1152
|
-
const recomputedDigest =
|
|
1165
|
+
const recomputedDigest = digestDocument(dist.installationContract);
|
|
1153
1166
|
const actionDigest = action.parameters?.installationContractDigest;
|
|
1154
1167
|
const distDigest = dist.installationContractDigest;
|
|
1155
1168
|
|
package/src/core/run.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @module core/run
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { lstat, mkdir, readFile } from 'node:fs/promises';
|
|
16
16
|
import { realpathSync } from 'node:fs';
|
|
17
17
|
import { dirname, join, resolve, basename, relative, isAbsolute } from 'node:path';
|
|
18
18
|
import Ajv from 'ajv';
|
|
@@ -22,6 +22,7 @@ import { assertImmutablePlanAuthority, computePlanDigest } from './plan.mjs';
|
|
|
22
22
|
import { sha256Hex } from './digest.mjs';
|
|
23
23
|
import { ReleaseError, GATE_FAILED } from './errors.mjs';
|
|
24
24
|
import { readTrustedPackageResource } from './trusted-resource.mjs';
|
|
25
|
+
import { publishFileExclusive, HARNESS_ERROR_KINDS as INFLIGHT_KINDS } from './foundation-inflight.mjs';
|
|
25
26
|
|
|
26
27
|
const RELEASE_RUN_SCHEMA = JSON.parse((await readTrustedPackageResource(
|
|
27
28
|
'schemas/release-run.schema.json',
|
|
@@ -578,25 +579,18 @@ function sealRun(run) {
|
|
|
578
579
|
return sealed;
|
|
579
580
|
}
|
|
580
581
|
|
|
581
|
-
async function syncDirectory(dir) {
|
|
582
|
-
const handle = await open(dir, 'r');
|
|
583
|
-
try {
|
|
584
|
-
await handle.sync();
|
|
585
|
-
} finally {
|
|
586
|
-
await handle.close();
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
582
|
/**
|
|
591
583
|
* Write a run file atomically to disk.
|
|
592
584
|
*
|
|
593
585
|
* Validates the run against the schema before writing.
|
|
594
|
-
*
|
|
586
|
+
* Delegates to Foundation `publishFileExclusive` (temp + fsync + exclusive
|
|
587
|
+
* link + byte/mode/identity verification + directory fsync).
|
|
595
588
|
*
|
|
596
589
|
* **Exclusive-create semantics**: if a file already exists at `runPath`
|
|
597
590
|
* with different bytes, the write fails. This prevents a stale run from
|
|
598
591
|
* silently overwriting a newer run record. Writes are idempotent when
|
|
599
|
-
* the content is byte-identical
|
|
592
|
+
* the content is byte-identical (wrapper-supplied branch; the in-flight
|
|
593
|
+
* Foundation version has no same-bytes idempotency — G4 conflict-surface.md).
|
|
600
594
|
*
|
|
601
595
|
* @param {string} runPath - Absolute path to write the run to.
|
|
602
596
|
* @param {Object} run - The run object to write.
|
|
@@ -608,34 +602,26 @@ export async function writeRunAtomic(runPath, run) {
|
|
|
608
602
|
const sealed = sealRun(run);
|
|
609
603
|
const json = JSON.stringify(sealed, null, 2);
|
|
610
604
|
const dir = dirname(runPath);
|
|
611
|
-
const tmpPath = `${dir}/.release-run-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`;
|
|
612
605
|
await mkdir(dir, { recursive: true });
|
|
613
606
|
|
|
614
|
-
const handle = await open(tmpPath, 'wx', 0o600);
|
|
615
607
|
try {
|
|
616
|
-
await
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (error.code !== 'EEXIST') throw error;
|
|
628
|
-
const existing = await readFile(runPath, 'utf8');
|
|
629
|
-
if (existing !== json) {
|
|
630
|
-
throw new ReleaseError(
|
|
631
|
-
GATE_FAILED,
|
|
632
|
-
'run file already exists with different bytes; exclusive-create rejected overwrite',
|
|
633
|
-
{ runPath },
|
|
634
|
-
);
|
|
635
|
-
}
|
|
608
|
+
await publishFileExclusive(dir, basename(runPath), json, { mode: 0o600 });
|
|
609
|
+
} catch (cause) {
|
|
610
|
+
if (cause?.details?.kind === INFLIGHT_KINDS.EXCLUSIVE_PUBLISH_CONFLICT) {
|
|
611
|
+
// 同字节幂等:目标已存在且字节相同 → 幂等成功(fail-closed 无双路径)。
|
|
612
|
+
const existing = await readFile(runPath, 'utf8').catch(() => null);
|
|
613
|
+
if (existing === json) return Object.freeze(sealed);
|
|
614
|
+
throw new ReleaseError(
|
|
615
|
+
GATE_FAILED,
|
|
616
|
+
'run file already exists with different bytes; exclusive-create rejected overwrite',
|
|
617
|
+
{ runPath },
|
|
618
|
+
);
|
|
636
619
|
}
|
|
637
|
-
|
|
638
|
-
|
|
620
|
+
throw new ReleaseError(
|
|
621
|
+
GATE_FAILED,
|
|
622
|
+
`run file write failed: ${cause?.message ?? String(cause)}`,
|
|
623
|
+
{ runPath },
|
|
624
|
+
);
|
|
639
625
|
}
|
|
640
626
|
|
|
641
627
|
return Object.freeze(sealed);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { lstat, readFile, readdir } from 'node:fs/promises';
|
|
11
11
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
12
12
|
|
|
13
|
-
import { canonicalJson,
|
|
13
|
+
import { canonicalJson, digestDocument } from 'skill-family-contracts';
|
|
14
14
|
import { GATE_FAILED, ReleaseError } from './errors.mjs';
|
|
15
15
|
import { computeFrozenSnapshot } from '../snapshot/frozen.mjs';
|
|
16
16
|
|
|
@@ -258,6 +258,8 @@ export function createSkillResourceClosureReceipt(result, identity = {}) {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
export function assertSkillResourceClosureReceipt(expected, observed, label = 'release unit') {
|
|
261
|
+
// 收据对象均为纯 JSON(JSON.parse 派生或固定字段投影),直接委托 Foundation
|
|
262
|
+
// contracts 严格 canonicalJson 比较(非 JSON 输入 fail-closed,抛 TypeError)。
|
|
261
263
|
if (canonicalJson(expected) !== canonicalJson(observed)) {
|
|
262
264
|
throw new ReleaseError(
|
|
263
265
|
GATE_FAILED,
|
|
@@ -319,9 +321,9 @@ export async function checkSkillResourceClosure({
|
|
|
319
321
|
})).digest;
|
|
320
322
|
} catch (error) {
|
|
321
323
|
snapshotError = error;
|
|
322
|
-
inputDigest =
|
|
324
|
+
inputDigest = digestDocument({
|
|
323
325
|
invalidTree: error.message,
|
|
324
|
-
})
|
|
326
|
+
});
|
|
325
327
|
}
|
|
326
328
|
const skillPaths = await discoverSkills(scanRoot);
|
|
327
329
|
const findings = [];
|
|
@@ -426,6 +428,6 @@ export async function checkSkillResourceClosure({
|
|
|
426
428
|
sourceOnlyCount,
|
|
427
429
|
findings,
|
|
428
430
|
};
|
|
429
|
-
result.receiptDigest =
|
|
431
|
+
result.receiptDigest = digestDocument(receiptProjection(result));
|
|
430
432
|
return result;
|
|
431
433
|
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read package-owned runtime resources without following untrusted links.
|
|
3
3
|
* Installed adapters use this for schemas and native manifests.
|
|
4
|
+
*
|
|
5
|
+
* 机制已委托 Foundation(skill-family-foundation-adoption-20260815 节点 1.6):
|
|
6
|
+
* - 异步变体 `readTrustedPackageResource`:路径包含三层校验(词法/符号链接/
|
|
7
|
+
* realpath 逃逸)委托 `resolveContained`,读取委托 `readFileContained`;
|
|
8
|
+
* 本地保留域策略前置校验 `assertStat`(拒绝任何符号链接、非常规文件与
|
|
9
|
+
* 硬链接),失败原因映射保持原语义。
|
|
10
|
+
* - 同步变体 `readTrustedPackageResourceSync`:Foundation 无同步路径 API,
|
|
11
|
+
* 词法分类委托同步纯函数 `classifyPathInput`,其余本地同步实现保留
|
|
12
|
+
* (残留,见 1.6 run-report 剩余风险)。
|
|
4
13
|
*/
|
|
5
14
|
|
|
6
15
|
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
7
|
-
import { lstat
|
|
16
|
+
import { lstat } from 'node:fs/promises';
|
|
8
17
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
9
18
|
|
|
19
|
+
import { classifyPathInput, readFileContained, resolveContained } from 'skill-family-harness-node';
|
|
20
|
+
|
|
10
21
|
import { CONFIG_INVALID, ReleaseError } from './errors.mjs';
|
|
11
22
|
import { PKG_ROOT } from './pkg-root.mjs';
|
|
12
23
|
|
|
@@ -18,6 +29,8 @@ function fail(code, resource, reason) {
|
|
|
18
29
|
);
|
|
19
30
|
}
|
|
20
31
|
|
|
32
|
+
// ---- 同步变体保留的本地实现(Foundation 无同步路径 API,文档化残留) ----
|
|
33
|
+
|
|
21
34
|
function lexicalPath(resource, code) {
|
|
22
35
|
if (typeof resource !== 'string' || resource.length === 0 || isAbsolute(resource)) {
|
|
23
36
|
fail(code, String(resource), 'INVALID_RESOURCE_PATH');
|
|
@@ -43,7 +56,44 @@ function assertPhysicalContainment(physicalRoot, physicalPath, resource, code) {
|
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
|
|
59
|
+
// ---- Foundation 机制错误 → 本地失败语义映射 ----
|
|
60
|
+
|
|
61
|
+
/** 把 Foundation HarnessError(SFC2004 + details.kind)映射为本地失败原因。 */
|
|
62
|
+
function harnessKindToReason(kind) {
|
|
63
|
+
switch (kind) {
|
|
64
|
+
case 'path-traversal':
|
|
65
|
+
return 'RESOURCE_PATH_ESCAPE';
|
|
66
|
+
case 'symlink-escape':
|
|
67
|
+
// 本地语义:lstat 前置校验对任何符号链接一律报 SYMLINK(不区分指向)。
|
|
68
|
+
// Foundation 对指向包外的符号链接先抛 symlink-escape,映射回 SYMLINK
|
|
69
|
+
// 保持原失败原因;包内符号链接由本地 assertStat 以 SYMLINK 拦截。
|
|
70
|
+
return 'SYMLINK';
|
|
71
|
+
case 'realpath-escape':
|
|
72
|
+
return 'PHYSICAL_PATH_ESCAPE';
|
|
73
|
+
case 'missing-resource':
|
|
74
|
+
return 'MISSING';
|
|
75
|
+
case 'read-failed':
|
|
76
|
+
return 'READ_FAILED';
|
|
77
|
+
default:
|
|
78
|
+
// invalid-path / absolute-path / windows-drive-path / unc-path /
|
|
79
|
+
// windows-path / invalid-root 等统一归入无效资源路径。
|
|
80
|
+
return 'INVALID_RESOURCE_PATH';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function mapHarnessError(cause, resource, code) {
|
|
85
|
+
if (cause && cause.code === 'SFC2004' && cause.details && typeof cause.details.kind === 'string') {
|
|
86
|
+
fail(code, resource, harnessKindToReason(cause.details.kind));
|
|
87
|
+
}
|
|
88
|
+
throw cause;
|
|
89
|
+
}
|
|
90
|
+
|
|
46
91
|
export function readTrustedPackageResourceSync(resource, { code = CONFIG_INVALID } = {}) {
|
|
92
|
+
// 词法分类委托 Foundation classifyPathInput(同步纯函数;比本地词法检查
|
|
93
|
+
// 更严格地拒绝反斜杠/NUL/UNC/盘符相对等跨平台歧义输入,fail-closed)。
|
|
94
|
+
const classification = classifyPathInput(resource, process.platform);
|
|
95
|
+
if (!classification.ok) fail(code, String(resource), 'INVALID_RESOURCE_PATH');
|
|
96
|
+
|
|
47
97
|
const path = lexicalPath(resource, code);
|
|
48
98
|
let stat;
|
|
49
99
|
try {
|
|
@@ -70,27 +120,26 @@ export function readTrustedPackageResourceSync(resource, { code = CONFIG_INVALID
|
|
|
70
120
|
}
|
|
71
121
|
|
|
72
122
|
export async function readTrustedPackageResource(resource, { code = CONFIG_INVALID } = {}) {
|
|
73
|
-
|
|
123
|
+
// 机制委托:resolveContained(词法/符号链接/realpath 三层包含校验)。
|
|
124
|
+
let target;
|
|
125
|
+
try {
|
|
126
|
+
target = await resolveContained(PKG_ROOT, resource);
|
|
127
|
+
} catch (cause) {
|
|
128
|
+
mapHarnessError(cause, resource, code);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 本地域策略前置:拒绝任何符号链接(含指向包内目标的)、非常规文件与硬链接。
|
|
74
132
|
let stat;
|
|
75
133
|
try {
|
|
76
|
-
stat = await lstat(
|
|
134
|
+
stat = await lstat(target);
|
|
77
135
|
} catch {
|
|
78
136
|
fail(code, resource, 'MISSING');
|
|
79
137
|
}
|
|
80
138
|
assertStat(stat, resource, code);
|
|
81
139
|
|
|
82
|
-
let physicalRoot;
|
|
83
|
-
let physicalPath;
|
|
84
140
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
fail(code, resource, 'REALPATH_FAILED');
|
|
89
|
-
}
|
|
90
|
-
assertPhysicalContainment(physicalRoot, physicalPath, resource, code);
|
|
91
|
-
try {
|
|
92
|
-
return await readFile(physicalPath);
|
|
93
|
-
} catch {
|
|
94
|
-
fail(code, resource, 'READ_FAILED');
|
|
141
|
+
return await readFileContained(PKG_ROOT, resource);
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
mapHarnessError(cause, resource, code);
|
|
95
144
|
}
|
|
96
145
|
}
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
import { readdir, mkdir, lstat, realpath, open, rm } from 'node:fs/promises';
|
|
22
22
|
import { constants as fsConstants } from 'node:fs';
|
|
23
23
|
import { join, dirname, relative, isAbsolute, sep, resolve, posix as pathPosix } from 'node:path';
|
|
24
|
-
import { createHash } from 'node:crypto';
|
|
25
24
|
|
|
26
25
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
26
|
+
import { digestDocument } from 'skill-family-contracts';
|
|
27
|
+
import { digestBytes } from 'skill-family-harness-node';
|
|
27
28
|
import { PLATFORMS as PLATFORM_REGISTRY } from '../platforms/registry.mjs';
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -186,11 +187,14 @@ async function ensureTrustedOutputDirectory(rootPath, rootIdentity, relativeDir,
|
|
|
186
187
|
* @returns {string}
|
|
187
188
|
*/
|
|
188
189
|
export function computeBuildAdaptersDigest(sourceBytes) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
// 多段哈希(源字节 + node 版本 + 固定 locale/timezone)等价于对拼接字节
|
|
191
|
+
// 的 sha256,委托 Foundation digestBytes 实现,字节逐位一致。
|
|
192
|
+
const bytes = Buffer.concat([
|
|
193
|
+
...sourceBytes,
|
|
194
|
+
Buffer.from(`node:${process.version}`),
|
|
195
|
+
Buffer.from('locale:en-US timezone:UTC'),
|
|
196
|
+
]);
|
|
197
|
+
return `sha256:${digestBytes(bytes)}`;
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
// Platform definitions, derived from the platform registry (the single source
|
|
@@ -488,8 +492,21 @@ async function generateAdapterFiles(platform, skills, templateJson, root, market
|
|
|
488
492
|
});
|
|
489
493
|
}
|
|
490
494
|
|
|
491
|
-
// Copy bin/ entry point and bundle (text files)
|
|
492
|
-
|
|
495
|
+
// Copy bin/ entry point and bundle (text files). The self-contained
|
|
496
|
+
// bundle ships runtime sidecars next to it (JSON resources read via
|
|
497
|
+
// new URL(<rel>, import.meta.url) by inlined Foundation modules, e.g.
|
|
498
|
+
// error-codes.json); enumerate the directory so every sidecar is
|
|
499
|
+
// replicated deterministically. The source CLI (release-skill-cli.mjs)
|
|
500
|
+
// is never shipped into adapters. The wrapper and the bundle are
|
|
501
|
+
// required entries: a missing one fails closed before any write.
|
|
502
|
+
const binFiles = (await readdir(join(root, 'bin')))
|
|
503
|
+
.filter((name) => name !== 'release-skill-cli.mjs')
|
|
504
|
+
.sort();
|
|
505
|
+
for (const required of ['release-skill.mjs', 'release-skill.bundle.mjs']) {
|
|
506
|
+
if (!binFiles.includes(required)) {
|
|
507
|
+
throw new Error(`SECURITY: required bundle entry is missing: bin/${required}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
493
510
|
for (const name of binFiles) {
|
|
494
511
|
const srcPath = join(root, 'bin', name);
|
|
495
512
|
const content = await readTrustedFile(srcPath, root, `bin/${name}`);
|
|
@@ -752,8 +769,9 @@ export async function produceBuildAdapters({ inputs, output, outputRoots, platfo
|
|
|
752
769
|
* @returns {string}
|
|
753
770
|
*/
|
|
754
771
|
export function digestEntryOutputs(entries) {
|
|
772
|
+
// 纯 JSON 投影(platform 条件展开 + 定字段),直接委托 Foundation digestDocument。
|
|
755
773
|
const canonical = entries.map(({ platform, path, type, mode, sha256, size }) => ({
|
|
756
774
|
...(platform ? { platform } : {}), path, type, mode, size, sha256,
|
|
757
775
|
}));
|
|
758
|
-
return `sha256:${
|
|
776
|
+
return `sha256:${digestDocument(canonical)}`;
|
|
759
777
|
}
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
import { readFile, writeFile, readdir, stat, mkdir } from 'node:fs/promises';
|
|
15
15
|
import { join, relative } from 'node:path';
|
|
16
|
-
import { createHash } from 'node:crypto';
|
|
17
16
|
|
|
18
17
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
18
|
+
import { digestDocument } from 'skill-family-contracts';
|
|
19
|
+
import { digestBytes } from 'skill-family-harness-node';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Compute an implementation digest from the source bytes of this module.
|
|
@@ -24,11 +25,14 @@ import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
|
24
25
|
* @returns {string}
|
|
25
26
|
*/
|
|
26
27
|
export function computeRenderDigest(sourceBytes) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
// 多段哈希(源字节 + node 版本 + 固定 locale/timezone)等价于对拼接字节
|
|
29
|
+
// 的 sha256,委托 Foundation digestBytes 实现,字节逐位一致。
|
|
30
|
+
const bytes = Buffer.concat([
|
|
31
|
+
...sourceBytes,
|
|
32
|
+
Buffer.from(`node:${process.version}`),
|
|
33
|
+
Buffer.from('locale:en-US timezone:UTC'),
|
|
34
|
+
]);
|
|
35
|
+
return `sha256:${digestBytes(bytes)}`;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -145,8 +149,9 @@ export async function produceRenderPublicAssets({ inputs, output } = {}) {
|
|
|
145
149
|
* @returns {string}
|
|
146
150
|
*/
|
|
147
151
|
export function digestEntryOutputs(entries) {
|
|
152
|
+
// 纯 JSON 投影(定字段),直接委托 Foundation digestDocument。
|
|
148
153
|
const canonical = entries.map(({ path, type, mode, sha256, size }) => ({
|
|
149
154
|
path, type, mode, size, sha256,
|
|
150
155
|
}));
|
|
151
|
-
return `sha256:${
|
|
156
|
+
return `sha256:${digestDocument(canonical)}`;
|
|
152
157
|
}
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
-
import { createHash } from 'node:crypto';
|
|
15
14
|
|
|
16
15
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
16
|
+
import { digestDocument } from 'skill-family-contracts';
|
|
17
|
+
import { digestBytes } from 'skill-family-harness-node';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Compute an implementation digest from the source bytes of this module.
|
|
@@ -22,11 +23,14 @@ import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
|
22
23
|
* @returns {string}
|
|
23
24
|
*/
|
|
24
25
|
export function computeSyncSkillsDigest(sourceBytes) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// 多段哈希(源字节 + node 版本 + 固定 locale/timezone)等价于对拼接字节
|
|
27
|
+
// 的 sha256,委托 Foundation digestBytes 实现,字节逐位一致。
|
|
28
|
+
const bytes = Buffer.concat([
|
|
29
|
+
...sourceBytes,
|
|
30
|
+
Buffer.from(`node:${process.version}`),
|
|
31
|
+
Buffer.from('locale:en-US timezone:UTC'),
|
|
32
|
+
]);
|
|
33
|
+
return `sha256:${digestBytes(bytes)}`;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/**
|
|
@@ -89,8 +93,9 @@ export async function produceSyncSkills({ inputs, output } = {}) {
|
|
|
89
93
|
* @returns {string}
|
|
90
94
|
*/
|
|
91
95
|
export function digestEntryOutputs(entries) {
|
|
96
|
+
// 纯 JSON 投影(定字段),直接委托 Foundation digestDocument。
|
|
92
97
|
const canonical = entries.map(({ path, type, mode, sha256, size }) => ({
|
|
93
98
|
path, type, mode, size, sha256,
|
|
94
99
|
}));
|
|
95
|
-
return `sha256:${
|
|
100
|
+
return `sha256:${digestDocument(canonical)}`;
|
|
96
101
|
}
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
import { lstat, readFile, mkdir, readdir, realpath, chmod as fsChmod } from 'node:fs/promises';
|
|
25
25
|
import { open as fsOpen } from 'node:fs/promises';
|
|
26
26
|
import { relative, resolve, dirname, sep as pathSep, isAbsolute } from 'node:path';
|
|
27
|
-
import { posix } from 'node:path';
|
|
27
|
+
import { posix, win32 } from 'node:path';
|
|
28
28
|
import { constants as fsConstants } from 'node:fs';
|
|
29
29
|
import { createHash } from 'node:crypto';
|
|
30
|
+
import { classifyPathInput } from 'skill-family-harness-node';
|
|
30
31
|
import {
|
|
31
32
|
ReleaseError,
|
|
32
33
|
PUBLIC_FILE_MISSING,
|
|
@@ -81,7 +82,14 @@ const MAX_PATH_LENGTH = 4096;
|
|
|
81
82
|
*/
|
|
82
83
|
export function _isContainedWith(relativeFn, isAbsoluteFn, root, candidate, relPathSep = pathSep) {
|
|
83
84
|
const rel = relativeFn(root, candidate);
|
|
84
|
-
|
|
85
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${relPathSep}`) || isAbsoluteFn(rel)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// 词法分类委托 Foundation classifyPathInput(同步纯函数):对相对路径做
|
|
89
|
+
// 跨平台歧义输入 fail-closed(反斜杠分隔符/NUL/UNC/盘符相对/绝对路径)。
|
|
90
|
+
// 平台从注入的 relativeFn 推导,保持纯同步、无文件系统访问的既有契约。
|
|
91
|
+
const platform = relativeFn === win32.relative ? 'win32' : 'posix';
|
|
92
|
+
return classifyPathInput(rel, platform).ok === true;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
/**
|