skill-family-harness-node 0.8.2 → 0.8.3
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/CHANGELOG.md +21 -0
- package/CHANGELOG.zh-CN.md +21 -0
- package/README.md +12 -12
- package/README.zh-CN.md +12 -12
- package/package.json +2 -2
- package/release-notes/0.8.3.yaml +23 -0
- package/src/paths.mjs +48 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
<!-- release-skill:changelog:start version=0.8.3 locale=en baseline=sha256:449943bedf3698888fec4dcb09a0460f337b9406d5258946a3d3ffcd23143b8d -->
|
|
4
|
+
## [0.8.3] - 2026-08-23
|
|
5
|
+
|
|
6
|
+
Path containment now tolerates one exact anchor-removal race while preserving fail-closed escape checks.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- A second ENOENT and every non-ENOENT failure remain closed; symlink swaps, out-of-root targets, and all existing containment checks remain rejected.
|
|
11
|
+
- Keeps the hook private to the paths module test surface and adds no public export, general retry policy, lock layer, ledger, or runner.
|
|
12
|
+
- Moves the package version to 0.8.3 together with Contracts and Engineering Kit.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- When realpath of the selected existing anchor fails with ENOENT because another process removed it, resolveContained recomputes the deepest existing ancestor exactly once.
|
|
17
|
+
|
|
18
|
+
### Upgrade Notes
|
|
19
|
+
|
|
20
|
+
Consumers must pin all three Foundation packages to exactly 0.8.3. Concurrent lock acquisition can now survive the current owner removing the selected lock-file anchor; no Harness API migration is required.
|
|
21
|
+
<!-- release-skill:changelog:end version=0.8.3 locale=en -->
|
|
22
|
+
|
|
23
|
+
|
|
3
24
|
<!-- release-skill:changelog:start version=0.8.2 locale=en baseline=sha256:af051a6925c7d1f46bef6fbe367aceea8685b4a916629af4e69334e73014e3a7 -->
|
|
4
25
|
## [0.8.2] - 2026-08-23
|
|
5
26
|
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
+
<!-- release-skill:changelog:start version=0.8.3 locale=zh-CN baseline=sha256:b84695a286aca7c1a9f90844f8682cde07d32276cebc11583568bc5ef11753fc -->
|
|
4
|
+
## [0.8.3] - 2026-08-23
|
|
5
|
+
|
|
6
|
+
路径收容现在可处理一次精确的锚点并发删除,同时保持越界检查失败关闭。
|
|
7
|
+
|
|
8
|
+
### 变更
|
|
9
|
+
|
|
10
|
+
- 第二次 ENOENT 与所有非 ENOENT 错误仍失败关闭;符号链接替换、根目录外目标和既有收容检查仍全部拒绝。
|
|
11
|
+
- 测试钩子只留在 paths 模块的测试表面,不新增公共导出、通用重试策略、锁层、账本或 runner。
|
|
12
|
+
- 包版本与 Contracts、Engineering Kit 一同升至 0.8.3。
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
|
|
16
|
+
- 当另一进程删除已选锚点,导致该锚点的 realpath 返回 ENOENT 时,resolveContained 仅重新计算一次最深既存祖先。
|
|
17
|
+
|
|
18
|
+
### 升级说明
|
|
19
|
+
|
|
20
|
+
消费者必须把三个 Foundation 包精确锁定到 0.8.3。并发获取锁时,即使当前持有者删除已选锁文件锚点,也能完成一次安全重求;Harness API 无需迁移。
|
|
21
|
+
<!-- release-skill:changelog:end version=0.8.3 locale=zh-CN -->
|
|
22
|
+
|
|
23
|
+
|
|
3
24
|
<!-- release-skill:changelog:start version=0.8.2 locale=zh-CN baseline=sha256:748d79d51dcabe6193332e60ec69a6eb26fd914ad3555f6e924b78db71016192 -->
|
|
4
25
|
## [0.8.2] - 2026-08-23
|
|
5
26
|
|
package/README.md
CHANGED
|
@@ -4,28 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
# skill-family-harness-node
|
|
6
6
|
|
|
7
|
-
<!-- release-skill:release-version: 0.8.
|
|
7
|
+
<!-- release-skill:release-version: 0.8.3 -->
|
|
8
8
|
|
|
9
9
|
The **single default Node implementation** of the Contracts mechanism protocol. This is a thin runtime: it only implements the mechanism protocol, introduces no business semantics, and does not provide a second-language implementation.
|
|
10
10
|
|
|
11
11
|
<!-- release-skill:managed:start id=latest-release -->
|
|
12
|
-
**0.8.
|
|
12
|
+
**0.8.3** (2026-08-23)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Path containment now tolerates one exact anchor-removal race while preserving fail-closed escape checks.
|
|
15
15
|
|
|
16
|
-
**
|
|
16
|
+
**Changed**
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- A second ENOENT and every non-ENOENT failure remain closed; symlink swaps, out-of-root targets, and all existing containment checks remain rejected.
|
|
19
|
+
- Keeps the hook private to the paths module test surface and adds no public export, general retry policy, lock layer, ledger, or runner.
|
|
20
|
+
- Moves the package version to 0.8.3 together with Contracts and Engineering Kit.
|
|
20
21
|
|
|
21
|
-
**
|
|
22
|
+
**Fixed**
|
|
22
23
|
|
|
23
|
-
-
|
|
24
|
-
- Moves the package version to 0.8.2 together with Contracts and Engineering Kit.
|
|
24
|
+
- When realpath of the selected existing anchor fails with ENOENT because another process removed it, resolveContained recomputes the deepest existing ancestor exactly once.
|
|
25
25
|
|
|
26
26
|
**Upgrade Notes**
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Consumers must pin all three Foundation packages to exactly 0.8.3. Concurrent lock acquisition can now survive the current owner removing the selected lock-file anchor; no Harness API migration is required.
|
|
29
29
|
<!-- release-skill:managed:end id=latest-release -->
|
|
30
30
|
|
|
31
31
|
## Problem It Solves
|
|
@@ -39,14 +39,14 @@ The Harness consumes `skill-family-contracts` (a workspace dependency), reusing
|
|
|
39
39
|
## Installation and Minimal Example
|
|
40
40
|
|
|
41
41
|
```sh
|
|
42
|
-
npm install skill-family-harness-node@0.8.
|
|
42
|
+
npm install skill-family-harness-node@0.8.3
|
|
43
43
|
npm info skill-family-harness-node --help
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
The minimal example shows validating a contract document inside Node:
|
|
47
47
|
|
|
48
48
|
```js
|
|
49
|
-
// Run from an empty directory: npm install skill-family-harness-node@0.8.
|
|
49
|
+
// Run from an empty directory: npm install skill-family-harness-node@0.8.3
|
|
50
50
|
import { validateContractDocument } from "skill-family-harness-node";
|
|
51
51
|
|
|
52
52
|
const document = {
|
package/README.zh-CN.md
CHANGED
|
@@ -5,28 +5,28 @@
|
|
|
5
5
|
|
|
6
6
|
# skill-family-harness-node
|
|
7
7
|
|
|
8
|
-
<!-- release-skill:release-version: 0.8.
|
|
8
|
+
<!-- release-skill:release-version: 0.8.3 -->
|
|
9
9
|
|
|
10
10
|
Contracts 机制协议的**唯一默认 Node 实现**。这是一个薄运行时(thin runtime):只实现机制协议,不引入业务语义,不做第二语言实现。
|
|
11
11
|
|
|
12
12
|
<!-- release-skill:managed:start id=latest-release -->
|
|
13
|
-
**0.8.
|
|
13
|
+
**0.8.3** (2026-08-23)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
路径收容现在可处理一次精确的锚点并发删除,同时保持越界检查失败关闭。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
**变更**
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
19
|
+
- 第二次 ENOENT 与所有非 ENOENT 错误仍失败关闭;符号链接替换、根目录外目标和既有收容检查仍全部拒绝。
|
|
20
|
+
- 测试钩子只留在 paths 模块的测试表面,不新增公共导出、通用重试策略、锁层、账本或 runner。
|
|
21
|
+
- 包版本与 Contracts、Engineering Kit 一同升至 0.8.3。
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
**修复**
|
|
23
24
|
|
|
24
|
-
-
|
|
25
|
-
- 包版本与 Contracts、Engineering Kit 一同升至 0.8.2。
|
|
25
|
+
- 当另一进程删除已选锚点,导致该锚点的 realpath 返回 ENOENT 时,resolveContained 仅重新计算一次最深既存祖先。
|
|
26
26
|
|
|
27
27
|
**升级说明**
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
消费者必须把三个 Foundation 包精确锁定到 0.8.3。并发获取锁时,即使当前持有者删除已选锁文件锚点,也能完成一次安全重求;Harness API 无需迁移。
|
|
30
30
|
<!-- release-skill:managed:end id=latest-release -->
|
|
31
31
|
|
|
32
32
|
## 解决的问题
|
|
@@ -40,14 +40,14 @@ Harness 消费 `skill-family-contracts`(工作区依赖),复用其方言
|
|
|
40
40
|
## 安装和最小示例
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
npm install skill-family-harness-node@0.8.
|
|
43
|
+
npm install skill-family-harness-node@0.8.3
|
|
44
44
|
npm info skill-family-harness-node --help
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
最小示例演示在 Node 内校验一份契约文档:
|
|
48
48
|
|
|
49
49
|
```js
|
|
50
|
-
// 从空目录运行:npm install skill-family-harness-node@0.8.
|
|
50
|
+
// 从空目录运行:npm install skill-family-harness-node@0.8.3
|
|
51
51
|
import { validateContractDocument } from "skill-family-harness-node";
|
|
52
52
|
|
|
53
53
|
const document = {
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@pnpm/lockfile.fs": "1001.1.35",
|
|
12
12
|
"ipaddr.js": "2.5.0",
|
|
13
|
-
"skill-family-contracts": "0.8.
|
|
13
|
+
"skill-family-contracts": "0.8.3",
|
|
14
14
|
"yaml": "2.9.0"
|
|
15
15
|
},
|
|
16
16
|
"description": "Thin Node.js mechanism runtime for Skill Family engineering contracts.",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"url": "https://github.com/ifoohoo/skill-family-harness-node.git"
|
|
46
46
|
},
|
|
47
47
|
"type": "module",
|
|
48
|
-
"version": "0.8.
|
|
48
|
+
"version": "0.8.3",
|
|
49
49
|
"scripts": {
|
|
50
50
|
"check": "node --test",
|
|
51
51
|
"test": "node --test"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: 0.8.3
|
|
2
|
+
date: 2026-08-23
|
|
3
|
+
locales:
|
|
4
|
+
en:
|
|
5
|
+
summary: Path containment now tolerates one exact anchor-removal race while preserving fail-closed escape checks.
|
|
6
|
+
changes:
|
|
7
|
+
fixed:
|
|
8
|
+
- When realpath of the selected existing anchor fails with ENOENT because another process removed it, resolveContained recomputes the deepest existing ancestor exactly once.
|
|
9
|
+
changed:
|
|
10
|
+
- A second ENOENT and every non-ENOENT failure remain closed; symlink swaps, out-of-root targets, and all existing containment checks remain rejected.
|
|
11
|
+
- Keeps the hook private to the paths module test surface and adds no public export, general retry policy, lock layer, ledger, or runner.
|
|
12
|
+
- Moves the package version to 0.8.3 together with Contracts and Engineering Kit.
|
|
13
|
+
upgradeNotes: Consumers must pin all three Foundation packages to exactly 0.8.3. Concurrent lock acquisition can now survive the current owner removing the selected lock-file anchor; no Harness API migration is required.
|
|
14
|
+
zh-CN:
|
|
15
|
+
summary: 路径收容现在可处理一次精确的锚点并发删除,同时保持越界检查失败关闭。
|
|
16
|
+
changes:
|
|
17
|
+
fixed:
|
|
18
|
+
- 当另一进程删除已选锚点,导致该锚点的 realpath 返回 ENOENT 时,resolveContained 仅重新计算一次最深既存祖先。
|
|
19
|
+
changed:
|
|
20
|
+
- 第二次 ENOENT 与所有非 ENOENT 错误仍失败关闭;符号链接替换、根目录外目标和既有收容检查仍全部拒绝。
|
|
21
|
+
- 测试钩子只留在 paths 模块的测试表面,不新增公共导出、通用重试策略、锁层、账本或 runner。
|
|
22
|
+
- 包版本与 Contracts、Engineering Kit 一同升至 0.8.3。
|
|
23
|
+
upgradeNotes: 消费者必须把三个 Foundation 包精确锁定到 0.8.3。并发获取锁时,即使当前持有者删除已选锁文件锚点,也能完成一次安全重求;Harness API 无需迁移。
|
package/src/paths.mjs
CHANGED
|
@@ -32,6 +32,13 @@ import { HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
|
|
|
32
32
|
const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:/;
|
|
33
33
|
const WINDOWS_UNC_PATTERN = /^\\\\/;
|
|
34
34
|
const POSIX_UNC_PATTERN = /^\/\//;
|
|
35
|
+
const ANCHOR_REALPATH_ATTEMPTS = 2;
|
|
36
|
+
|
|
37
|
+
let testHooks = null;
|
|
38
|
+
|
|
39
|
+
async function runTestHook(name, context) {
|
|
40
|
+
if (typeof testHooks?.[name] === "function") await testHooks[name](context);
|
|
41
|
+
}
|
|
35
42
|
|
|
36
43
|
/**
|
|
37
44
|
* Classifies one candidate relative path without touching the filesystem.
|
|
@@ -143,7 +150,13 @@ async function deepestExistingAncestor(rootReal, absPath) {
|
|
|
143
150
|
try {
|
|
144
151
|
await lstat(current);
|
|
145
152
|
return { anchor: current, missing };
|
|
146
|
-
} catch {
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
if (cause?.code !== "ENOENT" && cause?.code !== "ENOTDIR") {
|
|
155
|
+
throw mechanismError(
|
|
156
|
+
HARNESS_ERROR_KINDS.READ_FAILED,
|
|
157
|
+
`path ancestor cannot be inspected: ${cause?.code ?? "unknown"}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
147
160
|
missing.unshift(path.basename(current));
|
|
148
161
|
const parent = path.dirname(current);
|
|
149
162
|
if (parent === current) break;
|
|
@@ -158,6 +171,31 @@ async function deepestExistingAncestor(rootReal, absPath) {
|
|
|
158
171
|
);
|
|
159
172
|
}
|
|
160
173
|
|
|
174
|
+
async function canonicalAnchor(rootReal, resolved, relPath) {
|
|
175
|
+
for (let attempt = 0; attempt < ANCHOR_REALPATH_ATTEMPTS; attempt += 1) {
|
|
176
|
+
const { anchor, missing } = await deepestExistingAncestor(rootReal, resolved);
|
|
177
|
+
await runTestHook("beforeAnchorRealpath", { anchor, attempt });
|
|
178
|
+
try {
|
|
179
|
+
return { anchorReal: await realpath(anchor), missing };
|
|
180
|
+
} catch (cause) {
|
|
181
|
+
// An existing lock or staging file can be removed by its owner between
|
|
182
|
+
// lstat() and realpath(). Recompute the ancestor once for that exact
|
|
183
|
+
// disappearance race; every other error remains fail-closed.
|
|
184
|
+
if (cause?.code === "ENOENT" && attempt + 1 < ANCHOR_REALPATH_ATTEMPTS) continue;
|
|
185
|
+
throw mechanismError(
|
|
186
|
+
HARNESS_ERROR_KINDS.REALPATH_ESCAPE,
|
|
187
|
+
"anchor directory vanished during canonical resolution",
|
|
188
|
+
{ input: relPath, code: cause?.code },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw mechanismError(
|
|
193
|
+
HARNESS_ERROR_KINDS.REALPATH_ESCAPE,
|
|
194
|
+
"anchor directory vanished during canonical resolution",
|
|
195
|
+
{ input: relPath },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
161
199
|
/**
|
|
162
200
|
* Resolves `relPath` against `root` and proves the target stays inside.
|
|
163
201
|
* Returns the absolute, lexically resolved path (strictly inside the root).
|
|
@@ -221,17 +259,7 @@ export async function resolveContained(root, relPath) {
|
|
|
221
259
|
|
|
222
260
|
// Escape class 3: canonical resolution (any intermediate symlink chain)
|
|
223
261
|
// must stay inside the canonical root.
|
|
224
|
-
const {
|
|
225
|
-
let anchorReal;
|
|
226
|
-
try {
|
|
227
|
-
anchorReal = await realpath(anchor);
|
|
228
|
-
} catch {
|
|
229
|
-
throw mechanismError(
|
|
230
|
-
HARNESS_ERROR_KINDS.REALPATH_ESCAPE,
|
|
231
|
-
"anchor directory vanished during canonical resolution",
|
|
232
|
-
{ input: relPath },
|
|
233
|
-
);
|
|
234
|
-
}
|
|
262
|
+
const { anchorReal, missing } = await canonicalAnchor(rootReal, resolved, relPath);
|
|
235
263
|
if (!insideOrEqualRoot(rootReal, anchorReal)) {
|
|
236
264
|
throw mechanismError(
|
|
237
265
|
HARNESS_ERROR_KINDS.REALPATH_ESCAPE,
|
|
@@ -250,6 +278,14 @@ export async function resolveContained(root, relPath) {
|
|
|
250
278
|
return resolved;
|
|
251
279
|
}
|
|
252
280
|
|
|
281
|
+
/** Test-only hooks. Deliberately not re-exported from the package index. */
|
|
282
|
+
export function __setPathsTestHooks(hooks) {
|
|
283
|
+
if (hooks !== null && (!hooks || typeof hooks !== "object" || Array.isArray(hooks))) {
|
|
284
|
+
throw new TypeError("paths test hooks must be an object or null");
|
|
285
|
+
}
|
|
286
|
+
testHooks = hooks;
|
|
287
|
+
}
|
|
288
|
+
|
|
253
289
|
/**
|
|
254
290
|
* Contained read. Returns the file content as a Buffer (utf8 string when
|
|
255
291
|
* encoding is "utf8"). Missing resources surface as SFC2004 with the stable
|