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.
Files changed (91) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +44 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +10 -7
  10. package/README.zh-CN.md +10 -7
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/error-codes.json +154 -0
  14. package/adapters/claude/bin/kernel-protocol.json +60 -0
  15. package/adapters/claude/bin/registry.json +179 -0
  16. package/adapters/claude/bin/release-skill.bundle.mjs +37609 -23014
  17. package/adapters/claude/bin/rules.json +104 -0
  18. package/adapters/claude/skills/release-assess/SKILL.md +2 -0
  19. package/adapters/claude/skills/release-help/SKILL.md +3 -1
  20. package/adapters/claude/skills/release-prepare/SKILL.md +2 -2
  21. package/adapters/claude/skills/release-verify/SKILL.md +2 -0
  22. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  23. package/adapters/codex/bin/error-codes.json +154 -0
  24. package/adapters/codex/bin/kernel-protocol.json +60 -0
  25. package/adapters/codex/bin/registry.json +179 -0
  26. package/adapters/codex/bin/release-skill.bundle.mjs +37609 -23014
  27. package/adapters/codex/bin/rules.json +104 -0
  28. package/adapters/codex/skills/release-assess/SKILL.md +2 -0
  29. package/adapters/codex/skills/release-help/SKILL.md +3 -1
  30. package/adapters/codex/skills/release-prepare/SKILL.md +2 -2
  31. package/adapters/codex/skills/release-verify/SKILL.md +2 -0
  32. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  33. package/adapters/kimi/bin/error-codes.json +154 -0
  34. package/adapters/kimi/bin/kernel-protocol.json +60 -0
  35. package/adapters/kimi/bin/registry.json +179 -0
  36. package/adapters/kimi/bin/release-skill.bundle.mjs +37609 -23014
  37. package/adapters/kimi/bin/rules.json +104 -0
  38. package/adapters/kimi/skills/release-assess/SKILL.md +2 -0
  39. package/adapters/kimi/skills/release-help/SKILL.md +3 -1
  40. package/adapters/kimi/skills/release-prepare/SKILL.md +2 -2
  41. package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
  42. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  43. package/adapters/workbuddy/bin/error-codes.json +154 -0
  44. package/adapters/workbuddy/bin/kernel-protocol.json +60 -0
  45. package/adapters/workbuddy/bin/registry.json +179 -0
  46. package/adapters/workbuddy/bin/release-skill.bundle.mjs +37609 -23014
  47. package/adapters/workbuddy/bin/rules.json +104 -0
  48. package/adapters/workbuddy/skills/release-assess/SKILL.md +2 -0
  49. package/adapters/workbuddy/skills/release-help/SKILL.md +3 -1
  50. package/adapters/workbuddy/skills/release-prepare/SKILL.md +2 -2
  51. package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
  52. package/bin/error-codes.json +154 -0
  53. package/bin/kernel-protocol.json +60 -0
  54. package/bin/registry.json +179 -0
  55. package/bin/release-skill.bundle.mjs +37609 -23014
  56. package/bin/rules.json +104 -0
  57. package/package.json +3 -1
  58. package/scripts/build-bundle.mjs +159 -5
  59. package/scripts/sync-public-files.mjs +5 -0
  60. package/skills/release-assess/SKILL.md +2 -0
  61. package/skills/release-help/SKILL.md +3 -1
  62. package/skills/release-prepare/SKILL.md +2 -2
  63. package/skills/release-verify/SKILL.md +2 -0
  64. package/skills-src/release-assess/SKILL.md +2 -0
  65. package/skills-src/release-help/SKILL.md +3 -1
  66. package/skills-src/release-prepare/SKILL.md +2 -2
  67. package/skills-src/release-verify/SKILL.md +2 -0
  68. package/src/adapters/contract.mjs +10 -10
  69. package/src/adapters/plugin-marketplace.mjs +48 -4
  70. package/src/adapters/push-snapshot.mjs +12 -4
  71. package/src/artifacts/entry.mjs +4 -1
  72. package/src/artifacts/producer-registry.mjs +9 -6
  73. package/src/artifacts/project-lock.mjs +324 -214
  74. package/src/commands/assess.mjs +231 -2
  75. package/src/commands/attest.mjs +11 -0
  76. package/src/commands/hooks.mjs +6 -1
  77. package/src/commands/prepare.mjs +83 -1
  78. package/src/core/approval.mjs +5 -2
  79. package/src/core/baseline.mjs +11 -1
  80. package/src/core/digest.mjs +47 -12
  81. package/src/core/foundation-inflight.mjs +46 -0
  82. package/src/core/hook-cache.mjs +3 -0
  83. package/src/core/installation-contract.mjs +4 -2
  84. package/src/core/plan.mjs +42 -29
  85. package/src/core/run.mjs +22 -36
  86. package/src/core/skill-resource-closure.mjs +6 -4
  87. package/src/core/trusted-resource.mjs +64 -15
  88. package/src/producers/build-adapters.mjs +27 -9
  89. package/src/producers/render-public-assets.mjs +12 -7
  90. package/src/producers/sync-skills.mjs +12 -7
  91. 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, writeFile, rename, mkdir, open, link, unlink, lstat } from 'node:fs/promises';
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 to temp file in the same directory
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
- const tmpPath = `${dir}/.release-plan-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`;
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. A temporary
307
- * file is fsynced and atomically linked into place so concurrent prepares can
308
- * never replace an authority that another process already created.
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 link(tmpPath, planPath);
338
- } catch (error) {
339
- if (error.code !== 'EEXIST') throw error;
340
- const existing = await readFile(planPath, 'utf8');
341
- if (existing !== json) {
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
- } finally {
371
- await unlink(tmpPath).catch(() => {});
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 (frozen.parentCommit) {
535
- failures.push(`unit "${unitId}" create-release-branch must not freeze a parentCommit`);
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 = sha256Hex(canonicalJson(dist.installationContract));
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 { link, lstat, mkdir, open, readFile, unlink } from 'node:fs/promises';
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
- * Uses temp-file + rename for atomicity.
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 handle.writeFile(json, 'utf8');
617
- await handle.sync();
618
- } finally {
619
- await handle.close();
620
- }
621
-
622
- try {
623
- try {
624
- await link(tmpPath, runPath);
625
- await syncDirectory(dir);
626
- } catch (error) {
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
- } finally {
638
- await unlink(tmpPath).catch(() => {});
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, sha256Hex } from './digest.mjs';
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 = sha256Hex(canonicalJson({
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 = sha256Hex(canonicalJson(receiptProjection(result)));
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, readFile, realpath } from 'node:fs/promises';
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
- const path = lexicalPath(resource, code);
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(path);
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
- physicalRoot = await realpath(PKG_ROOT);
86
- physicalPath = await realpath(path);
87
- } catch {
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
- const h = createHash('sha256');
190
- for (const buf of sourceBytes) h.update(buf);
191
- h.update(`node:${process.version}`);
192
- h.update('locale:en-US timezone:UTC');
193
- return `sha256:${h.digest('hex')}`;
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
- const binFiles = ['release-skill.mjs', 'release-skill.bundle.mjs'];
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:${sha256Hex(canonicalJson(canonical))}`;
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
- const h = createHash('sha256');
28
- for (const buf of sourceBytes) h.update(buf);
29
- h.update(`node:${process.version}`);
30
- h.update('locale:en-US timezone:UTC');
31
- return `sha256:${h.digest('hex')}`;
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:${sha256Hex(canonicalJson(canonical))}`;
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
- const h = createHash('sha256');
26
- for (const buf of sourceBytes) h.update(buf);
27
- h.update(`node:${process.version}`);
28
- h.update('locale:en-US timezone:UTC');
29
- return `sha256:${h.digest('hex')}`;
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:${sha256Hex(canonicalJson(canonical))}`;
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
- return rel !== '' && rel !== '..' && !rel.startsWith(`..${relPathSep}`) && !isAbsoluteFn(rel);
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
  /**