skill-family-engineering-kit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +12 -2
- package/LICENSE +2 -1
- package/README.md +12 -5
- package/SECURITY.md +9 -5
- package/THIRD_PARTY_NOTICES +129 -0
- package/package.json +21 -20
- package/src/check.mjs +29 -8
- package/src/cli.mjs +1 -1
- package/src/skeleton.mjs +163 -31
package/CONTRIBUTING.md
CHANGED
|
@@ -7,8 +7,18 @@ Git 写操作的授权边界见仓库内文档站的生命周期指南(`docs/g
|
|
|
7
7
|
拓扑说明:本工作区仓永久 private,是唯一开发真源;三个 public 仓
|
|
8
8
|
(`skill-family-contracts`、`skill-family-harness-node`、
|
|
9
9
|
`skill-family-engineering-kit`)是版本化发布镜像,不承载完整开发历史与
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
测试。本指南描述的是私有工作区内经授权的开发流程;消费坐标与发布状态
|
|
11
|
+
唯一见 `docs/status/current.md`。
|
|
12
|
+
|
|
13
|
+
## Bug 报告与 Issue
|
|
14
|
+
|
|
15
|
+
普通 bug 报告欢迎通过各 public 发布镜像仓的 GitHub Issues 提交(例如
|
|
16
|
+
`https://github.com/ifoohoo/skill-family-engineering-kit/issues`)。提交
|
|
17
|
+
bug 报告时请尽量提供:受影响包与版本、最小复现步骤、预期与实际行为。
|
|
18
|
+
|
|
19
|
+
**未经事先讨论的 Pull Request 不予接受。** 如果你希望贡献代码,请先
|
|
20
|
+
通过 Issue 讨论变更的动机与范围,获得维护者确认后再提交 PR。这是为了
|
|
21
|
+
避免重复劳动、确保变更与本项目的架构裁决和发布合同一致。
|
|
12
22
|
|
|
13
23
|
## 本地安装
|
|
14
24
|
|
package/LICENSE
CHANGED
|
@@ -186,7 +186,8 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright
|
|
189
|
+
Copyright (c) 2026 skill-family-foundation-workspace contributors
|
|
190
|
+
Copyright (c) 2026 广州市风荷科技有限公司
|
|
190
191
|
|
|
191
192
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
193
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -15,11 +15,18 @@
|
|
|
15
15
|
## 使用
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
npx skill-family-kit --help
|
|
19
|
+
npx skill-family-kit scaffold --root <empty-dir> --project-id my-project
|
|
20
|
+
npx skill-family-kit adopt-plan --root <repo>
|
|
21
|
+
npx skill-family-kit projection --root <repo>
|
|
22
|
+
npx skill-family-kit check --root <repo>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
或全局安装后直接使用:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install -g skill-family-engineering-kit
|
|
29
|
+
skill-family-kit --help
|
|
23
30
|
```
|
|
24
31
|
|
|
25
32
|
## 边界机制
|
package/SECURITY.md
CHANGED
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
## 报告漏洞
|
|
13
13
|
|
|
14
14
|
请勿通过公开 issue 直接披露未修复的安全漏洞。private workspace 仓永久
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
private,不是公开报告渠道;请通过以下渠道私下报告:
|
|
16
|
+
|
|
17
|
+
1. **首选**:通过任一 public 发布镜像仓(例如
|
|
18
|
+
`https://github.com/ifoohoo/skill-family-engineering-kit`)在 GitHub 上使用
|
|
19
|
+
**Private vulnerability reporting**(仓库 Security 页签的
|
|
20
|
+
"Report a vulnerability");
|
|
21
|
+
2. **备选**:如上述通道不可用,请通过邮件联系仓库维护者
|
|
22
|
+
(`ifoohoo` GitHub 组织的 `security` 团队,或仓库 `README.md` 中列出的
|
|
23
|
+
维护者邮箱)。
|
|
20
24
|
|
|
21
25
|
报告时请尽量提供:
|
|
22
26
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
THIRD-PARTY SOFTWARE NOTICES AND ATTRIBUTION
|
|
2
|
+
=============================================
|
|
3
|
+
|
|
4
|
+
The public documentation site for skill-family-engineering-kit bundles the
|
|
5
|
+
following third-party assets in docs/assets/ (delivered as docs/ in the
|
|
6
|
+
published Pages site). This file provides the required attribution for
|
|
7
|
+
those assets.
|
|
8
|
+
|
|
9
|
+
The project's own source code is licensed under Apache-2.0; see LICENSE.
|
|
10
|
+
The notices below cover only the frontend assets distributed with the
|
|
11
|
+
public documentation site.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
1. MkDocs Material (CSS, JavaScript, favicon)
|
|
15
|
+
----------------------------------------------
|
|
16
|
+
|
|
17
|
+
Version: 9.7.7
|
|
18
|
+
License: MIT
|
|
19
|
+
Copyright: Copyright (c) 2016-2025 Martin Donath (squidfunk)
|
|
20
|
+
Files:
|
|
21
|
+
- docs/assets/stylesheets/main.*.min.css
|
|
22
|
+
- docs/assets/stylesheets/palette.*.min.css
|
|
23
|
+
- docs/assets/javascripts/bundle.*.min.js
|
|
24
|
+
- docs/assets/javascripts/workers/search.*.min.js
|
|
25
|
+
- docs/assets/images/favicon.png
|
|
26
|
+
|
|
27
|
+
MIT License
|
|
28
|
+
|
|
29
|
+
Copyright (c) 2016-2025 Martin Donath
|
|
30
|
+
|
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
32
|
+
of this software and associated documentation files (the "Software"), to
|
|
33
|
+
deal in the Software without restriction, including without limitation the
|
|
34
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
35
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
|
36
|
+
furnished to do so, subject to the following conditions:
|
|
37
|
+
|
|
38
|
+
The above copyright notice and this permission notice shall be included in
|
|
39
|
+
all copies or substantial portions of the Software.
|
|
40
|
+
|
|
41
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
42
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
43
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
|
44
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
45
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
46
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
47
|
+
IN THE SOFTWARE.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
2. lunr (search library)
|
|
51
|
+
-------------------------
|
|
52
|
+
|
|
53
|
+
Version: 2.3.9
|
|
54
|
+
License: MIT
|
|
55
|
+
Copyright: Copyright (C) 2013 by Oliver Nightingale
|
|
56
|
+
Files:
|
|
57
|
+
- docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js
|
|
58
|
+
- docs/assets/javascripts/lunr/min/lunr.multi.min.js
|
|
59
|
+
|
|
60
|
+
MIT License
|
|
61
|
+
|
|
62
|
+
Copyright (C) 2013 by Oliver Nightingale
|
|
63
|
+
|
|
64
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
65
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
66
|
+
in the Software without restriction, including without limitation the rights
|
|
67
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
68
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
69
|
+
furnished to do so, subject to the following conditions:
|
|
70
|
+
|
|
71
|
+
The above copyright notice and this permission notice shall be included in
|
|
72
|
+
all copies or substantial portions of the Software.
|
|
73
|
+
|
|
74
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
75
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
76
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
77
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
78
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
79
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
80
|
+
IN THE SOFTWARE.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
3. lunr-languages (language stemmers and word cutters)
|
|
84
|
+
-------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
License: MPL-1.1
|
|
87
|
+
Copyright: Mihai Valentin (and individual language stemmer authors)
|
|
88
|
+
Files:
|
|
89
|
+
- docs/assets/javascripts/lunr/min/lunr.ar.min.js
|
|
90
|
+
- docs/assets/javascripts/lunr/min/lunr.da.min.js
|
|
91
|
+
- docs/assets/javascripts/lunr/min/lunr.de.min.js
|
|
92
|
+
- docs/assets/javascripts/lunr/min/lunr.du.min.js
|
|
93
|
+
- docs/assets/javascripts/lunr/min/lunr.el.min.js
|
|
94
|
+
- docs/assets/javascripts/lunr/min/lunr.es.min.js
|
|
95
|
+
- docs/assets/javascripts/lunr/min/lunr.fi.min.js
|
|
96
|
+
- docs/assets/javascripts/lunr/min/lunr.fr.min.js
|
|
97
|
+
- docs/assets/javascripts/lunr/min/lunr.he.min.js
|
|
98
|
+
- docs/assets/javascripts/lunr/min/lunr.hi.min.js
|
|
99
|
+
- docs/assets/javascripts/lunr/min/lunr.hu.min.js
|
|
100
|
+
- docs/assets/javascripts/lunr/min/lunr.hy.min.js
|
|
101
|
+
- docs/assets/javascripts/lunr/min/lunr.it.min.js
|
|
102
|
+
- docs/assets/javascripts/lunr/min/lunr.ja.min.js
|
|
103
|
+
- docs/assets/javascripts/lunr/min/lunr.jp.min.js
|
|
104
|
+
- docs/assets/javascripts/lunr/min/lunr.kn.min.js
|
|
105
|
+
- docs/assets/javascripts/lunr/min/lunr.ko.min.js
|
|
106
|
+
- docs/assets/javascripts/lunr/min/lunr.nl.min.js
|
|
107
|
+
- docs/assets/javascripts/lunr/min/lunr.no.min.js
|
|
108
|
+
- docs/assets/javascripts/lunr/min/lunr.pt.min.js
|
|
109
|
+
- docs/assets/javascripts/lunr/min/lunr.ro.min.js
|
|
110
|
+
- docs/assets/javascripts/lunr/min/lunr.ru.min.js
|
|
111
|
+
- docs/assets/javascripts/lunr/min/lunr.sa.min.js
|
|
112
|
+
- docs/assets/javascripts/lunr/min/lunr.sv.min.js
|
|
113
|
+
- docs/assets/javascripts/lunr/min/lunr.ta.min.js
|
|
114
|
+
- docs/assets/javascripts/lunr/min/lunr.te.min.js
|
|
115
|
+
- docs/assets/javascripts/lunr/min/lunr.th.min.js
|
|
116
|
+
- docs/assets/javascripts/lunr/min/lunr.tr.min.js
|
|
117
|
+
- docs/assets/javascripts/lunr/min/lunr.vi.min.js
|
|
118
|
+
- docs/assets/javascripts/lunr/min/lunr.zh.min.js
|
|
119
|
+
- docs/assets/javascripts/lunr/tinyseg.js
|
|
120
|
+
- docs/assets/javascripts/lunr/wordcut.js
|
|
121
|
+
|
|
122
|
+
These files are licensed under the Mozilla Public License Version 1.1
|
|
123
|
+
(MPL-1.1). The full text of MPL-1.1 is available from the Mozilla project
|
|
124
|
+
at mozilla.org (path: /en-US/MPL/1.1/).
|
|
125
|
+
|
|
126
|
+
MPL-1.1 requires that recipients of a larger work containing MPL-covered
|
|
127
|
+
files be informed of the location of the MPL source. The source for these
|
|
128
|
+
files is the lunr-languages project on GitHub (user: MihaiValentin,
|
|
129
|
+
repo: lunr-languages).
|
package/package.json
CHANGED
|
@@ -1,42 +1,43 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": false,
|
|
5
|
-
"description": "Build-time scaffold, adoption planning, projection, and checks.",
|
|
2
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\".",
|
|
6
3
|
"author": "广州市风荷科技有限公司",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/ifoohoo/skill-family-engineering-kit.git"
|
|
4
|
+
"bin": {
|
|
5
|
+
"skill-family-kit": "./src/cli.mjs"
|
|
11
6
|
},
|
|
12
|
-
"homepage": "https://github.com/ifoohoo/skill-family-engineering-kit",
|
|
13
7
|
"bugs": {
|
|
14
8
|
"url": "https://github.com/ifoohoo/skill-family-engineering-kit/issues"
|
|
15
9
|
},
|
|
16
|
-
"
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"skill-family-contracts": "0.1.1",
|
|
12
|
+
"skill-family-harness-node": "0.1.1"
|
|
13
|
+
},
|
|
14
|
+
"description": "Build-time scaffold, adoption planning, projection, and checks.",
|
|
17
15
|
"engines": {
|
|
18
16
|
"node": ">=22.22.2 <23"
|
|
19
17
|
},
|
|
20
18
|
"exports": "./src/index.mjs",
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"skill-family-contracts": "0.1.0",
|
|
23
|
-
"skill-family-harness-node": "0.1.0"
|
|
24
|
-
},
|
|
25
|
-
"bin": {
|
|
26
|
-
"skill-family-kit": "./src/cli.mjs"
|
|
27
|
-
},
|
|
28
19
|
"files": [
|
|
29
20
|
"src",
|
|
30
21
|
"data",
|
|
31
22
|
"LICENSE",
|
|
23
|
+
"THIRD_PARTY_NOTICES",
|
|
32
24
|
"SECURITY.md",
|
|
33
25
|
"CONTRIBUTING.md",
|
|
34
26
|
"CODE_OF_CONDUCT.md",
|
|
35
27
|
"docs"
|
|
36
28
|
],
|
|
37
|
-
"
|
|
29
|
+
"homepage": "https://github.com/ifoohoo/skill-family-engineering-kit",
|
|
30
|
+
"license": "Apache-2.0",
|
|
31
|
+
"name": "skill-family-engineering-kit",
|
|
32
|
+
"private": false,
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/ifoohoo/skill-family-engineering-kit.git"
|
|
36
|
+
},
|
|
37
|
+
"type": "module",
|
|
38
|
+
"version": "0.1.1",
|
|
38
39
|
"scripts": {
|
|
39
|
-
"check": "node --
|
|
40
|
-
"test": "node --
|
|
40
|
+
"check": "node --input-type=module -e \"import('./src/index.mjs').then(() => console.log('smoke ok')).catch(e => { console.error(e); process.exit(1) })\"",
|
|
41
|
+
"test": "node --input-type=module -e \"import('./src/index.mjs').then(() => console.log('smoke ok')).catch(e => { console.error(e); process.exit(1) })\""
|
|
41
42
|
}
|
|
42
43
|
}
|
package/src/check.mjs
CHANGED
|
@@ -127,10 +127,19 @@ async function readManagedCandidate(rootAbs, rel, findings) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Loads and parses the managed-file-lock JSON independently of any check class.
|
|
132
|
+
* Returns the parsed lock object, or null when the lock is absent or unreadable.
|
|
133
|
+
*/
|
|
134
|
+
async function loadManagedFileLock(rootAbs) {
|
|
131
135
|
const loaded = await readOptionalJson(rootAbs, MANAGED_LOCK_PATH);
|
|
132
|
-
if (!loaded.ok) return null;
|
|
133
|
-
|
|
136
|
+
if (!loaded.ok) return null;
|
|
137
|
+
return loaded.value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function checkDrift(rootAbs, findings) {
|
|
141
|
+
const lock = await loadManagedFileLock(rootAbs);
|
|
142
|
+
if (!lock) return null; // no lock => nothing to drift against
|
|
134
143
|
const entries = Array.isArray(lock?.entries) ? lock.entries : [];
|
|
135
144
|
for (const entry of entries) {
|
|
136
145
|
const rel = typeof entry?.path === "string" ? normalizeRelPath(entry.path) : null;
|
|
@@ -210,8 +219,13 @@ async function checkClosure(rootAbs, lock, findings) {
|
|
|
210
219
|
}
|
|
211
220
|
}
|
|
212
221
|
|
|
213
|
-
async function checkVersion(docs, findings) {
|
|
214
|
-
|
|
222
|
+
async function checkVersion(rootAbs, docs, findings) {
|
|
223
|
+
let manifest = docs[PROJECT_MANIFEST_PATH];
|
|
224
|
+
if (!manifest) {
|
|
225
|
+
// Load independently when the contracts check didn't run first
|
|
226
|
+
const loaded = await readOptionalJson(rootAbs, PROJECT_MANIFEST_PATH);
|
|
227
|
+
if (loaded.ok) manifest = loaded.value;
|
|
228
|
+
}
|
|
215
229
|
const declared = manifest?.contracts?.version;
|
|
216
230
|
if (typeof declared !== "string") return { contractsVersion: null };
|
|
217
231
|
if (declared !== CONTRACTS_VERSION) {
|
|
@@ -360,11 +374,18 @@ export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot
|
|
|
360
374
|
let identityRecord = null;
|
|
361
375
|
|
|
362
376
|
if (classes.includes("contracts")) await checkContracts(rootAbs, docs, findings);
|
|
377
|
+
|
|
378
|
+
let driftLock = null;
|
|
363
379
|
if (classes.includes("drift")) {
|
|
364
|
-
|
|
365
|
-
if (classes.includes("closure")) closureInfo = await checkClosure(rootAbs, lock, findings);
|
|
380
|
+
driftLock = await checkDrift(rootAbs, findings);
|
|
366
381
|
}
|
|
367
|
-
|
|
382
|
+
|
|
383
|
+
if (classes.includes("closure")) {
|
|
384
|
+
const lock = driftLock ?? (classes.includes("drift") ? null : await loadManagedFileLock(rootAbs));
|
|
385
|
+
closureInfo = await checkClosure(rootAbs, lock, findings);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (classes.includes("version")) versionInfo = await checkVersion(rootAbs, docs, findings);
|
|
368
389
|
if (classes.includes("docs")) await checkDocs(rootAbs, docs, findings);
|
|
369
390
|
if (classes.includes("git")) git = await checkGit(rootAbs, findings, allowGitSpawn);
|
|
370
391
|
if (classes.includes("identity")) identityRecord = await checkIdentity(rootAbs, docs, findings, profilesRoot);
|
package/src/cli.mjs
CHANGED
package/src/skeleton.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
1
4
|
import { CONTRACTS_VERSION } from "skill-family-contracts";
|
|
2
5
|
import { digestBytes } from "skill-family-harness-node";
|
|
3
6
|
import { invalidParamsError, kitError, KIT_ERROR_KINDS } from "./errors.mjs";
|
|
@@ -31,7 +34,10 @@ import { MIGRATION_MANIFEST_PATH } from "./migration.mjs";
|
|
|
31
34
|
*/
|
|
32
35
|
|
|
33
36
|
export const KIT_TOOL_NAME = "skill-family-engineering-kit";
|
|
34
|
-
|
|
37
|
+
|
|
38
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const _kitPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
|
|
40
|
+
export const KIT_VERSION = _kitPkg.version;
|
|
35
41
|
|
|
36
42
|
/** Kit-owned document paths inside a target workspace. */
|
|
37
43
|
export const PROJECT_MANIFEST_PATH = "skill-family.project-manifest.json";
|
|
@@ -143,6 +149,10 @@ function renderRootPackageJson({ projectId, projectName, licensingProfile }) {
|
|
|
143
149
|
devDependencies: {
|
|
144
150
|
projen: SKELETON_PROJEN_VERSION,
|
|
145
151
|
},
|
|
152
|
+
dependencies: {
|
|
153
|
+
"skill-family-contracts": "^0.1.0",
|
|
154
|
+
"skill-family-harness-node": "^0.1.0",
|
|
155
|
+
},
|
|
146
156
|
"//": PROJEN_MARKER,
|
|
147
157
|
});
|
|
148
158
|
}
|
|
@@ -371,21 +381,100 @@ function renderCheckCoreScript() {
|
|
|
371
381
|
return [
|
|
372
382
|
"#!/usr/bin/env node",
|
|
373
383
|
"// core check: verifies the managed lock binds the real bytes and the",
|
|
374
|
-
"// file registry stays a closed world.
|
|
375
|
-
|
|
376
|
-
'import {
|
|
377
|
-
"",
|
|
378
|
-
|
|
384
|
+
"// file registry stays a closed world. Uses Foundation packages for",
|
|
385
|
+
"// path containment, digest computation, and closure verification.",
|
|
386
|
+
'import { lstat } from "node:fs/promises";',
|
|
387
|
+
'import { readFileSync } from "node:fs";',
|
|
388
|
+
'import path from "node:path";',
|
|
389
|
+
'import { findSchemaByObject } from "skill-family-contracts";',
|
|
390
|
+
'import {',
|
|
391
|
+
' classifyPathInput,',
|
|
392
|
+
' computeResourceClosure,',
|
|
393
|
+
' digestBytes,',
|
|
394
|
+
' readFileContained,',
|
|
395
|
+
' validateContractDocument,',
|
|
396
|
+
'} from "skill-family-harness-node";',
|
|
397
|
+
"",
|
|
398
|
+
"const root = process.cwd();",
|
|
379
399
|
"const failures = [];",
|
|
400
|
+
"",
|
|
401
|
+
"// Load and validate the managed file lock",
|
|
402
|
+
'let lockText;',
|
|
403
|
+
'try {',
|
|
404
|
+
' lockText = await readFileContained(root, "skill-family.managed-file-lock.json", { encoding: "utf8" });',
|
|
405
|
+
'} catch (cause) {',
|
|
406
|
+
' console.error(`[check-core] cannot read managed-file-lock: ${cause.message}`);',
|
|
407
|
+
' process.exit(2);',
|
|
408
|
+
'}',
|
|
409
|
+
"const lock = JSON.parse(lockText);",
|
|
410
|
+
"",
|
|
411
|
+
"// Validate lock schema",
|
|
412
|
+
"const lockSchema = findSchemaByObject(\"managed-file-lock\");",
|
|
413
|
+
"const lockValidation = validateContractDocument(lock, { schemaId: lockSchema.$id });",
|
|
414
|
+
"if (!lockValidation.valid) {",
|
|
415
|
+
' console.error(`[check-core] managed-file-lock schema invalid: ${JSON.stringify(lockValidation.errors)}`);',
|
|
416
|
+
" process.exit(2);",
|
|
417
|
+
"}",
|
|
418
|
+
"",
|
|
419
|
+
"// Verify each lock entry: lstat for type check, readFileContained for safe read, digestBytes for hash",
|
|
380
420
|
"for (const entry of lock.entries) {",
|
|
381
|
-
"
|
|
382
|
-
"
|
|
421
|
+
" const relPath = entry.path;",
|
|
422
|
+
"",
|
|
423
|
+
" // Classify the path input first",
|
|
424
|
+
" const classification = classifyPathInput(relPath);",
|
|
425
|
+
" if (!classification.ok) {",
|
|
426
|
+
" failures.push(`lock entry has invalid path: ${relPath} (kind: ${classification.kind})`);",
|
|
427
|
+
" continue;",
|
|
428
|
+
" }",
|
|
429
|
+
"",
|
|
430
|
+
" // lstat to detect symlinks and special files",
|
|
431
|
+
" let stat;",
|
|
432
|
+
" try {",
|
|
433
|
+
" stat = await lstat(path.resolve(root, relPath));",
|
|
434
|
+
" } catch {",
|
|
435
|
+
" failures.push(`managed file missing: ${relPath}`);",
|
|
436
|
+
" continue;",
|
|
437
|
+
" }",
|
|
438
|
+
"",
|
|
439
|
+
" // Reject symlinks",
|
|
440
|
+
" if (stat.isSymbolicLink()) {",
|
|
441
|
+
" failures.push(`managed file is a symlink: ${relPath}`);",
|
|
383
442
|
" continue;",
|
|
384
443
|
" }",
|
|
385
|
-
"
|
|
386
|
-
"
|
|
444
|
+
"",
|
|
445
|
+
" // Reject special files (FIFO, socket, device, etc.)",
|
|
446
|
+
" if (!stat.isFile()) {",
|
|
447
|
+
" failures.push(`managed file is not a regular file: ${relPath}`);",
|
|
448
|
+
" continue;",
|
|
449
|
+
" }",
|
|
450
|
+
"",
|
|
451
|
+
" // Read through Foundation's contained read",
|
|
452
|
+
" let content;",
|
|
453
|
+
" try {",
|
|
454
|
+
" content = await readFileContained(root, relPath);",
|
|
455
|
+
" } catch (cause) {",
|
|
456
|
+
" failures.push(`cannot read managed file: ${relPath} (${cause.message})`);",
|
|
457
|
+
" continue;",
|
|
458
|
+
" }",
|
|
459
|
+
"",
|
|
460
|
+
" // Compare digest",
|
|
461
|
+
" const actual = digestBytes(content);",
|
|
462
|
+
" if (actual !== entry.hash.value) {",
|
|
463
|
+
" failures.push(`managed file drifted: ${relPath}`);",
|
|
464
|
+
" }",
|
|
387
465
|
"}",
|
|
388
|
-
"
|
|
466
|
+
"",
|
|
467
|
+
"// Load and validate the file registry",
|
|
468
|
+
'let registryText;',
|
|
469
|
+
'try {',
|
|
470
|
+
' registryText = await readFileContained(root, ".foundation/file-registry.json", { encoding: "utf8" });',
|
|
471
|
+
'} catch (cause) {',
|
|
472
|
+
' console.error(`[check-core] cannot read file-registry: ${cause.message}`);',
|
|
473
|
+
' process.exit(2);',
|
|
474
|
+
'}',
|
|
475
|
+
"const registry = JSON.parse(registryText);",
|
|
476
|
+
"",
|
|
477
|
+
"// Check for duplicate classifications",
|
|
389
478
|
"const seen = new Map();",
|
|
390
479
|
"for (const [className, files] of [",
|
|
391
480
|
' ["projenManaged", registry.classes.projenManaged.files],',
|
|
@@ -393,10 +482,33 @@ function renderCheckCoreScript() {
|
|
|
393
482
|
' ["handwritten", registry.classes.handwritten.entries],',
|
|
394
483
|
"]) {",
|
|
395
484
|
" for (const file of files) {",
|
|
396
|
-
|
|
485
|
+
" if (seen.has(file)) {",
|
|
486
|
+
" failures.push(`file classified twice: ${file} (${seen.get(file)} and ${className})`);",
|
|
487
|
+
" }",
|
|
397
488
|
" seen.set(file, className);",
|
|
398
489
|
" }",
|
|
399
490
|
"}",
|
|
491
|
+
"",
|
|
492
|
+
"// Use Foundation's closure computation to verify closed-world",
|
|
493
|
+
"// Skip if there are already failures (to avoid hanging on special files like FIFOs)",
|
|
494
|
+
"if (failures.length === 0) {",
|
|
495
|
+
" const resources = [",
|
|
496
|
+
' { path: "skill-family.managed-file-lock.json", role: "input" },',
|
|
497
|
+
" ...lock.entries.map((entry) => ({ path: entry.path, role: \"output\" })),",
|
|
498
|
+
" ];",
|
|
499
|
+
" try {",
|
|
500
|
+
" const closure = await computeResourceClosure({ root, resources });",
|
|
501
|
+
" const missingOutputs = closure.resources.filter(",
|
|
502
|
+
" (r) => r.role === \"output\" && !r.exists,",
|
|
503
|
+
" );",
|
|
504
|
+
" for (const missing of missingOutputs) {",
|
|
505
|
+
" failures.push(`closure output missing: ${missing.path}`);",
|
|
506
|
+
" }",
|
|
507
|
+
" } catch (cause) {",
|
|
508
|
+
" failures.push(`closure computation failed: ${cause.message}`);",
|
|
509
|
+
" }",
|
|
510
|
+
"}",
|
|
511
|
+
"",
|
|
400
512
|
"if (failures.length > 0) {",
|
|
401
513
|
' for (const failure of failures) console.error(`[check-core] ${failure}`);',
|
|
402
514
|
" process.exit(2);",
|
|
@@ -434,31 +546,51 @@ function renderSecurityNegativeScript() {
|
|
|
434
546
|
return [
|
|
435
547
|
"#!/usr/bin/env node",
|
|
436
548
|
"// security negative cases: every hostile path MUST be rejected by the",
|
|
437
|
-
"// containment
|
|
438
|
-
"",
|
|
439
|
-
"
|
|
440
|
-
'
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
'
|
|
445
|
-
"
|
|
549
|
+
"// Foundation path containment mechanisms; if any passes, this script exits 2.",
|
|
550
|
+
"// Tests both lexical classification (classifyPathInput) and resolution-time",
|
|
551
|
+
"// containment (resolveContained).",
|
|
552
|
+
'import { classifyPathInput, resolveContained } from "skill-family-harness-node";',
|
|
553
|
+
"",
|
|
554
|
+
"// Paths that should be rejected at the lexical classification level",
|
|
555
|
+
"const lexicalNegatives = [",
|
|
556
|
+
' "/etc/passwd", // absolute path',
|
|
557
|
+
' "C:\\\\Windows\\\\system32", // Windows drive path',
|
|
558
|
+
' "\\\\\\\\server\\\\share", // UNC path',
|
|
559
|
+
' "", // empty path',
|
|
560
|
+
"];",
|
|
446
561
|
"",
|
|
447
|
-
"
|
|
448
|
-
|
|
449
|
-
' "
|
|
450
|
-
' "
|
|
451
|
-
' "\\\\\\\\server\\\\share",',
|
|
452
|
-
' "a/../../b",',
|
|
453
|
-
' "file:///etc/passwd",',
|
|
454
|
-
' "",',
|
|
562
|
+
"// Paths that should be rejected at the resolution level (path traversal)",
|
|
563
|
+
"const resolutionNegatives = [",
|
|
564
|
+
' "../outside.txt", // parent traversal',
|
|
565
|
+
' "a/../../b", // nested traversal',
|
|
455
566
|
"];",
|
|
456
|
-
"
|
|
567
|
+
"",
|
|
568
|
+
"const failures = [];",
|
|
569
|
+
"",
|
|
570
|
+
"// Test lexical classification",
|
|
571
|
+
"for (const candidate of lexicalNegatives) {",
|
|
572
|
+
" const result = classifyPathInput(candidate);",
|
|
573
|
+
" if (result.ok) {",
|
|
574
|
+
" failures.push(`lexical: ${candidate}`);",
|
|
575
|
+
" }",
|
|
576
|
+
"}",
|
|
577
|
+
"",
|
|
578
|
+
"// Test resolution-time containment",
|
|
579
|
+
'const fakeRoot = "/nonexistent-root-for-testing";',
|
|
580
|
+
"for (const candidate of resolutionNegatives) {",
|
|
581
|
+
" try {",
|
|
582
|
+
" await resolveContained(fakeRoot, candidate);",
|
|
583
|
+
" failures.push(`resolution: ${candidate}`);",
|
|
584
|
+
" } catch {",
|
|
585
|
+
" // Expected: resolution should fail",
|
|
586
|
+
" }",
|
|
587
|
+
"}",
|
|
588
|
+
"",
|
|
457
589
|
"if (failures.length > 0) {",
|
|
458
590
|
" console.error(`[security-negative] hostile path accepted: ${failures.join(\", \")}`);",
|
|
459
591
|
" process.exit(2);",
|
|
460
592
|
"}",
|
|
461
|
-
'console.log(`[security-negative] OK (${
|
|
593
|
+
'console.log(`[security-negative] OK (${lexicalNegatives.length + resolutionNegatives.length} hostile paths rejected)`);',
|
|
462
594
|
"",
|
|
463
595
|
].join("\n");
|
|
464
596
|
}
|