skill-family-harness-node 0.13.0 → 0.15.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.
- package/CHANGELOG.md +41 -0
- package/CHANGELOG.zh-CN.md +41 -0
- package/README.md +35 -11
- package/README.zh-CN.md +35 -11
- package/package.json +3 -2
- package/release-notes/0.14.0.yaml +23 -0
- package/release-notes/0.15.0.yaml +21 -0
- package/src/contract-testing.mjs +173 -0
- package/src/errors.mjs +2 -0
- package/src/executable-identity.mjs +480 -0
- package/src/filesystem-observation.mjs +181 -5
- package/src/index.mjs +7 -0
- package/src/version.mjs +2 -0
- package/src/workspace.mjs +46 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
<!-- release-skill:changelog:start version=0.15.0 locale=en baseline=sha256:8c81395a147ed1e995e21ae0074f66d434b87070ea33350fa3e97cbcb5f2fa8e -->
|
|
4
|
+
## [0.15.0] - 2026-08-29
|
|
5
|
+
|
|
6
|
+
Harness 0.15.0 adds executable identity observation and a best-effort Node tree record mode.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Records symlink target bytes without following links on a stable isolated tree.
|
|
11
|
+
- Rechecks the original script after observing its interpreter and orders paths by Unicode code point.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Keeps omitted and reject symlink policy on the 0.14 native path.
|
|
16
|
+
|
|
17
|
+
### Upgrade Notes
|
|
18
|
+
|
|
19
|
+
Record mode is candidate best effort; it does not provide a transaction snapshot or hostile concurrent-writer safety.
|
|
20
|
+
<!-- release-skill:changelog:end version=0.15.0 locale=en -->
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
<!-- release-skill:changelog:start version=0.14.0 locale=en baseline=sha256:23ac2de57433409f1dafac90d7bdd130fd10bd399fd188d7d848a49320b86f30 -->
|
|
24
|
+
## [0.14.0] - 2026-08-28
|
|
25
|
+
|
|
26
|
+
Harness 0.14.0 adds an official atomic-write fake, a synchronized package version export, and canonical temporary workspace roots for safe raw sinks.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Adds createAtomicWriteFake({ vector }) with deterministic write, replace, and observation facts and no filesystem side effects.
|
|
31
|
+
- Exposes FOUNDATION_PACKAGE_VERSION for lockstep consumers.
|
|
32
|
+
- Returns canonical realpaths from TemporaryWorkspace.create() and fromBaseline(). A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Documents that the fake verifies adapter wiring, not the domain guarantee or real-host qualification.
|
|
37
|
+
|
|
38
|
+
### Upgrade Notes
|
|
39
|
+
|
|
40
|
+
Pin Contracts, Harness, and Engineering Kit to 0.14.0 together. Use the official fake with the Contracts vectors; retain real filesystem and domain tests for production guarantees. Temporary workspace roots are canonical. A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
|
|
41
|
+
<!-- release-skill:changelog:end version=0.14.0 locale=en -->
|
|
42
|
+
|
|
43
|
+
|
|
3
44
|
<!-- release-skill:changelog:start version=0.13.0 locale=en baseline=sha256:ba0667eb4b4805aafc2423cd79c04df3c835b8ae9bb07391867175f6ad96c36b -->
|
|
4
45
|
## [0.13.0] - 2026-08-26
|
|
5
46
|
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
+
<!-- release-skill:changelog:start version=0.15.0 locale=zh-CN baseline=sha256:945e9d06ed1cc8178ac13f3ec27ee753ec129d2f54ae59b59c6d8f4ec137d821 -->
|
|
4
|
+
## [0.15.0] - 2026-08-29
|
|
5
|
+
|
|
6
|
+
Harness 0.15.0 增加可执行文件身份观察与 Node 树 best-effort 记录模式。
|
|
7
|
+
|
|
8
|
+
### 新增
|
|
9
|
+
|
|
10
|
+
- 在稳定隔离树上记录 symlink 自身与原始 target bytes,不跟随链接。
|
|
11
|
+
- 观察解释器后重查原脚本,并按 Unicode code point 排序路径。
|
|
12
|
+
|
|
13
|
+
### 变更
|
|
14
|
+
|
|
15
|
+
- 省略或 reject symlinkPolicy 时继续使用 0.14 native 路径。
|
|
16
|
+
|
|
17
|
+
### 升级说明
|
|
18
|
+
|
|
19
|
+
record 模式仍为 candidate best-effort,不提供事务快照或恶意并发写者安全保证。
|
|
20
|
+
<!-- release-skill:changelog:end version=0.15.0 locale=zh-CN -->
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
<!-- release-skill:changelog:start version=0.14.0 locale=zh-CN baseline=sha256:11857b4e61d5111fab3dbc5eb1afcf99fd8fd6fd41f190bad5a5e16c09633e25 -->
|
|
24
|
+
## [0.14.0] - 2026-08-28
|
|
25
|
+
|
|
26
|
+
Harness 0.14.0 增加消费者契约测试用正式 atomic-write 测试替身(`fake`)、同步公开包版本导出,并让临时工作区根目录返回 canonical realpath。
|
|
27
|
+
|
|
28
|
+
### 新增
|
|
29
|
+
|
|
30
|
+
- 新增 `createAtomicWriteFake({ vector })`,提供确定性的写入、替换和观察事实且不触碰文件系统。
|
|
31
|
+
- 公开 `FOUNDATION_PACKAGE_VERSION`,供锁步消费者使用。
|
|
32
|
+
- `TemporaryWorkspace.create()` 与 `fromBaseline()` 返回 canonical realpath。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。
|
|
33
|
+
|
|
34
|
+
### 变更
|
|
35
|
+
|
|
36
|
+
- 明确测试替身用于验证适配器接线,不代表领域保证或真实宿主资格。
|
|
37
|
+
|
|
38
|
+
### 升级说明
|
|
39
|
+
|
|
40
|
+
Contracts、Harness 与 Engineering Kit 须一起精确锁定到 0.14.0。用正式测试替身配合 Contracts 向量;生产保证仍须由真实文件系统和领域测试覆盖。临时工作区根目录已经 canonical。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。
|
|
41
|
+
<!-- release-skill:changelog:end version=0.14.0 locale=zh-CN -->
|
|
42
|
+
|
|
43
|
+
|
|
3
44
|
<!-- release-skill:changelog:start version=0.13.0 locale=zh-CN baseline=sha256:1356b2ecff87da4a8bbe5bc04980b125ddf181873ff2b536be87a2e246b59609 -->
|
|
4
45
|
## [0.13.0] - 2026-08-26
|
|
5
46
|
|
package/README.md
CHANGED
|
@@ -4,26 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
# skill-family-harness-node
|
|
6
6
|
|
|
7
|
-
<!-- release-skill:release-version: 0.
|
|
7
|
+
<!-- release-skill:release-version: 0.15.0 -->
|
|
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.
|
|
12
|
+
**0.15.0** (2026-08-29)
|
|
13
13
|
|
|
14
|
-
Harness 0.
|
|
14
|
+
Harness 0.15.0 adds executable identity observation and a best-effort Node tree record mode.
|
|
15
15
|
|
|
16
16
|
**Added**
|
|
17
17
|
|
|
18
|
-
-
|
|
18
|
+
- Records symlink target bytes without following links on a stable isolated tree.
|
|
19
|
+
- Rechecks the original script after observing its interpreter and orders paths by Unicode code point.
|
|
19
20
|
|
|
20
21
|
**Changed**
|
|
21
22
|
|
|
22
|
-
-
|
|
23
|
+
- Keeps omitted and reject symlink policy on the 0.14 native path.
|
|
23
24
|
|
|
24
25
|
**Upgrade Notes**
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
Record mode is candidate best effort; it does not provide a transaction snapshot or hostile concurrent-writer safety.
|
|
27
28
|
<!-- release-skill:managed:end id=latest-release -->
|
|
28
29
|
|
|
29
30
|
## Problem It Solves
|
|
@@ -36,17 +37,28 @@ The Harness consumes `skill-family-contracts` (a workspace dependency), reusing
|
|
|
36
37
|
|
|
37
38
|
## Installation and Minimal Example
|
|
38
39
|
|
|
39
|
-
Version 0.
|
|
40
|
+
Version 0.15.0 is a local candidate. Build all three tarballs into one temporary directory and install those exact files for a candidate check:
|
|
40
41
|
|
|
41
42
|
```sh
|
|
42
|
-
|
|
43
|
+
pack_dir="$(mktemp -d)"
|
|
44
|
+
(cd packages/skill-family-contracts && pnpm pack --pack-destination "$pack_dir")
|
|
45
|
+
(cd packages/skill-family-harness-node && pnpm pack --pack-destination "$pack_dir")
|
|
46
|
+
(cd packages/skill-family-engineering-kit && pnpm pack --pack-destination "$pack_dir")
|
|
47
|
+
mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
|
|
48
|
+
(cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.15.0.tgz" "$pack_dir/skill-family-harness-node-0.15.0.tgz" "$pack_dir/skill-family-engineering-kit-0.15.0.tgz")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
After publication, use the registry coordinate:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm install skill-family-harness-node@0.15.0
|
|
43
55
|
npm info skill-family-harness-node --help
|
|
44
56
|
```
|
|
45
57
|
|
|
46
58
|
The minimal example shows validating a contract document inside Node:
|
|
47
59
|
|
|
48
60
|
```js
|
|
49
|
-
// Run from an
|
|
61
|
+
// Run from an installed consumer directory after publication.
|
|
50
62
|
import { validateContractDocument } from "skill-family-harness-node";
|
|
51
63
|
|
|
52
64
|
const document = {
|
|
@@ -100,13 +112,17 @@ The capability remains **candidate**. Pin all three Foundation packages exactly
|
|
|
100
112
|
| Export | Responsibility |
|
|
101
113
|
| --- | --- |
|
|
102
114
|
| `HARNESS_CAPABILITIES` / `HARNESS_EXCLUSIONS` | Capability and exclusion lists (frozen constants). |
|
|
115
|
+
| `FOUNDATION_PACKAGE_VERSION` | Exact Foundation package version used for lockstep checks. |
|
|
103
116
|
| `HarnessError` / `HARNESS_ERROR_KINDS` / `mechanismError` | Mechanism failures uniformly carry the registered error code `SFC2004`; `details.kind` gives a stable subcategory. |
|
|
104
117
|
| `validateContractDocument` / `getValidator` / `resolveSchemaContext` / `validatorCacheSize` | Routes and caches validators by Schema dialect; reuses Contracts' Ajv instances and dialect/policy semantics. |
|
|
105
118
|
| `classifyPathInput` / `resolveContained` / `readFileContained` | Path containment: intercepts path overruns, symlink escapes, and realpath escapes. |
|
|
106
119
|
| `writeFileAtomic` | Atomic write: leaves no half-written artifact on failure (temp file + fsync + rename). |
|
|
120
|
+
| `createAtomicWriteFake({ vector })` | Official no-filesystem fake for consumer contract tests; emits deterministic write/replace/observation facts. |
|
|
107
121
|
| `TemporaryWorkspace` / `createTemporaryWorkspace` / `withTemporaryWorkspace` | Auto-cleanup temporary workspace, cleaned up even on exception paths. |
|
|
108
122
|
| `digestBytes` / `computeResourceClosure` / `closureContains` | Resource closure and deterministic sha256 digest. |
|
|
109
123
|
| `superviseProcess` / `validateTimeoutPolicy` | The single bounded subprocess supervisor. Its 0.11.0 `rawSink` option writes raw stdout/stderr bytes only to a fresh canonical private root, then waits for child/stream close, queued writes, fsync, and handle close. The caller must exclusively control the sink namespace for the entire call; handle protection does not prove stable pathname/root identity. |
|
|
124
|
+
| `observeFilesystemTree` | Observe a complete bound tree. Default/reject preserves the existing UTF-16 member order; record uses code-point order and records symlink target bytes without following them. |
|
|
125
|
+
| `observeExecutableIdentity` | Observe a caller-bound executable, its symlink chain, launch bytes, and any script interpreter chain for immediate pre-spawn re-observation. |
|
|
110
126
|
| `parseRequest` / `processRequest` | Parse `operation-request`, output terminal `operation-result`. |
|
|
111
127
|
| `validateReportModel` / `renderReportMarkdown` / `buildBinding` / `checkReport` | Consume a Contracts-validated report model, deterministically render neutral Markdown, and verify source/result/report binding; does not interpret business output. |
|
|
112
128
|
| `normalizeAdapterSource` / `buildAdapterClosure` / `verifyAdapterBuildManifest` / `materializeAdapterBuild` | Generic text-source closure, full-manifest digest re-verification, and atomic landing of the target set; specific Profile/driver is not in the Harness. |
|
|
@@ -168,7 +184,7 @@ Mechanism failures uniformly throw `SFC2004` (EXECUTION_FAILED), with `details.k
|
|
|
168
184
|
### Do not use when
|
|
169
185
|
|
|
170
186
|
- You need to put file-selection business rules into the Foundation (business rules are owned by the caller).
|
|
171
|
-
- You need host identity policy, host
|
|
187
|
+
- You need host identity policy, host-specific lifecycle plans, remote publication, deleting uninstall, or binary adapter source. Host policy and lifecycle plans belong to Engineering Kit.
|
|
172
188
|
- You need domain audit semantics, retry orchestration, or a compatibility-frozen Quickstart API.
|
|
173
189
|
|
|
174
190
|
### Capability selection
|
|
@@ -229,4 +245,12 @@ Mechanism failures uniformly throw `SFC2004` (EXECUTION_FAILED), with `details.k
|
|
|
229
245
|
|
|
230
246
|
The candidate observeFilesystemTree({ root, rootBinding }) reads complete tree facts. Existing superviseProcess accepts optional per-stream raw-byte caps. Observing a payload does not accept it.
|
|
231
247
|
|
|
232
|
-
|
|
248
|
+
`observeFilesystemTree({ root, rootBinding, symlinkPolicy: { mode: "record" } })` is for a stable installation, cache, or projection tree after the host command has completed, with caller isolation and no concurrent namespace writer during the scan. It records each symlink itself as `targetBase64`, `bytes`, and `statMode`, reads only the raw target bytes, and never follows the target; regular files continue to reuse `readFileBound`. Record is a Node/JS best-effort candidate: observed drift fails closed, but the mode does not provide a transaction snapshot, same-UID malicious-concurrency, or ABA guarantee. A stable-tree result remains a normal usable result. Consumers such as release-skill own invocation timing and domain acceptance rules such as npm `.bin`.
|
|
249
|
+
|
|
250
|
+
Member ordering is mode-specific for compatibility: omitted/reject mode retains the 0.14 UTF-16 relational order, while record mode uses Unicode code-point order. The selected order is also the order used to derive `membersDigest`.
|
|
251
|
+
|
|
252
|
+
When the actual threat includes malicious concurrency, return a minimal upstream capability gap to Foundation for a decision. Do not copy a generic walker, native addon, Harness, schema, Registry, runner, or receipt chain into a skill family, and do not independently upgrade ordinary consumers to a four-platform native implementation.
|
|
253
|
+
|
|
254
|
+
The separate candidate `observeExecutableIdentity({ boundRoots, lookup, interpreterPolicy? })` provides a read-only point-in-time observation of only the caller-explicit roots and lookup paths, for an immediate re-observation before launch. When an `/usr/bin/env` shebang resolves an interpreter through explicit `pathEntries`, the observation preserves the interpreter candidate's complete symlink chain rather than collapsing it to the final file. It is not part of `host-adapter` and does not prove wrapper control flow, ambient `PATH`, fd-exec/kernel image, signature trust, cross-call caching, host support/lifecycle, or domain acceptance; the caller owns those semantics. The candidate entry alone does not qualify a host.
|
|
255
|
+
|
|
256
|
+
Version 0.15.0 is a local source candidate and is not published. Consume the three locally verified tarballs; a version marker, unit test or successful install is not complete contract integration, migration completion, or real-host qualification.
|
package/README.zh-CN.md
CHANGED
|
@@ -5,26 +5,27 @@
|
|
|
5
5
|
|
|
6
6
|
# skill-family-harness-node
|
|
7
7
|
|
|
8
|
-
<!-- release-skill:release-version: 0.
|
|
8
|
+
<!-- release-skill:release-version: 0.15.0 -->
|
|
9
9
|
|
|
10
10
|
Contracts 机制协议的**唯一默认 Node 实现**。这是一个薄运行时(thin runtime):只实现机制协议,不引入业务语义,不做第二语言实现。
|
|
11
11
|
|
|
12
12
|
<!-- release-skill:managed:start id=latest-release -->
|
|
13
|
-
**0.
|
|
13
|
+
**0.15.0** (2026-08-29)
|
|
14
14
|
|
|
15
|
-
Harness 0.
|
|
15
|
+
Harness 0.15.0 增加可执行文件身份观察与 Node 树 best-effort 记录模式。
|
|
16
16
|
|
|
17
17
|
**新增**
|
|
18
18
|
|
|
19
|
-
-
|
|
19
|
+
- 在稳定隔离树上记录 symlink 自身与原始 target bytes,不跟随链接。
|
|
20
|
+
- 观察解释器后重查原脚本,并按 Unicode code point 排序路径。
|
|
20
21
|
|
|
21
22
|
**变更**
|
|
22
23
|
|
|
23
|
-
-
|
|
24
|
+
- 省略或 reject symlinkPolicy 时继续使用 0.14 native 路径。
|
|
24
25
|
|
|
25
26
|
**升级说明**
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
record 模式仍为 candidate best-effort,不提供事务快照或恶意并发写者安全保证。
|
|
28
29
|
<!-- release-skill:managed:end id=latest-release -->
|
|
29
30
|
|
|
30
31
|
## 解决的问题
|
|
@@ -37,17 +38,28 @@ Harness 消费 `skill-family-contracts`(工作区依赖),复用其方言
|
|
|
37
38
|
|
|
38
39
|
## 安装和最小示例
|
|
39
40
|
|
|
40
|
-
0.
|
|
41
|
+
0.15.0 是本地候选版本。候选验证先把三个包分别打入同一个临时目录,再安装这三个精确 tarball:
|
|
41
42
|
|
|
42
43
|
```sh
|
|
43
|
-
|
|
44
|
+
pack_dir="$(mktemp -d)"
|
|
45
|
+
(cd packages/skill-family-contracts && pnpm pack --pack-destination "$pack_dir")
|
|
46
|
+
(cd packages/skill-family-harness-node && pnpm pack --pack-destination "$pack_dir")
|
|
47
|
+
(cd packages/skill-family-engineering-kit && pnpm pack --pack-destination "$pack_dir")
|
|
48
|
+
mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
|
|
49
|
+
(cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.15.0.tgz" "$pack_dir/skill-family-harness-node-0.15.0.tgz" "$pack_dir/skill-family-engineering-kit-0.15.0.tgz")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
发布后再使用 registry 坐标:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm install skill-family-harness-node@0.15.0
|
|
44
56
|
npm info skill-family-harness-node --help
|
|
45
57
|
```
|
|
46
58
|
|
|
47
59
|
最小示例演示在 Node 内校验一份契约文档:
|
|
48
60
|
|
|
49
61
|
```js
|
|
50
|
-
//
|
|
62
|
+
// 发布后从已安装的消费者目录运行。
|
|
51
63
|
import { validateContractDocument } from "skill-family-harness-node";
|
|
52
64
|
|
|
53
65
|
const document = {
|
|
@@ -101,13 +113,17 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
|
|
|
101
113
|
| 导出 | 职责 |
|
|
102
114
|
| --- | --- |
|
|
103
115
|
| `HARNESS_CAPABILITIES` / `HARNESS_EXCLUSIONS` | 能力与排除清单(冻结常量)。 |
|
|
116
|
+
| `FOUNDATION_PACKAGE_VERSION` | 供锁步检查使用的精确 Foundation 包版本。 |
|
|
104
117
|
| `HarnessError` / `HARNESS_ERROR_KINDS` / `mechanismError` | 机制失败统一携带注册错误码 `SFC2004`,`details.kind` 给出稳定细分。 |
|
|
105
118
|
| `validateContractDocument` / `getValidator` / `resolveSchemaContext` / `validatorCacheSize` | 按 Schema 方言路由并缓存 validator;复用 Contracts 的 Ajv 实例与 dialect/policy 语义。 |
|
|
106
119
|
| `classifyPathInput` / `resolveContained` / `readFileContained` | 路径收容:拦截路径越界、符号链接逃逸、真实路径逃逸。 |
|
|
107
120
|
| `writeFileAtomic` | 原子写:失败不留半成品(临时文件 + fsync + rename)。 |
|
|
121
|
+
| `createAtomicWriteFake({ vector })` | 消费者契约测试用正式无文件系统测试替身;产生确定性的写入/替换/观察事实。 |
|
|
108
122
|
| `TemporaryWorkspace` / `createTemporaryWorkspace` / `withTemporaryWorkspace` | 自动清理的临时工作区,异常路径也清理。 |
|
|
109
123
|
| `digestBytes` / `computeResourceClosure` / `closureContains` | 资源闭包与确定性 sha256 摘要。 |
|
|
110
124
|
| `superviseProcess` / `validateTimeoutPolicy` | 唯一的受约束子进程监督器。0.11.0 的 `rawSink` 只向 fresh canonical 私有根写原始 stdout/stderr 字节,并等待子进程、流、排队写入、fsync 与句柄全部关闭。调用方必须在整个调用期间独占 sink 命名空间;句柄保护不证明 pathname 或根目录身份始终不变。 |
|
|
125
|
+
| `observeFilesystemTree` | 观察完整绑定树。默认/reject 保留既有 UTF-16 成员顺序;record 使用码点顺序,记录 symlink target bytes 且不跟随目标。 |
|
|
126
|
+
| `observeExecutableIdentity` | 观察调用方绑定的可执行入口、符号链接链、启动字节和脚本解释器链,供每次 spawn 前紧邻重观察。 |
|
|
111
127
|
| `parseRequest` / `processRequest` | 解析 `operation-request`,输出终态 `operation-result`。 |
|
|
112
128
|
| `validateReportModel` / `renderReportMarkdown` / `buildBinding` / `checkReport` | 消费经 Contracts 验证的 report model,确定性渲染中性 Markdown 并校验来源/结果/报告绑定;不解释业务输出。 |
|
|
113
129
|
| `normalizeAdapterSource` / `buildAdapterClosure` / `verifyAdapterBuildManifest` / `materializeAdapterBuild` | 通用文本 source closure、manifest 全摘要复验和目标集合原子落盘;具体 Profile/driver 不在 Harness。 |
|
|
@@ -169,7 +185,7 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
|
|
|
169
185
|
### Do not use when
|
|
170
186
|
|
|
171
187
|
- 需要把文件选择的业务规则放入 Foundation(业务规则由调用方拥有)。
|
|
172
|
-
-
|
|
188
|
+
- 需要宿主身份策略、宿主专属生命周期计划、远端发布、删除式 uninstall 或二进制 adapter source。宿主策略与生命周期计划归 Engineering Kit。
|
|
173
189
|
- 需要领域审计语义、重试编排或兼容性已冻结的 Quickstart API。
|
|
174
190
|
|
|
175
191
|
### Capability selection
|
|
@@ -229,4 +245,12 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
|
|
|
229
245
|
|
|
230
246
|
新增候选 observeFilesystemTree({ root, rootBinding }) 读取完整树事实;既有 superviseProcess 支持可选每流原始字节上限。观察完成不等于接受载荷。
|
|
231
247
|
|
|
232
|
-
|
|
248
|
+
`observeFilesystemTree({ root, rootBinding, symlinkPolicy: { mode: "record" } })` 适用于宿主命令完成后、调用方已隔离且扫描期间没有并发 namespace writer 的稳定安装树、缓存树或投影树。它记录 symlink 自身的 `targetBase64`、`bytes` 和 `statMode`,只读取原始 target bytes,绝不跟随 target;普通文件仍复用 `readFileBound`。record 是 Node/JS best-effort candidate:发现扫描期间的漂移会失败关闭,但不提供事务快照、同 UID 恶意并发或 ABA 保证,这不改变稳定树结果的正常使用语义。release-skill 等消费者负责调用时机,并继续负责 npm `.bin` 等领域接受规则。
|
|
249
|
+
|
|
250
|
+
成员排序按模式区分,以保留兼容性:省略策略或使用 reject 时沿用 0.14 的 UTF-16 关系顺序,record 使用 Unicode 码点顺序;`membersDigest` 也按对应顺序推导。
|
|
251
|
+
|
|
252
|
+
真实威胁包含恶意并发时,应形成最小上游能力缺口并交回 Foundation 裁决;不要在技能族内复制通用 walker、native addon、Harness、schema、Registry、runner 或 receipt 链,也不要自行升级为四平台原生实现。
|
|
253
|
+
|
|
254
|
+
另一个独立候选 `observeExecutableIdentity({ boundRoots, lookup, interpreterPolicy? })` 只对调用方显式提供的根和查找路径做逐次只读观察,供正式启动前紧邻重观察。`/usr/bin/env` shebang 通过显式 `pathEntries` 找到解释器时,结果保留解释器候选的完整 symlink chain,不折叠成最终文件。它不属于 `host-adapter`,也不证明 wrapper 控制流、ambient `PATH`、fd-exec/内核映像、签名信任、跨调用缓存、宿主支持/生命周期或领域接受;这些语义仍由调用方负责。候选入口存在不等于宿主已获资格。
|
|
255
|
+
|
|
256
|
+
0.15.0 为本地源码候选,尚未发布。消费本地已验证的三包 tarball;版本标记、单元测试或安装成功都不等于契约接入完成、迁移完成或真实宿主资格。
|
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.
|
|
13
|
+
"skill-family-contracts": "0.15.0",
|
|
14
14
|
"yaml": "2.9.0"
|
|
15
15
|
},
|
|
16
16
|
"description": "Thin Node.js mechanism runtime for Skill Family engineering contracts.",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
".": "./src/index.mjs",
|
|
23
23
|
"./candidate/quickstart-profile": "./candidate/quickstart-profile.mjs",
|
|
24
24
|
"./candidate/rename-directory-no-replace": "./candidate/rename-directory-no-replace/rename-directory-no-replace.mjs",
|
|
25
|
+
"./contract-testing": "./src/contract-testing.mjs",
|
|
25
26
|
"./fixed-set-publication": "./src/fixed-set-publication.mjs",
|
|
26
27
|
"./quickstart-profile": "./candidate/quickstart-profile.mjs",
|
|
27
28
|
"./rename-directory-no-replace": "./candidate/rename-directory-no-replace/rename-directory-no-replace.mjs"
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
"url": "https://github.com/ifoohoo/skill-family-harness-node.git"
|
|
49
50
|
},
|
|
50
51
|
"type": "module",
|
|
51
|
-
"version": "0.
|
|
52
|
+
"version": "0.15.0",
|
|
52
53
|
"scripts": {
|
|
53
54
|
"check": "node --test",
|
|
54
55
|
"test": "node --test"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: 0.14.0
|
|
2
|
+
date: 2026-08-28
|
|
3
|
+
locales:
|
|
4
|
+
en:
|
|
5
|
+
summary: Harness 0.14.0 adds an official atomic-write fake, a synchronized package version export, and canonical temporary workspace roots for safe raw sinks.
|
|
6
|
+
changes:
|
|
7
|
+
added:
|
|
8
|
+
- Adds createAtomicWriteFake({ vector }) with deterministic write, replace, and observation facts and no filesystem side effects.
|
|
9
|
+
- Exposes FOUNDATION_PACKAGE_VERSION for lockstep consumers.
|
|
10
|
+
- Returns canonical realpaths from TemporaryWorkspace.create() and fromBaseline(). A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
|
|
11
|
+
changed:
|
|
12
|
+
- Documents that the fake verifies adapter wiring, not the domain guarantee or real-host qualification.
|
|
13
|
+
upgradeNotes: Pin Contracts, Harness, and Engineering Kit to 0.14.0 together. Use the official fake with the Contracts vectors; retain real filesystem and domain tests for production guarantees. Temporary workspace roots are canonical. A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
|
|
14
|
+
zh-CN:
|
|
15
|
+
summary: 'Harness 0.14.0 增加消费者契约测试用正式 atomic-write 测试替身(`fake`)、同步公开包版本导出,并让临时工作区根目录返回 canonical realpath。'
|
|
16
|
+
changes:
|
|
17
|
+
added:
|
|
18
|
+
- 新增 `createAtomicWriteFake({ vector })`,提供确定性的写入、替换和观察事实且不触碰文件系统。
|
|
19
|
+
- 公开 `FOUNDATION_PACKAGE_VERSION`,供锁步消费者使用。
|
|
20
|
+
- '`TemporaryWorkspace.create()` 与 `fromBaseline()` 返回 canonical realpath。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。'
|
|
21
|
+
changed:
|
|
22
|
+
- 明确测试替身用于验证适配器接线,不代表领域保证或真实宿主资格。
|
|
23
|
+
upgradeNotes: 'Contracts、Harness 与 Engineering Kit 须一起精确锁定到 0.14.0。用正式测试替身配合 Contracts 向量;生产保证仍须由真实文件系统和领域测试覆盖。临时工作区根目录已经 canonical。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
version: 0.15.0
|
|
2
|
+
date: 2026-08-29
|
|
3
|
+
locales:
|
|
4
|
+
en:
|
|
5
|
+
summary: Harness 0.15.0 adds executable identity observation and a best-effort Node tree record mode.
|
|
6
|
+
changes:
|
|
7
|
+
added:
|
|
8
|
+
- Records symlink target bytes without following links on a stable isolated tree.
|
|
9
|
+
- Rechecks the original script after observing its interpreter and orders paths by Unicode code point.
|
|
10
|
+
changed:
|
|
11
|
+
- Keeps omitted and reject symlink policy on the 0.14 native path.
|
|
12
|
+
upgradeNotes: Record mode is candidate best effort; it does not provide a transaction snapshot or hostile concurrent-writer safety.
|
|
13
|
+
zh-CN:
|
|
14
|
+
summary: Harness 0.15.0 增加可执行文件身份观察与 Node 树 best-effort 记录模式。
|
|
15
|
+
changes:
|
|
16
|
+
added:
|
|
17
|
+
- 在稳定隔离树上记录 symlink 自身与原始 target bytes,不跟随链接。
|
|
18
|
+
- 观察解释器后重查原脚本,并按 Unicode code point 排序路径。
|
|
19
|
+
changed:
|
|
20
|
+
- 省略或 reject symlinkPolicy 时继续使用 0.14 native 路径。
|
|
21
|
+
upgradeNotes: record 模式仍为 candidate best-effort,不提供事务快照或恶意并发写者安全保证。
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
ContractsError,
|
|
5
|
+
assertRegisteredErrorCode,
|
|
6
|
+
verifyConsumerContractVector,
|
|
7
|
+
} from "skill-family-contracts";
|
|
8
|
+
import { HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
|
|
9
|
+
import { FOUNDATION_PACKAGE_VERSION } from "./version.mjs";
|
|
10
|
+
|
|
11
|
+
const ATOMIC_WRITE_CAPABILITY = "foundation.harness.atomic-write";
|
|
12
|
+
const ATOMIC_WRITE_VECTOR_SET = "foundation.harness.atomic-write.consumer-v1";
|
|
13
|
+
const ATOMIC_WRITE_CONTRACT = "skill-family-harness-node:writeFileAtomic";
|
|
14
|
+
|
|
15
|
+
function deepFreeze(value, seen = new Set()) {
|
|
16
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return value;
|
|
17
|
+
seen.add(value);
|
|
18
|
+
for (const child of Object.values(value)) deepFreeze(child, seen);
|
|
19
|
+
return Object.freeze(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sameValue(left, right, seen = new Set()) {
|
|
23
|
+
if (Object.is(left, right)) return true;
|
|
24
|
+
if (left === null || right === null || typeof left !== typeof right) return false;
|
|
25
|
+
if (typeof left !== "object") return false;
|
|
26
|
+
if (Buffer.isBuffer(left) || Buffer.isBuffer(right)) {
|
|
27
|
+
return Buffer.isBuffer(left) && Buffer.isBuffer(right) && left.equals(right);
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
30
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length &&
|
|
31
|
+
left.every((value, index) => sameValue(value, right[index], seen));
|
|
32
|
+
}
|
|
33
|
+
if (seen.has(left)) return true;
|
|
34
|
+
seen.add(left);
|
|
35
|
+
const leftKeys = Object.keys(left).sort();
|
|
36
|
+
const rightKeys = Object.keys(right).sort();
|
|
37
|
+
return leftKeys.length === rightKeys.length &&
|
|
38
|
+
leftKeys.every((key, index) => key === rightKeys[index] && sameValue(left[key], right[key], seen));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dataSummary(data) {
|
|
42
|
+
if (typeof data === "string") return data;
|
|
43
|
+
if (Buffer.isBuffer(data)) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "Buffer",
|
|
46
|
+
bytes: data.byteLength,
|
|
47
|
+
sha256: createHash("sha256").update(data).digest("hex"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
51
|
+
const bytes = data instanceof ArrayBuffer
|
|
52
|
+
? Buffer.from(data)
|
|
53
|
+
: Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
54
|
+
return {
|
|
55
|
+
kind: data.constructor?.name ?? "ArrayBufferView",
|
|
56
|
+
bytes: bytes.byteLength,
|
|
57
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cloneForObservation(value, seen = new Map()) {
|
|
64
|
+
if (value === null || typeof value !== "object") return value;
|
|
65
|
+
if (Buffer.isBuffer(value)) return dataSummary(value);
|
|
66
|
+
if (seen.has(value)) return "[Circular]";
|
|
67
|
+
seen.set(value, true);
|
|
68
|
+
if (Array.isArray(value)) return value.map((child) => cloneForObservation(child, seen));
|
|
69
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, cloneForObservation(value[key], seen)]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function contractMismatch(validation) {
|
|
73
|
+
const code = validation?.mismatchCode ?? "SFC1001";
|
|
74
|
+
return new ContractsError(code, "atomic-write fake requires an official verified consumer contract vector", {
|
|
75
|
+
vectorId: validation?.vectorId ?? null,
|
|
76
|
+
observed: validation,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function invalidRequest(message, details) {
|
|
81
|
+
return mechanismError(HARNESS_ERROR_KINDS.ATOMIC_WRITE_FAILED, message, details);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function requestMatches(vector, root, relPath, data, mode) {
|
|
85
|
+
const expected = vector.request;
|
|
86
|
+
return typeof root === "string" && root.length > 0 && path.isAbsolute(root) &&
|
|
87
|
+
path.normalize(root) === root &&
|
|
88
|
+
relPath === expected.relPath && sameValue(data, expected.data) &&
|
|
89
|
+
sameValue({ mode }, expected.options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates the atomic-write fake published for the exact Foundation vector set.
|
|
94
|
+
* The fake is deliberately single-use and performs no filesystem operation.
|
|
95
|
+
*/
|
|
96
|
+
export function createAtomicWriteFake({ vector } = {}) {
|
|
97
|
+
let vectorSnapshot;
|
|
98
|
+
try {
|
|
99
|
+
vectorSnapshot = structuredClone(vector);
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
throw new ContractsError("SFC1001", "atomic-write fake requires a structured consumer contract vector", {
|
|
102
|
+
cause: cause?.name ?? "DataCloneError",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const validation = verifyConsumerContractVector(vectorSnapshot, {
|
|
106
|
+
capabilityId: ATOMIC_WRITE_CAPABILITY,
|
|
107
|
+
foundationVersion: FOUNDATION_PACKAGE_VERSION,
|
|
108
|
+
vectorSetId: ATOMIC_WRITE_VECTOR_SET,
|
|
109
|
+
});
|
|
110
|
+
if (!validation.ok) throw contractMismatch(validation);
|
|
111
|
+
if (vectorSnapshot.contractId !== ATOMIC_WRITE_CONTRACT || vectorSnapshot.strategy !== "official-fake") {
|
|
112
|
+
throw contractMismatch({ ...validation, mismatchCode: "SFC1013" });
|
|
113
|
+
}
|
|
114
|
+
deepFreeze(vectorSnapshot);
|
|
115
|
+
|
|
116
|
+
const identity = deepFreeze({
|
|
117
|
+
capabilityId: ATOMIC_WRITE_CAPABILITY,
|
|
118
|
+
vectorSetId: ATOMIC_WRITE_VECTOR_SET,
|
|
119
|
+
foundationVersion: FOUNDATION_PACKAGE_VERSION,
|
|
120
|
+
});
|
|
121
|
+
let consumed = false;
|
|
122
|
+
let state = { callCount: 0, request: null, outcome: "indeterminate" };
|
|
123
|
+
|
|
124
|
+
function observation() {
|
|
125
|
+
return deepFreeze(cloneForObservation(state));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function writeFileAtomic(root, relPath, data, { mode = 0o644 } = {}) {
|
|
129
|
+
if (consumed) {
|
|
130
|
+
throw invalidRequest("atomic-write fake vector was already consumed", { vectorId: vectorSnapshot.vectorId });
|
|
131
|
+
}
|
|
132
|
+
if (!requestMatches(vectorSnapshot, root, relPath, data, mode)) {
|
|
133
|
+
throw invalidRequest("atomic-write fake received a request different from its verified vector", {
|
|
134
|
+
vectorId: vectorSnapshot.vectorId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
consumed = true;
|
|
138
|
+
state = {
|
|
139
|
+
callCount: 1,
|
|
140
|
+
request: {
|
|
141
|
+
root: cloneForObservation(root),
|
|
142
|
+
relPath: cloneForObservation(relPath),
|
|
143
|
+
data: cloneForObservation(data),
|
|
144
|
+
options: cloneForObservation({ mode }),
|
|
145
|
+
},
|
|
146
|
+
outcome: vectorSnapshot.expected.outcome,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (vectorSnapshot.expected.outcome === "return" || vectorSnapshot.expected.outcome === "indeterminate") {
|
|
150
|
+
const value = path.join(root, vectorSnapshot.request.relPath);
|
|
151
|
+
if (!path.isAbsolute(value) || value === root) {
|
|
152
|
+
state = { ...state, outcome: "throw", errorCode: "SFC2004" };
|
|
153
|
+
throw invalidRequest("atomic-write fake could not project an absolute target", { vectorId: vectorSnapshot.vectorId });
|
|
154
|
+
}
|
|
155
|
+
state = { ...state, outcome: "return", value };
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const errorCode = vectorSnapshot.expected.errorCode ?? "SFC2004";
|
|
160
|
+
const errorKind = vectorSnapshot.expected.errorKind;
|
|
161
|
+
assertRegisteredErrorCode(errorCode);
|
|
162
|
+
state = { ...state, errorCode, errorKind };
|
|
163
|
+
if (errorCode === "SFC2004") {
|
|
164
|
+
if (errorKind === HARNESS_ERROR_KINDS.PATH_TRAVERSAL) {
|
|
165
|
+
throw mechanismError(errorKind, "atomic-write fake projected the vector failure", { vectorId: vectorSnapshot.vectorId });
|
|
166
|
+
}
|
|
167
|
+
throw invalidRequest("atomic-write fake projected the vector failure", { vectorId: vectorSnapshot.vectorId });
|
|
168
|
+
}
|
|
169
|
+
throw new ContractsError(errorCode, "atomic-write fake projected the vector failure", { vectorId: vectorSnapshot.vectorId });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return Object.freeze({ identity, writeFileAtomic, observation });
|
|
173
|
+
}
|
package/src/errors.mjs
CHANGED
|
@@ -59,6 +59,8 @@ export const HARNESS_ERROR_KINDS = Object.freeze({
|
|
|
59
59
|
HOST_BUILD_FAILED: "host-build-failed",
|
|
60
60
|
HOST_PROBE_FAILED: "host-probe-failed",
|
|
61
61
|
UNTRUSTED_EXECUTABLE: "untrusted-executable",
|
|
62
|
+
EXECUTABLE_NOT_FOUND: "executable-not-found",
|
|
63
|
+
UNSUPPORTED_INTERPRETER: "unsupported-interpreter",
|
|
62
64
|
PORTABLE_PATH_COLLISION: "portable-path-collision",
|
|
63
65
|
MANIFEST_MISMATCH: "manifest-mismatch",
|
|
64
66
|
BASELINE_MISMATCH: "baseline-mismatch",
|