skill-family-harness-node 0.4.0 → 0.6.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 +48 -0
- package/CHANGELOG.zh-CN.md +48 -0
- package/README.md +13 -12
- package/README.zh-CN.md +13 -12
- package/package.json +8 -2
- package/release-notes/0.5.0.yaml +29 -0
- package/release-notes/0.6.0.yaml +29 -0
- package/src/atomic.mjs +39 -4
- package/src/declared-read-surface.mjs +198 -0
- package/src/errors.mjs +8 -0
- package/src/index.mjs +54 -1
- package/src/strict-read.mjs +148 -0
- package/src/structured-scan.mjs +652 -0
- package/src/supervise-process.mjs +1055 -0
- package/src/token-estimate-cli.mjs +175 -0
- package/src/token-estimate.mjs +139 -1
- package/src/url-redaction.mjs +55 -0
- package/CODE_OF_CONDUCT.md +0 -131
- package/CONTRIBUTING.md +0 -25
- package/SECURITY.md +0 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
<!-- release-skill:changelog:start version=0.6.0 locale=en baseline=sha256:c67e713aebe53382c651655a2be9e78ace8da6a1b7852c1e6acadcb383ff2422 -->
|
|
4
|
+
## [0.6.0] - 2026-08-21
|
|
5
|
+
|
|
6
|
+
This release adds bounded subprocess supervision (FND-ADR-012), completes the Foundation strict authority read path with contained nested directory preparation (FG-1), adds pre-persistence URL credential redaction (FG-2), and grows HARNESS_CAPABILITIES from 18 to 21.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Adds superviseProcess with validateTimeoutPolicy, WATCHDOG_REASONS, TERMINATION_REASONS, PROCESS_STATUSES and ENVELOPE_GUARANTEES (FND-ADR-012) - one bounded spawn, liveness by explicit events, consumer-supplied timeout policy, SIGTERM then grace then SIGKILL against the process group, and a single closed-enum termination envelope; the mechanism never restarts the supervised process and never holds timeout values.
|
|
11
|
+
- Adds readFileStrict (FG-1), the read-side twin of the strict write path - containment first, symlink refusal with O_NOFOLLOW, regular-file identity re-assertion on the opened handle (dev/ino), and a sha256 digest receipt of the exact bytes read; an optional expectedSha256 content guard fails closed before any delivery.
|
|
12
|
+
- Adds the createParents option to publishFileExclusive (FG-1) - the missing portion of the parent chain is prepared as real directories inside the containment layer and every intermediate entry is re-verified as a real directory; symlink components are still refused and no entry is ever replaced.
|
|
13
|
+
- Adds redactUrlCredentials with REDACTED_URL_PLACEHOLDER (FG-2) - strips the userinfo component from any URL before the value reaches disk or logs; unparseable input degrades to an opaque placeholder and never leaks to the output.
|
|
14
|
+
- Re-exports the contracts-owned token estimate consumption contract (consumeTokenEstimate, consumeTokenEstimateStrict and companions) next to estimateTokens, and carries the authoritative token estimator estimateTokens with the skill-family-token-estimate CLI (audit remediation C1).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Grows HARNESS_CAPABILITIES from 18 to 21 (adds supervise-process, strict-read and url-credential-redaction); the strict write path keeps its no-replace, byte-verified receipt semantics unchanged.
|
|
19
|
+
- Keeps business semantics, retry/restart policy, budget thresholds, and the decision of which values are URLs under consumer ownership; the harness owns mechanism only.
|
|
20
|
+
|
|
21
|
+
### Upgrade Notes
|
|
22
|
+
|
|
23
|
+
Version 0.6.0 is the Foundation capability completion line. The createParents option of publishFileExclusive is a Foundation-side profile behavior change under the 2026-08-19 discipline; consumers needing contained nested publication must pin exactly 0.6.0.
|
|
24
|
+
<!-- release-skill:changelog:end version=0.6.0 locale=en -->
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
<!-- release-skill:changelog:start version=0.5.0 locale=en baseline=sha256:03eca87a8c9bea814e741518168696f17c869bdb71baf2046cef4c3596d9ebf3 -->
|
|
28
|
+
## [0.5.0] - 2026-08-16
|
|
29
|
+
|
|
30
|
+
This release adds the declared read surface assertion (FND-ADR-010) and the structured surface scanner (FND-ADR-011) to the stable Harness surface, growing HARNESS_CAPABILITIES from 16 to 18 entries, and adds three dependency-reviewed runtime libraries.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Adds assertDeclaredReadSurface (FND-ADR-010): a no-execution, syntax-surface-only assertion that every node:fs named import inside a declared module set stays inside the consumer-declared read surface, with a closed violation vocabulary and a frozen declared-read-surface-result envelope.
|
|
35
|
+
- Adds scanSurfaceStructured (FND-ADR-011): the structured sibling of scanSurface. IP-shaped candidates enter a single standard parse entry (ipaddr.js) with consumer-declared CIDR approval and fail closed when unparseable; scoped and unscoped coordinates, registries and hosts need consumer-declared approval; format adapters (pnpm-lockfile via @pnpm/lockfile.fs with yaml AST comment regions, and tree-json) parse structurally with no position-level exemptions; binary and symlink policies fail closed. The closed nine-rule vocabulary travels in details.rule of the structured-scan-violation mechanism error.
|
|
36
|
+
- Adds three dependency-reviewed runtime libraries for the structured scanner, reviewed per FND-ADR-006 section-4 analogy (dependency-closure pre-review, executor self-review pending independent review): ipaddr.js 2.5.0 (MIT, zero dependencies), yaml 2.9.0 (ISC), and @pnpm/lockfile.fs 1001.1.35 (MIT, 18 transitive dependencies all inside the approved public coordinates).
|
|
37
|
+
- Grows HARNESS_CAPABILITIES from 16 to 18 entries for the two new mechanisms.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Documents the relationship of the two policy documents to workspace-private leak policies: a workspace-private leak-policy.json instance document is not a subset, not isomorphic and not a migration target of the surface-scan-policy or structured-scan-policy schemas — the documents share rule vocabulary and fail-closed semantics by design, but their byte-level shapes are independent and must not be compared for compatibility. scanSurface is the execution-core generalization projection: the public, consumer-parameterized form of the same mechanism family, without any private identity, path, or approval-list interpretation of its own.
|
|
42
|
+
- Records the cost of the dependency-review decision honestly: the harness tarball grows with the @pnpm/lockfile.fs closure, the thin-runtime property changes from zero third-party runtime dependencies to three reviewed ones, and the pnpm-lockfile adapter writes one temporary lockfile copy under the OS temp directory (removed afterwards). The engineering-kit offline-consumer verification gates now derive the complete third-party production closure of the three Foundation packages mechanically (identity-deduplicated, npm: alias-aware, range-scoped override selectors) instead of a single-package closure, so the review decision is continuously verified against the real installed bytes.
|
|
43
|
+
- Keeps mechanism purity: no execution of scanned files, no model calls, no network; symlinked entries are never followed.
|
|
44
|
+
|
|
45
|
+
### Upgrade Notes
|
|
46
|
+
|
|
47
|
+
Version 0.5.0 is the FND-ADR-010/011 harness line. Mechanism imports use the stable capability names published in HARNESS_CAPABILITIES; structured-scan policies must pass the structured-scan-policy contract validation of contract-spec 1.5.0.
|
|
48
|
+
<!-- release-skill:changelog:end version=0.5.0 locale=en -->
|
|
49
|
+
|
|
50
|
+
|
|
3
51
|
<!-- release-skill:changelog:start version=0.4.0 locale=en baseline=sha256:72d3fe048ced8eb44a84fabe6f1fdd2eb0c938b4f6bb98740474f72aec04d873 -->
|
|
4
52
|
## [0.4.0] - 2026-08-16
|
|
5
53
|
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
+
<!-- release-skill:changelog:start version=0.6.0 locale=zh-CN baseline=sha256:6a4a94f692909faf2090445638988b27841a8bca67a85fd7812864d656299175 -->
|
|
4
|
+
## [0.6.0] - 2026-08-21
|
|
5
|
+
|
|
6
|
+
本版新增有界子进程监督(FND-ADR-012),补齐 Foundation 严格权威读取路径与受收容嵌套目录准备(FG-1),新增落盘前 URL 凭证脱敏(FG-2),HARNESS_CAPABILITIES 从 18 项增至 21 项。
|
|
7
|
+
|
|
8
|
+
### 新增
|
|
9
|
+
|
|
10
|
+
- 新增 superviseProcess 与 validateTimeoutPolicy、WATCHDOG_REASONS、TERMINATION_REASONS、PROCESS_STATUSES、ENVELOPE_GUARANTEES(FND-ADR-012):单次有界 spawn、显式事件判活、消费者提供的超时策略、对进程组 SIGTERM→宽限→SIGKILL、单一闭集枚举终止信封;机制从不重启被监督进程,也从不持有超时数值。
|
|
11
|
+
- 新增 readFileStrict(FG-1):严格写入路径的读取孪生体——先收容、拒绝符号链接(O_NOFOLLOW)、在已打开句柄上复核常规文件身份(dev/ino),并返回所读精确字节的 sha256 摘要回执;可选 expectedSha256 内容守卫在任何交付前失败关闭。
|
|
12
|
+
- publishFileExclusive 新增 createParents 选项(FG-1):父目录链缺失部分在收容层内准备为真实目录,每个中间条目都复核为真实目录;符号链接组件仍被拒绝,任何条目都不被替换。
|
|
13
|
+
- 新增 redactUrlCredentials 与 REDACTED_URL_PLACEHOLDER(FG-2):任何 URL 的 userinfo 部分在数值到达磁盘或日志前剥离;不可解析输入退化为不透明占位符,绝不泄漏到输出。
|
|
14
|
+
- 在 estimateTokens 旁转导出 contracts 拥有的 token 估算消费合同(consumeTokenEstimate、consumeTokenEstimateStrict 及伴生常量),并承载权威词元估算器 estimateTokens 与 skill-family-token-estimate CLI(审计整改 C1)。
|
|
15
|
+
|
|
16
|
+
### 变更
|
|
17
|
+
|
|
18
|
+
- HARNESS_CAPABILITIES 从 18 项增至 21 项(新增 supervise-process、strict-read、url-credential-redaction);严格写入路径的不替换、字节验证回执语义保持不变。
|
|
19
|
+
- 业务语义、重试/重启策略、预算阈值与「哪些值是 URL」的判定继续归消费者所有;harness 只拥有机制。
|
|
20
|
+
|
|
21
|
+
### 升级说明
|
|
22
|
+
|
|
23
|
+
0.6.0 是 Foundation 能力补齐线。publishFileExclusive 的 createParents 选项是按 2026-08-19 纪律的 Foundation 侧 profile 行为变更;需要受收容嵌套发布的消费者必须精确锁定 0.6.0。
|
|
24
|
+
<!-- release-skill:changelog:end version=0.6.0 locale=zh-CN -->
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
<!-- release-skill:changelog:start version=0.5.0 locale=zh-CN baseline=sha256:003472359596147fe26ad2ba53a82303a217c28cb89a6afacdaa955793001f71 -->
|
|
28
|
+
## [0.5.0] - 2026-08-16
|
|
29
|
+
|
|
30
|
+
本版在稳定 Harness 面上新增声明读取表面断言(FND-ADR-010)与结构化表面扫描器(FND-ADR-011),HARNESS_CAPABILITIES 从 16 项增至 18 项,并引入三个经依赖评审的运行时库。
|
|
31
|
+
|
|
32
|
+
### 新增
|
|
33
|
+
|
|
34
|
+
- 新增 assertDeclaredReadSurface(FND-ADR-010):不执行、仅语法面的断言,声明模块集内的每个 node:fs 具名导入必须落在消费者声明的读取表面内,违规词汇表为闭集,返回冻结的 declared-read-surface-result 信封。
|
|
35
|
+
- 新增 scanSurfaceStructured(FND-ADR-011):scanSurface 的结构化兄弟。IP 形候选统一进入单一标准解析入口(ipaddr.js),按消费者声明的 CIDR 批准,不可解析即失败关闭;坐标(scoped 与非 scoped)、注册表与主机均需消费者声明批准;格式适配器(pnpm-lockfile 经 @pnpm/lockfile.fs + yaml AST 注释区域,tree-json 经 JSON.parse)结构化解析且无位置级豁免;二进制与符号链接策略失败关闭。闭集九规则词汇表经 structured-scan-violation 机制错误的 details.rule 承载。
|
|
36
|
+
- 为结构化扫描器引入三个经依赖评审的运行时库,评审按 FND-ADR-006 第 4 节类比执行(依赖闭包预审,执行者自审,独立复核另行安排):ipaddr.js 2.5.0(MIT,零依赖)、yaml 2.9.0(ISC)、@pnpm/lockfile.fs 1001.1.35(MIT,18 个传递依赖全部在批准公共坐标内)。
|
|
37
|
+
- HARNESS_CAPABILITIES 从 16 项增至 18 项,覆盖两个新机制。
|
|
38
|
+
|
|
39
|
+
### 变更
|
|
40
|
+
|
|
41
|
+
- 显式说明两个策略文档与工作区私有 leak 策略的关系:工作区私有的 leak-policy.json 实例文档既不是 surface-scan-policy 或 structured-scan-policy schema 的子集、也不同构、更不是迁移目标——两类文档按设计共享规则词汇与失败关闭语义,但字节级形状相互独立,不得比较兼容性。scanSurface 是执行内核通用化投影:同一机制族的公开、消费者参数化形态,自身不解释任何私有身份、路径或批准清单。
|
|
42
|
+
- 如实记录依赖评审决策的成本:harness tarball 随 @pnpm/lockfile.fs 闭包扩大;薄运行时属性从零第三方运行时依赖变为三个经评审依赖;pnpm-lockfile 适配器会在 OS 临时目录写一份临时锁文件副本(用后即删)。engineering-kit 的离线消费者验证门随之把第三方闭包推导从单包闭包机械扩展为三个 Foundation 包的完整生产闭包(真实身份去重、npm: 别名感知、range-scoped override selector),使评审决策持续对着真实安装字节被验证。
|
|
43
|
+
- 保持机制纯度:不执行被扫描文件、无模型调用、无网络;永不跟随符号链接。
|
|
44
|
+
|
|
45
|
+
### 升级说明
|
|
46
|
+
|
|
47
|
+
0.5.0 是 FND-ADR-010/011 harness 线。机制导入使用 HARNESS_CAPABILITIES 公布的稳定能力名;structured-scan 策略必须通过契约规格 1.5.0 的 structured-scan-policy 契约校验。
|
|
48
|
+
<!-- release-skill:changelog:end version=0.5.0 locale=zh-CN -->
|
|
49
|
+
|
|
50
|
+
|
|
3
51
|
<!-- release-skill:changelog:start version=0.4.0 locale=zh-CN baseline=sha256:dafaa1100bf6163a28d53fc982a30e8325d8bd3cab43bbb6daf2e6002befa2c1 -->
|
|
4
52
|
## [0.4.0] - 2026-08-16
|
|
5
53
|
|
package/README.md
CHANGED
|
@@ -4,30 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
# skill-family-harness-node
|
|
6
6
|
|
|
7
|
-
<!-- release-skill:release-version: 0.
|
|
7
|
+
<!-- release-skill:release-version: 0.6.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.6.0** (2026-08-21)
|
|
13
13
|
|
|
14
|
-
This release adds
|
|
14
|
+
This release adds bounded subprocess supervision (FND-ADR-012), completes the Foundation strict authority read path with contained nested directory preparation (FG-1), adds pre-persistence URL credential redaction (FG-2), and grows HARNESS_CAPABILITIES from 18 to 21.
|
|
15
15
|
|
|
16
16
|
**Added**
|
|
17
17
|
|
|
18
|
-
- Adds
|
|
19
|
-
- Adds the
|
|
20
|
-
-
|
|
21
|
-
- Adds
|
|
18
|
+
- Adds superviseProcess with validateTimeoutPolicy, WATCHDOG_REASONS, TERMINATION_REASONS, PROCESS_STATUSES and ENVELOPE_GUARANTEES (FND-ADR-012) - one bounded spawn, liveness by explicit events, consumer-supplied timeout policy, SIGTERM then grace then SIGKILL against the process group, and a single closed-enum termination envelope; the mechanism never restarts the supervised process and never holds timeout values.
|
|
19
|
+
- Adds readFileStrict (FG-1), the read-side twin of the strict write path - containment first, symlink refusal with O_NOFOLLOW, regular-file identity re-assertion on the opened handle (dev/ino), and a sha256 digest receipt of the exact bytes read; an optional expectedSha256 content guard fails closed before any delivery.
|
|
20
|
+
- Adds the createParents option to publishFileExclusive (FG-1) - the missing portion of the parent chain is prepared as real directories inside the containment layer and every intermediate entry is re-verified as a real directory; symlink components are still refused and no entry is ever replaced.
|
|
21
|
+
- Adds redactUrlCredentials with REDACTED_URL_PLACEHOLDER (FG-2) - strips the userinfo component from any URL before the value reaches disk or logs; unparseable input degrades to an opaque placeholder and never leaks to the output.
|
|
22
|
+
- Re-exports the contracts-owned token estimate consumption contract (consumeTokenEstimate, consumeTokenEstimateStrict and companions) next to estimateTokens, and carries the authoritative token estimator estimateTokens with the skill-family-token-estimate CLI (audit remediation C1).
|
|
22
23
|
|
|
23
24
|
**Changed**
|
|
24
25
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
26
|
+
- Grows HARNESS_CAPABILITIES from 18 to 21 (adds supervise-process, strict-read and url-credential-redaction); the strict write path keeps its no-replace, byte-verified receipt semantics unchanged.
|
|
27
|
+
- Keeps business semantics, retry/restart policy, budget thresholds, and the decision of which values are URLs under consumer ownership; the harness owns mechanism only.
|
|
27
28
|
|
|
28
29
|
**Upgrade Notes**
|
|
29
30
|
|
|
30
|
-
Version 0.
|
|
31
|
+
Version 0.6.0 is the Foundation capability completion line. The createParents option of publishFileExclusive is a Foundation-side profile behavior change under the 2026-08-19 discipline; consumers needing contained nested publication must pin exactly 0.6.0.
|
|
31
32
|
<!-- release-skill:managed:end id=latest-release -->
|
|
32
33
|
|
|
33
34
|
## Problem It Solves
|
|
@@ -41,14 +42,14 @@ The Harness consumes `skill-family-contracts` (a workspace dependency), reusing
|
|
|
41
42
|
## Installation and Minimal Example
|
|
42
43
|
|
|
43
44
|
```sh
|
|
44
|
-
npm install skill-family-harness-node@0.
|
|
45
|
+
npm install skill-family-harness-node@0.6.0
|
|
45
46
|
npm info skill-family-harness-node --help
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
The minimal example shows validating a contract document inside Node:
|
|
49
50
|
|
|
50
51
|
```js
|
|
51
|
-
// Run from an empty directory: npm install skill-family-harness-node@0.
|
|
52
|
+
// Run from an empty directory: npm install skill-family-harness-node@0.6.0
|
|
52
53
|
import { validateContractDocument } from "skill-family-harness-node";
|
|
53
54
|
|
|
54
55
|
const document = {
|
package/README.zh-CN.md
CHANGED
|
@@ -5,30 +5,31 @@
|
|
|
5
5
|
|
|
6
6
|
# skill-family-harness-node
|
|
7
7
|
|
|
8
|
-
<!-- release-skill:release-version: 0.
|
|
8
|
+
<!-- release-skill:release-version: 0.6.0 -->
|
|
9
9
|
|
|
10
10
|
Contracts 机制协议的**唯一默认 Node 实现**。这是一个薄运行时(thin runtime):只实现机制协议,不引入业务语义,不做第二语言实现。
|
|
11
11
|
|
|
12
12
|
<!-- release-skill:managed:start id=latest-release -->
|
|
13
|
-
**0.
|
|
13
|
+
**0.6.0** (2026-08-21)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
本版新增有界子进程监督(FND-ADR-012),补齐 Foundation 严格权威读取路径与受收容嵌套目录准备(FG-1),新增落盘前 URL 凭证脱敏(FG-2),HARNESS_CAPABILITIES 从 18 项增至 21 项。
|
|
16
16
|
|
|
17
17
|
**新增**
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
- 新增
|
|
21
|
-
-
|
|
22
|
-
- 新增
|
|
19
|
+
- 新增 superviseProcess 与 validateTimeoutPolicy、WATCHDOG_REASONS、TERMINATION_REASONS、PROCESS_STATUSES、ENVELOPE_GUARANTEES(FND-ADR-012):单次有界 spawn、显式事件判活、消费者提供的超时策略、对进程组 SIGTERM→宽限→SIGKILL、单一闭集枚举终止信封;机制从不重启被监督进程,也从不持有超时数值。
|
|
20
|
+
- 新增 readFileStrict(FG-1):严格写入路径的读取孪生体——先收容、拒绝符号链接(O_NOFOLLOW)、在已打开句柄上复核常规文件身份(dev/ino),并返回所读精确字节的 sha256 摘要回执;可选 expectedSha256 内容守卫在任何交付前失败关闭。
|
|
21
|
+
- publishFileExclusive 新增 createParents 选项(FG-1):父目录链缺失部分在收容层内准备为真实目录,每个中间条目都复核为真实目录;符号链接组件仍被拒绝,任何条目都不被替换。
|
|
22
|
+
- 新增 redactUrlCredentials 与 REDACTED_URL_PLACEHOLDER(FG-2):任何 URL 的 userinfo 部分在数值到达磁盘或日志前剥离;不可解析输入退化为不透明占位符,绝不泄漏到输出。
|
|
23
|
+
- 在 estimateTokens 旁转导出 contracts 拥有的 token 估算消费合同(consumeTokenEstimate、consumeTokenEstimateStrict 及伴生常量),并承载权威词元估算器 estimateTokens 与 skill-family-token-estimate CLI(审计整改 C1)。
|
|
23
24
|
|
|
24
25
|
**变更**
|
|
25
26
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
27
|
+
- HARNESS_CAPABILITIES 从 18 项增至 21 项(新增 supervise-process、strict-read、url-credential-redaction);严格写入路径的不替换、字节验证回执语义保持不变。
|
|
28
|
+
- 业务语义、重试/重启策略、预算阈值与「哪些值是 URL」的判定继续归消费者所有;harness 只拥有机制。
|
|
28
29
|
|
|
29
30
|
**升级说明**
|
|
30
31
|
|
|
31
|
-
0.
|
|
32
|
+
0.6.0 是 Foundation 能力补齐线。publishFileExclusive 的 createParents 选项是按 2026-08-19 纪律的 Foundation 侧 profile 行为变更;需要受收容嵌套发布的消费者必须精确锁定 0.6.0。
|
|
32
33
|
<!-- release-skill:managed:end id=latest-release -->
|
|
33
34
|
|
|
34
35
|
## 解决的问题
|
|
@@ -42,14 +43,14 @@ Harness 消费 `skill-family-contracts`(工作区依赖),复用其方言
|
|
|
42
43
|
## 安装和最小示例
|
|
43
44
|
|
|
44
45
|
```sh
|
|
45
|
-
npm install skill-family-harness-node@0.
|
|
46
|
+
npm install skill-family-harness-node@0.6.0
|
|
46
47
|
npm info skill-family-harness-node --help
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
最小示例演示在 Node 内校验一份契约文档:
|
|
50
51
|
|
|
51
52
|
```js
|
|
52
|
-
// 从空目录运行:npm install skill-family-harness-node@0.
|
|
53
|
+
// 从空目录运行:npm install skill-family-harness-node@0.6.0
|
|
53
54
|
import { validateContractDocument } from "skill-family-harness-node";
|
|
54
55
|
|
|
55
56
|
const document = {
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\".",
|
|
3
3
|
"author": "广州市风荷科技有限公司",
|
|
4
|
+
"bin": {
|
|
5
|
+
"skill-family-token-estimate": "./src/token-estimate-cli.mjs"
|
|
6
|
+
},
|
|
4
7
|
"bugs": {
|
|
5
8
|
"url": "https://github.com/ifoohoo/skill-family-harness-node/issues"
|
|
6
9
|
},
|
|
7
10
|
"dependencies": {
|
|
8
|
-
"
|
|
11
|
+
"@pnpm/lockfile.fs": "1001.1.35",
|
|
12
|
+
"ipaddr.js": "2.5.0",
|
|
13
|
+
"skill-family-contracts": "0.6.0",
|
|
14
|
+
"yaml": "2.9.0"
|
|
9
15
|
},
|
|
10
16
|
"description": "Thin Node.js mechanism runtime for Skill Family engineering contracts.",
|
|
11
17
|
"engines": {
|
|
@@ -39,7 +45,7 @@
|
|
|
39
45
|
"url": "https://github.com/ifoohoo/skill-family-harness-node.git"
|
|
40
46
|
},
|
|
41
47
|
"type": "module",
|
|
42
|
-
"version": "0.
|
|
48
|
+
"version": "0.6.0",
|
|
43
49
|
"scripts": {
|
|
44
50
|
"check": "node --test",
|
|
45
51
|
"test": "node --test"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
version: 0.5.0
|
|
2
|
+
date: 2026-08-16
|
|
3
|
+
locales:
|
|
4
|
+
en:
|
|
5
|
+
summary: This release adds the declared read surface assertion (FND-ADR-010) and the structured surface scanner (FND-ADR-011) to the stable Harness surface, growing HARNESS_CAPABILITIES from 16 to 18 entries, and adds three dependency-reviewed runtime libraries.
|
|
6
|
+
changes:
|
|
7
|
+
added:
|
|
8
|
+
- "Adds assertDeclaredReadSurface (FND-ADR-010): a no-execution, syntax-surface-only assertion that every node:fs named import inside a declared module set stays inside the consumer-declared read surface, with a closed violation vocabulary and a frozen declared-read-surface-result envelope."
|
|
9
|
+
- "Adds scanSurfaceStructured (FND-ADR-011): the structured sibling of scanSurface. IP-shaped candidates enter a single standard parse entry (ipaddr.js) with consumer-declared CIDR approval and fail closed when unparseable; scoped and unscoped coordinates, registries and hosts need consumer-declared approval; format adapters (pnpm-lockfile via @pnpm/lockfile.fs with yaml AST comment regions, and tree-json) parse structurally with no position-level exemptions; binary and symlink policies fail closed. The closed nine-rule vocabulary travels in details.rule of the structured-scan-violation mechanism error."
|
|
10
|
+
- "Adds three dependency-reviewed runtime libraries for the structured scanner, reviewed per FND-ADR-006 section-4 analogy (dependency-closure pre-review, executor self-review pending independent review): ipaddr.js 2.5.0 (MIT, zero dependencies), yaml 2.9.0 (ISC), and @pnpm/lockfile.fs 1001.1.35 (MIT, 18 transitive dependencies all inside the approved public coordinates)."
|
|
11
|
+
- Grows HARNESS_CAPABILITIES from 16 to 18 entries for the two new mechanisms.
|
|
12
|
+
changed:
|
|
13
|
+
- "Documents the relationship of the two policy documents to workspace-private leak policies: a workspace-private leak-policy.json instance document is not a subset, not isomorphic and not a migration target of the surface-scan-policy or structured-scan-policy schemas — the documents share rule vocabulary and fail-closed semantics by design, but their byte-level shapes are independent and must not be compared for compatibility. scanSurface is the execution-core generalization projection: the public, consumer-parameterized form of the same mechanism family, without any private identity, path, or approval-list interpretation of its own."
|
|
14
|
+
- "Records the cost of the dependency-review decision honestly: the harness tarball grows with the @pnpm/lockfile.fs closure, the thin-runtime property changes from zero third-party runtime dependencies to three reviewed ones, and the pnpm-lockfile adapter writes one temporary lockfile copy under the OS temp directory (removed afterwards). The engineering-kit offline-consumer verification gates now derive the complete third-party production closure of the three Foundation packages mechanically (identity-deduplicated, npm: alias-aware, range-scoped override selectors) instead of a single-package closure, so the review decision is continuously verified against the real installed bytes."
|
|
15
|
+
- "Keeps mechanism purity: no execution of scanned files, no model calls, no network; symlinked entries are never followed."
|
|
16
|
+
upgradeNotes: Version 0.5.0 is the FND-ADR-010/011 harness line. Mechanism imports use the stable capability names published in HARNESS_CAPABILITIES; structured-scan policies must pass the structured-scan-policy contract validation of contract-spec 1.5.0.
|
|
17
|
+
zh-CN:
|
|
18
|
+
summary: 本版在稳定 Harness 面上新增声明读取表面断言(FND-ADR-010)与结构化表面扫描器(FND-ADR-011),HARNESS_CAPABILITIES 从 16 项增至 18 项,并引入三个经依赖评审的运行时库。
|
|
19
|
+
changes:
|
|
20
|
+
added:
|
|
21
|
+
- 新增 assertDeclaredReadSurface(FND-ADR-010):不执行、仅语法面的断言,声明模块集内的每个 node:fs 具名导入必须落在消费者声明的读取表面内,违规词汇表为闭集,返回冻结的 declared-read-surface-result 信封。
|
|
22
|
+
- 新增 scanSurfaceStructured(FND-ADR-011):scanSurface 的结构化兄弟。IP 形候选统一进入单一标准解析入口(ipaddr.js),按消费者声明的 CIDR 批准,不可解析即失败关闭;坐标(scoped 与非 scoped)、注册表与主机均需消费者声明批准;格式适配器(pnpm-lockfile 经 @pnpm/lockfile.fs + yaml AST 注释区域,tree-json 经 JSON.parse)结构化解析且无位置级豁免;二进制与符号链接策略失败关闭。闭集九规则词汇表经 structured-scan-violation 机制错误的 details.rule 承载。
|
|
23
|
+
- 为结构化扫描器引入三个经依赖评审的运行时库,评审按 FND-ADR-006 第 4 节类比执行(依赖闭包预审,执行者自审,独立复核另行安排):ipaddr.js 2.5.0(MIT,零依赖)、yaml 2.9.0(ISC)、@pnpm/lockfile.fs 1001.1.35(MIT,18 个传递依赖全部在批准公共坐标内)。
|
|
24
|
+
- HARNESS_CAPABILITIES 从 16 项增至 18 项,覆盖两个新机制。
|
|
25
|
+
changed:
|
|
26
|
+
- 显式说明两个策略文档与工作区私有 leak 策略的关系:工作区私有的 leak-policy.json 实例文档既不是 surface-scan-policy 或 structured-scan-policy schema 的子集、也不同构、更不是迁移目标——两类文档按设计共享规则词汇与失败关闭语义,但字节级形状相互独立,不得比较兼容性。scanSurface 是执行内核通用化投影:同一机制族的公开、消费者参数化形态,自身不解释任何私有身份、路径或批准清单。
|
|
27
|
+
- "如实记录依赖评审决策的成本:harness tarball 随 @pnpm/lockfile.fs 闭包扩大;薄运行时属性从零第三方运行时依赖变为三个经评审依赖;pnpm-lockfile 适配器会在 OS 临时目录写一份临时锁文件副本(用后即删)。engineering-kit 的离线消费者验证门随之把第三方闭包推导从单包闭包机械扩展为三个 Foundation 包的完整生产闭包(真实身份去重、npm: 别名感知、range-scoped override selector),使评审决策持续对着真实安装字节被验证。"
|
|
28
|
+
- 保持机制纯度:不执行被扫描文件、无模型调用、无网络;永不跟随符号链接。
|
|
29
|
+
upgradeNotes: 0.5.0 是 FND-ADR-010/011 harness 线。机制导入使用 HARNESS_CAPABILITIES 公布的稳定能力名;structured-scan 策略必须通过契约规格 1.5.0 的 structured-scan-policy 契约校验。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
version: 0.6.0
|
|
2
|
+
date: 2026-08-21
|
|
3
|
+
locales:
|
|
4
|
+
en:
|
|
5
|
+
summary: This release adds bounded subprocess supervision (FND-ADR-012), completes the Foundation strict authority read path with contained nested directory preparation (FG-1), adds pre-persistence URL credential redaction (FG-2), and grows HARNESS_CAPABILITIES from 18 to 21.
|
|
6
|
+
changes:
|
|
7
|
+
added:
|
|
8
|
+
- Adds superviseProcess with validateTimeoutPolicy, WATCHDOG_REASONS, TERMINATION_REASONS, PROCESS_STATUSES and ENVELOPE_GUARANTEES (FND-ADR-012) - one bounded spawn, liveness by explicit events, consumer-supplied timeout policy, SIGTERM then grace then SIGKILL against the process group, and a single closed-enum termination envelope; the mechanism never restarts the supervised process and never holds timeout values.
|
|
9
|
+
- Adds readFileStrict (FG-1), the read-side twin of the strict write path - containment first, symlink refusal with O_NOFOLLOW, regular-file identity re-assertion on the opened handle (dev/ino), and a sha256 digest receipt of the exact bytes read; an optional expectedSha256 content guard fails closed before any delivery.
|
|
10
|
+
- Adds the createParents option to publishFileExclusive (FG-1) - the missing portion of the parent chain is prepared as real directories inside the containment layer and every intermediate entry is re-verified as a real directory; symlink components are still refused and no entry is ever replaced.
|
|
11
|
+
- Adds redactUrlCredentials with REDACTED_URL_PLACEHOLDER (FG-2) - strips the userinfo component from any URL before the value reaches disk or logs; unparseable input degrades to an opaque placeholder and never leaks to the output.
|
|
12
|
+
- Re-exports the contracts-owned token estimate consumption contract (consumeTokenEstimate, consumeTokenEstimateStrict and companions) next to estimateTokens, and carries the authoritative token estimator estimateTokens with the skill-family-token-estimate CLI (audit remediation C1).
|
|
13
|
+
changed:
|
|
14
|
+
- Grows HARNESS_CAPABILITIES from 18 to 21 (adds supervise-process, strict-read and url-credential-redaction); the strict write path keeps its no-replace, byte-verified receipt semantics unchanged.
|
|
15
|
+
- Keeps business semantics, retry/restart policy, budget thresholds, and the decision of which values are URLs under consumer ownership; the harness owns mechanism only.
|
|
16
|
+
upgradeNotes: Version 0.6.0 is the Foundation capability completion line. The createParents option of publishFileExclusive is a Foundation-side profile behavior change under the 2026-08-19 discipline; consumers needing contained nested publication must pin exactly 0.6.0.
|
|
17
|
+
zh-CN:
|
|
18
|
+
summary: 本版新增有界子进程监督(FND-ADR-012),补齐 Foundation 严格权威读取路径与受收容嵌套目录准备(FG-1),新增落盘前 URL 凭证脱敏(FG-2),HARNESS_CAPABILITIES 从 18 项增至 21 项。
|
|
19
|
+
changes:
|
|
20
|
+
added:
|
|
21
|
+
- 新增 superviseProcess 与 validateTimeoutPolicy、WATCHDOG_REASONS、TERMINATION_REASONS、PROCESS_STATUSES、ENVELOPE_GUARANTEES(FND-ADR-012):单次有界 spawn、显式事件判活、消费者提供的超时策略、对进程组 SIGTERM→宽限→SIGKILL、单一闭集枚举终止信封;机制从不重启被监督进程,也从不持有超时数值。
|
|
22
|
+
- 新增 readFileStrict(FG-1):严格写入路径的读取孪生体——先收容、拒绝符号链接(O_NOFOLLOW)、在已打开句柄上复核常规文件身份(dev/ino),并返回所读精确字节的 sha256 摘要回执;可选 expectedSha256 内容守卫在任何交付前失败关闭。
|
|
23
|
+
- publishFileExclusive 新增 createParents 选项(FG-1):父目录链缺失部分在收容层内准备为真实目录,每个中间条目都复核为真实目录;符号链接组件仍被拒绝,任何条目都不被替换。
|
|
24
|
+
- 新增 redactUrlCredentials 与 REDACTED_URL_PLACEHOLDER(FG-2):任何 URL 的 userinfo 部分在数值到达磁盘或日志前剥离;不可解析输入退化为不透明占位符,绝不泄漏到输出。
|
|
25
|
+
- 在 estimateTokens 旁转导出 contracts 拥有的 token 估算消费合同(consumeTokenEstimate、consumeTokenEstimateStrict 及伴生常量),并承载权威词元估算器 estimateTokens 与 skill-family-token-estimate CLI(审计整改 C1)。
|
|
26
|
+
changed:
|
|
27
|
+
- HARNESS_CAPABILITIES 从 18 项增至 21 项(新增 supervise-process、strict-read、url-credential-redaction);严格写入路径的不替换、字节验证回执语义保持不变。
|
|
28
|
+
- 业务语义、重试/重启策略、预算阈值与「哪些值是 URL」的判定继续归消费者所有;harness 只拥有机制。
|
|
29
|
+
upgradeNotes: 0.6.0 是 Foundation 能力补齐线。publishFileExclusive 的 createParents 选项是按 2026-08-19 纪律的 Foundation 侧 profile 行为变更;需要受收容嵌套发布的消费者必须精确锁定 0.6.0。
|
package/src/atomic.mjs
CHANGED
|
@@ -90,7 +90,30 @@ async function runTestHook(name, context) {
|
|
|
90
90
|
if (typeof testHooks?.[name] === "function") await testHooks[name](context);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async function
|
|
93
|
+
async function createParentDirectory(cursor, relPath) {
|
|
94
|
+
try {
|
|
95
|
+
await mkdir(cursor);
|
|
96
|
+
} catch (cause) {
|
|
97
|
+
// EEXIST is a benign concurrent creation race: re-verify the entry below.
|
|
98
|
+
if (cause?.code !== "EEXIST") {
|
|
99
|
+
throw mechanismError(
|
|
100
|
+
HARNESS_ERROR_KINDS.INVALID_ROOT,
|
|
101
|
+
"strict file operation could not create a missing parent directory",
|
|
102
|
+
{ input: relPath, code: cause?.code },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const created = await lstat(cursor);
|
|
107
|
+
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
108
|
+
throw mechanismError(
|
|
109
|
+
HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY,
|
|
110
|
+
"strict file operation requires real intermediate directories",
|
|
111
|
+
{ input: relPath },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function strictTarget(root, relPath, { target = "optional", createParents = false } = {}) {
|
|
94
117
|
const lexical = await resolveContained(root, relPath);
|
|
95
118
|
const rootReal = await realpath(root);
|
|
96
119
|
const relative = path.relative(rootReal, lexical);
|
|
@@ -102,6 +125,10 @@ async function strictTarget(root, relPath, { target = "optional" } = {}) {
|
|
|
102
125
|
try {
|
|
103
126
|
stats = await lstat(cursor);
|
|
104
127
|
} catch (cause) {
|
|
128
|
+
if (createParents && cause?.code === "ENOENT") {
|
|
129
|
+
await createParentDirectory(cursor, relPath);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
105
132
|
throw mechanismError(
|
|
106
133
|
HARNESS_ERROR_KINDS.INVALID_ROOT,
|
|
107
134
|
"strict file operation requires every parent directory to exist",
|
|
@@ -267,17 +294,25 @@ function strictFailure(kind, operation, cause, relPath, committed, verified) {
|
|
|
267
294
|
}
|
|
268
295
|
|
|
269
296
|
/**
|
|
270
|
-
* Creates one file without replacing any existing directory entry.
|
|
271
|
-
* parent must
|
|
297
|
+
* Creates one file without replacing any existing directory entry. Every
|
|
298
|
+
* parent must be a real directory and contain no symlink components; with
|
|
299
|
+
* `createParents` enabled, the missing portion of the parent chain is created
|
|
300
|
+
* as real directories (any symlink component is still refused). The returned
|
|
272
301
|
* receipt is emitted only after byte/mode/identity verification and directory
|
|
273
302
|
* fsync have completed.
|
|
274
303
|
*/
|
|
275
|
-
export async function publishFileExclusive(
|
|
304
|
+
export async function publishFileExclusive(
|
|
305
|
+
root,
|
|
306
|
+
relPath,
|
|
307
|
+
data,
|
|
308
|
+
{ mode = 0o644, createParents = false } = {},
|
|
309
|
+
) {
|
|
276
310
|
assertWritableData(data, "publishFileExclusive");
|
|
277
311
|
const normalizedMode = normalizeMode(mode);
|
|
278
312
|
const bytes = bytesOf(data);
|
|
279
313
|
const { targetPath, directory, directoryStats, directoryReal } = await strictTarget(root, relPath, {
|
|
280
314
|
target: "absent",
|
|
315
|
+
createParents,
|
|
281
316
|
});
|
|
282
317
|
const staging = path.join(
|
|
283
318
|
directory,
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Static declared read surface verification (FND-ADR-010).
|
|
7
|
+
*
|
|
8
|
+
* assertDeclaredReadSurface({ root, declaredReaders }) checks, by static
|
|
9
|
+
* source analysis, that every `.mjs`/`.js` module under `root` only reads
|
|
10
|
+
* node:fs through the consumer-declared reader set:
|
|
11
|
+
*
|
|
12
|
+
* - a module that imports named APIs from "node:fs" without being declared
|
|
13
|
+
* violates rule `undeclared-module-imports-fs`;
|
|
14
|
+
* - a declared module importing an API outside its declared set violates
|
|
15
|
+
* rule `fs-api-outside-declared-set`;
|
|
16
|
+
* - any write-family fs API name appearing anywhere in the tree (whole
|
|
17
|
+
* source substring detection, including comments and string literals)
|
|
18
|
+
* violates rule `write-family-fs-api` (conservative approximation: static
|
|
19
|
+
* syntax-level violations are never under-reported).
|
|
20
|
+
*
|
|
21
|
+
* The verdict semantics mirror the audit tree's P3 check one-to-one; the
|
|
22
|
+
* result is shaped as the contracts `declared-read-surface-result` envelope
|
|
23
|
+
* whose scope, rule enumeration and guarantees are the machine contract.
|
|
24
|
+
* Input is fully parameterized: root and declaredReaders are consumer data,
|
|
25
|
+
* the mechanism knows no audit paths, no workspace layout, and no business
|
|
26
|
+
* meaning of any module.
|
|
27
|
+
*
|
|
28
|
+
* Violations are collected as an evidence list (deterministic order:
|
|
29
|
+
* sorted module list, then rule order per module) rather than stopping at
|
|
30
|
+
* the first hit; `ok` is false exactly when the list is non-empty.
|
|
31
|
+
*
|
|
32
|
+
* Mechanism purity: read-only (never writes, never executes the scanned
|
|
33
|
+
* modules, no model call, no network); symlinked entries under the root are
|
|
34
|
+
* never followed and never scanned (conservative: a symlink may not smuggle
|
|
35
|
+
* a module out of the declared surface).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const MODULE_EXTENSIONS = [".mjs", ".js"];
|
|
39
|
+
const FS_NAMED_IMPORT_PATTERN = /import\s*\{([^}]*)\}\s*from\s*"node:fs"/g;
|
|
40
|
+
|
|
41
|
+
// Write-family API names are built by composition so this module's own
|
|
42
|
+
// source never contains the full forbidden names as literals (self-hit
|
|
43
|
+
// hygiene, inherited from the audit tree's P3 check). Detection of scanned
|
|
44
|
+
// modules is unaffected: they would spell the full names.
|
|
45
|
+
const WRITE_FS_VERBS = ["writeFile", "appendFile", "mkdir", "rm", "unlink", "rename", "copyFile"];
|
|
46
|
+
const WRITE_FS_APIS = WRITE_FS_VERBS.map((verb) => `${verb}Sync`).concat([
|
|
47
|
+
`${"createWrite"}Stream`,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export const DECLARED_READ_SURFACE_RULES = Object.freeze({
|
|
51
|
+
UNDECLARED_MODULE_IMPORTS_FS: "undeclared-module-imports-fs",
|
|
52
|
+
FS_API_OUTSIDE_DECLARED_SET: "fs-api-outside-declared-set",
|
|
53
|
+
WRITE_FAMILY_FS_API: "write-family-fs-api",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const RESULT_GUARANTEES = Object.freeze([
|
|
57
|
+
"syntax-surface-only",
|
|
58
|
+
"conservative-approximation",
|
|
59
|
+
"no-execution",
|
|
60
|
+
"no-model-calls",
|
|
61
|
+
"no-network-access",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function invalidInput(reason, extra) {
|
|
65
|
+
return mechanismError(
|
|
66
|
+
HARNESS_ERROR_KINDS.DECLARED_READ_SURFACE_INVALID,
|
|
67
|
+
`assertDeclaredReadSurface: ${reason}`,
|
|
68
|
+
extra,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertPlainObject(value, label) {
|
|
73
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
74
|
+
throw invalidInput(`${label} must be a plain object`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isValidDeclaredModulePath(modulePath) {
|
|
79
|
+
if (typeof modulePath !== "string" || modulePath.length === 0) return false;
|
|
80
|
+
if (modulePath.includes("\0")) return false;
|
|
81
|
+
if (modulePath.includes("\\")) return false;
|
|
82
|
+
if (path.posix.isAbsolute(modulePath)) return false;
|
|
83
|
+
if (/^[A-Za-z]:/.test(modulePath)) return false;
|
|
84
|
+
const normalized = path.posix.normalize(modulePath);
|
|
85
|
+
if (normalized === "." || normalized === "..") return false;
|
|
86
|
+
if (normalized.split("/").some((segment) => segment === "..")) return false;
|
|
87
|
+
return normalized === modulePath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function collectModules(root) {
|
|
91
|
+
const modules = [];
|
|
92
|
+
const walk = (directory) => {
|
|
93
|
+
const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
|
|
94
|
+
a.name.localeCompare(b.name),
|
|
95
|
+
);
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (entry.isSymbolicLink()) continue; // never followed, never scanned
|
|
98
|
+
const fullPath = path.join(directory, entry.name);
|
|
99
|
+
if (entry.isDirectory()) {
|
|
100
|
+
walk(fullPath);
|
|
101
|
+
} else if (entry.isFile() && MODULE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
|
|
102
|
+
modules.push(fullPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
walk(root);
|
|
107
|
+
return modules;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function namedFsImports(source) {
|
|
111
|
+
return [...source.matchAll(FS_NAMED_IMPORT_PATTERN)].flatMap((match) =>
|
|
112
|
+
match[1].split(",").map((api) => api.trim()).filter(Boolean),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Static declared read surface verification. Returns the contracts
|
|
118
|
+
* declared-read-surface-result envelope (frozen). Invalid inputs (non-empty
|
|
119
|
+
* root that is not a directory, empty declaredReaders, illegal module path
|
|
120
|
+
* shapes, malformed declared sets) throw SFC2004 with details.kind
|
|
121
|
+
* declared-read-surface-invalid; a detected violation is reported in the
|
|
122
|
+
* result's violations list (ok: false), never as an exception.
|
|
123
|
+
*/
|
|
124
|
+
export function assertDeclaredReadSurface({ root, declaredReaders } = {}) {
|
|
125
|
+
if (typeof root !== "string" || root.length === 0) {
|
|
126
|
+
throw invalidInput("root must be a non-empty path");
|
|
127
|
+
}
|
|
128
|
+
assertPlainObject(declaredReaders, "declaredReaders");
|
|
129
|
+
const readerKeys = Object.keys(declaredReaders);
|
|
130
|
+
if (readerKeys.length === 0) {
|
|
131
|
+
throw invalidInput("declaredReaders must declare at least one reader module");
|
|
132
|
+
}
|
|
133
|
+
const declared = new Map();
|
|
134
|
+
for (const key of readerKeys) {
|
|
135
|
+
if (!isValidDeclaredModulePath(key)) {
|
|
136
|
+
throw invalidInput(`declared reader module path has an illegal shape: ${JSON.stringify(key)}`);
|
|
137
|
+
}
|
|
138
|
+
const apis = declaredReaders[key];
|
|
139
|
+
if (!Array.isArray(apis)) {
|
|
140
|
+
throw invalidInput(`declared set for ${key} must be an array of fs API names`);
|
|
141
|
+
}
|
|
142
|
+
for (const api of apis) {
|
|
143
|
+
if (typeof api !== "string" || api.length === 0) {
|
|
144
|
+
throw invalidInput(`declared set for ${key} contains a malformed fs API name`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
declared.set(key, apis);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const rootResolved = path.resolve(root);
|
|
151
|
+
let rootStat;
|
|
152
|
+
try {
|
|
153
|
+
rootStat = statSync(rootResolved);
|
|
154
|
+
} catch {
|
|
155
|
+
throw invalidInput("root does not exist or cannot be inspected", { root: "<opaque>" });
|
|
156
|
+
}
|
|
157
|
+
if (!rootStat.isDirectory()) {
|
|
158
|
+
throw invalidInput("root is not a directory", { root: "<opaque>" });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const modules = collectModules(rootResolved);
|
|
162
|
+
const violations = [];
|
|
163
|
+
const pushViolation = (entry) => violations.push(entry);
|
|
164
|
+
|
|
165
|
+
for (const module of modules) {
|
|
166
|
+
const relative = path.relative(rootResolved, module).split(path.sep).join("/");
|
|
167
|
+
const source = readFileSync(module, "utf8");
|
|
168
|
+
const fsImports = namedFsImports(source);
|
|
169
|
+
const allowed = declared.get(relative);
|
|
170
|
+
if (fsImports.length > 0 && allowed === undefined) {
|
|
171
|
+
pushViolation({ module: relative, rule: DECLARED_READ_SURFACE_RULES.UNDECLARED_MODULE_IMPORTS_FS });
|
|
172
|
+
}
|
|
173
|
+
if (allowed !== undefined) {
|
|
174
|
+
for (const api of fsImports) {
|
|
175
|
+
if (!allowed.includes(api)) {
|
|
176
|
+
pushViolation({ module: relative, api, rule: DECLARED_READ_SURFACE_RULES.FS_API_OUTSIDE_DECLARED_SET });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const api of WRITE_FS_APIS) {
|
|
181
|
+
if (source.includes(api)) {
|
|
182
|
+
pushViolation({ module: relative, api, rule: DECLARED_READ_SURFACE_RULES.WRITE_FAMILY_FS_API });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
schemaVersion: 1,
|
|
189
|
+
kind: "skill-family.declared-read-surface-result",
|
|
190
|
+
ok: violations.length === 0,
|
|
191
|
+
violations,
|
|
192
|
+
scannedModules: modules.map((module) =>
|
|
193
|
+
path.relative(rootResolved, module).split(path.sep).join("/"),
|
|
194
|
+
),
|
|
195
|
+
scope: "esm-named-imports-only",
|
|
196
|
+
guarantees: [...RESULT_GUARANTEES],
|
|
197
|
+
});
|
|
198
|
+
}
|
package/src/errors.mjs
CHANGED
|
@@ -65,7 +65,15 @@ export const HARNESS_ERROR_KINDS = Object.freeze({
|
|
|
65
65
|
READ_CHOKEPOINT_REJECTED: "read-chokepoint-rejected",
|
|
66
66
|
SURFACE_SCAN_VIOLATION: "surface-scan-violation",
|
|
67
67
|
SCAN_POLICY_INVALID: "scan-policy-invalid",
|
|
68
|
+
DECLARED_READ_SURFACE_VIOLATION: "declared-read-surface-violation",
|
|
69
|
+
DECLARED_READ_SURFACE_INVALID: "declared-read-surface-invalid",
|
|
70
|
+
STRUCTURED_SCAN_VIOLATION: "structured-scan-violation",
|
|
71
|
+
STRUCTURED_SCAN_INVALID: "structured-scan-invalid",
|
|
68
72
|
UPPER_BOUND_EXCEEDED: "upper-bound-exceeded",
|
|
73
|
+
// Bounded subprocess supervision (FND-ADR-012). Both kinds report through
|
|
74
|
+
// SFC2004 with details.kind; the AUD namespace never enters here.
|
|
75
|
+
TIMEOUT_POLICY_INVALID: "timeout-policy-invalid",
|
|
76
|
+
SUPERVISE_PROCESS_FAILED: "supervise-process-failed",
|
|
69
77
|
});
|
|
70
78
|
|
|
71
79
|
/**
|