release-skill 0.4.2 → 0.5.0

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 (86) 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 +26 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +17 -6
  10. package/README.zh-CN.md +16 -5
  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 +165 -0
  16. package/adapters/claude/bin/release-skill.bundle.mjs +3838 -1445
  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 +165 -0
  26. package/adapters/codex/bin/release-skill.bundle.mjs +3838 -1445
  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 +165 -0
  36. package/adapters/kimi/bin/release-skill.bundle.mjs +3838 -1445
  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 +165 -0
  46. package/adapters/workbuddy/bin/release-skill.bundle.mjs +3838 -1445
  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 +165 -0
  55. package/bin/release-skill.bundle.mjs +3838 -1445
  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/artifacts/entry.mjs +4 -1
  70. package/src/artifacts/producer-registry.mjs +9 -6
  71. package/src/artifacts/project-lock.mjs +324 -214
  72. package/src/commands/assess.mjs +60 -2
  73. package/src/commands/attest.mjs +11 -0
  74. package/src/core/approval.mjs +5 -2
  75. package/src/core/digest.mjs +47 -12
  76. package/src/core/foundation-inflight.mjs +39 -0
  77. package/src/core/hook-cache.mjs +3 -0
  78. package/src/core/installation-contract.mjs +4 -2
  79. package/src/core/plan.mjs +30 -27
  80. package/src/core/run.mjs +22 -36
  81. package/src/core/skill-resource-closure.mjs +6 -4
  82. package/src/core/trusted-resource.mjs +64 -15
  83. package/src/producers/build-adapters.mjs +27 -9
  84. package/src/producers/render-public-assets.mjs +12 -7
  85. package/src/producers/sync-skills.mjs +12 -7
  86. package/src/snapshot/public-map.mjs +10 -2
@@ -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
  /**