skill-family-harness-node 0.1.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.
@@ -0,0 +1,153 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
4
+ import { resolveContained, readFileContained } from "./paths.mjs";
5
+
6
+ /**
7
+ * Resource closure and digests.
8
+ *
9
+ * A closure bounds the read/write scope of an operation: the normalized,
10
+ * containment-verified set of input and output resources, each with a
11
+ * deterministic sha256 content digest, plus one digest over the whole set.
12
+ *
13
+ * Determinism rules: entries are de-duplicated on their normalized relative
14
+ * path, sorted by UTF-16 code units, and serialized with a fixed key order;
15
+ * equal resource sets always produce the equal closure digest.
16
+ */
17
+
18
+ const CLOSURE_KIND = "skill-family.resource-closure";
19
+ const CLOSURE_SCHEMA_VERSION = 1;
20
+ const DIGEST_ALGORITHM = "sha256";
21
+ const ROLES = Object.freeze(["input", "output"]);
22
+
23
+ /** sha256 hex digest of raw bytes. */
24
+ export function digestBytes(bytes) {
25
+ return createHash(DIGEST_ALGORITHM).update(bytes).digest("hex");
26
+ }
27
+
28
+ function normalizeResourcePath(relPath) {
29
+ if (typeof relPath !== "string" || relPath.length === 0 || relPath.includes("\0")) {
30
+ throw new TypeError("computeResourceClosure: every resource path must be a non-empty string");
31
+ }
32
+ const normalized = path.posix.normalize(relPath.replaceAll("\\", "/"));
33
+ if (normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
34
+ // Lexical escape inside a closure declaration: reject before any IO.
35
+ throw mechanismError(
36
+ HARNESS_ERROR_KINDS.PATH_TRAVERSAL,
37
+ "closure resource path leaves the workspace root",
38
+ { input: relPath },
39
+ );
40
+ }
41
+ return normalized === "." ? "" : normalized;
42
+ }
43
+
44
+ /**
45
+ * Computes the resource closure of `resources` relative to `root`.
46
+ *
47
+ * resources: [{ path, role }] with role "input" | "output".
48
+ * - input entries must exist; their bytes are read through the containment
49
+ * layer and digested (missing input => SFC2004 missing-resource).
50
+ * - output entries may not exist yet; existing outputs are digested,
51
+ * absent outputs are recorded with exists=false and digest=null.
52
+ *
53
+ * Returns { schemaVersion, kind, digestAlgorithm, digest, resources } where
54
+ * resources is the sorted array of { path, role, exists, sha256 }.
55
+ */
56
+ export async function computeResourceClosure({ root, resources } = {}) {
57
+ if (!root || typeof root !== "string") {
58
+ throw new TypeError("computeResourceClosure: root must be a directory path string");
59
+ }
60
+ if (!Array.isArray(resources)) {
61
+ throw new TypeError("computeResourceClosure: resources must be an array");
62
+ }
63
+ const byPath = new Map();
64
+ for (const entry of resources) {
65
+ if (!entry || typeof entry !== "object") {
66
+ throw new TypeError("computeResourceClosure: every resource entry must be an object");
67
+ }
68
+ if (!ROLES.includes(entry.role)) {
69
+ throw new TypeError(
70
+ `computeResourceClosure: role must be one of ${ROLES.join(", ")}`,
71
+ );
72
+ }
73
+ const normalized = normalizeResourcePath(entry.path);
74
+ if (normalized === "") {
75
+ throw mechanismError(
76
+ HARNESS_ERROR_KINDS.INVALID_PATH,
77
+ "closure resource path must address a resource strictly inside the workspace root",
78
+ );
79
+ }
80
+ const existing = byPath.get(normalized);
81
+ if (existing && existing.role !== entry.role) {
82
+ throw mechanismError(
83
+ HARNESS_ERROR_KINDS.CLOSURE_CONFLICT,
84
+ `resource declared with conflicting roles: ${normalized}`,
85
+ { path: normalized },
86
+ );
87
+ }
88
+ if (!existing) {
89
+ byPath.set(normalized, { path: normalized, role: entry.role });
90
+ }
91
+ }
92
+
93
+ const records = [];
94
+ for (const normalized of [...byPath.keys()].sort()) {
95
+ const record = byPath.get(normalized);
96
+ // Containment proof: resolve through the same layer as every other
97
+ // filesystem access (traversal, symlink, and realpath checks included).
98
+ await resolveContained(root, normalized);
99
+ if (record.role === "input") {
100
+ const bytes = await readFileContained(root, normalized);
101
+ records.push({ path: normalized, role: record.role, exists: true, sha256: digestBytes(bytes) });
102
+ } else {
103
+ let bytes;
104
+ try {
105
+ bytes = await readFileContained(root, normalized);
106
+ } catch (cause) {
107
+ if (cause && cause.details && cause.details.kind === HARNESS_ERROR_KINDS.MISSING_RESOURCE) {
108
+ records.push({ path: normalized, role: record.role, exists: false, sha256: null });
109
+ continue;
110
+ }
111
+ throw cause;
112
+ }
113
+ records.push({ path: normalized, role: record.role, exists: true, sha256: digestBytes(bytes) });
114
+ }
115
+ }
116
+
117
+ const canonical = JSON.stringify({
118
+ kind: CLOSURE_KIND,
119
+ schemaVersion: CLOSURE_SCHEMA_VERSION,
120
+ digestAlgorithm: DIGEST_ALGORITHM,
121
+ resources: records.map((record) => ({
122
+ path: record.path,
123
+ role: record.role,
124
+ exists: record.exists,
125
+ sha256: record.sha256,
126
+ })),
127
+ });
128
+ return {
129
+ schemaVersion: CLOSURE_SCHEMA_VERSION,
130
+ kind: CLOSURE_KIND,
131
+ digestAlgorithm: DIGEST_ALGORITHM,
132
+ digest: digestBytes(Buffer.from(canonical, "utf8")),
133
+ resources: records,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Whether a closure already bounds `relPath` (lexical normalization only, no
139
+ * filesystem access).
140
+ */
141
+ export function closureContains(closure, relPath) {
142
+ if (!closure || !Array.isArray(closure.resources)) {
143
+ throw new TypeError("closureContains: closure must be a computed closure object");
144
+ }
145
+ let normalized;
146
+ try {
147
+ normalized = normalizeResourcePath(relPath);
148
+ } catch {
149
+ return false;
150
+ }
151
+ if (normalized === "") return false;
152
+ return closure.resources.some((record) => record.path === normalized);
153
+ }
package/src/errors.mjs ADDED
@@ -0,0 +1,75 @@
1
+ import { ContractsError, isRegisteredErrorCode } from "skill-family-contracts";
2
+
3
+ /**
4
+ * Harness error policy.
5
+ *
6
+ * The harness never invents error codes: every thrown or reported error
7
+ * carries a code from the frozen contracts registry. Mechanism failures use
8
+ * SFC2004 (EXECUTION_FAILED) whose registered meaning is "the mechanism
9
+ * runtime failed while executing a well-formed operation; details carry the
10
+ * mechanism evidence". The mechanism evidence is the stable `details.kind`
11
+ * value enumerated in HARNESS_ERROR_KINDS.
12
+ *
13
+ * Adding a brand-new SFC code would be a contracts change (the registry file
14
+ * lives in skill-family-contracts), which is outside this package's write
15
+ * set; the SFC2004 + details.kind pairing keeps the public surface stable
16
+ * without touching the frozen registry.
17
+ */
18
+
19
+ /**
20
+ * Stable mechanism-failure kinds. Each value appears as `details.kind` on an
21
+ * SFC2004-coded HarnessError or operation-result error entry. The set is
22
+ * frozen for the v1 harness; values are strings so they serialize unchanged.
23
+ */
24
+ export const HARNESS_ERROR_KINDS = Object.freeze({
25
+ INVALID_PATH: "invalid-path",
26
+ ABSOLUTE_PATH: "absolute-path",
27
+ WINDOWS_DRIVE_PATH: "windows-drive-path",
28
+ WINDOWS_PATH: "windows-path",
29
+ UNC_PATH: "unc-path",
30
+ PATH_TRAVERSAL: "path-traversal",
31
+ SYMLINK_ESCAPE: "symlink-escape",
32
+ REALPATH_ESCAPE: "realpath-escape",
33
+ INVALID_ROOT: "invalid-root",
34
+ ATOMIC_WRITE_FAILED: "atomic-write-failed",
35
+ READ_FAILED: "read-failed",
36
+ MISSING_RESOURCE: "missing-resource",
37
+ WORKSPACE_CREATE_FAILED: "workspace-create-failed",
38
+ WORKSPACE_DISPOSE_FAILED: "workspace-dispose-failed",
39
+ WORKSPACE_DISPOSED: "workspace-disposed",
40
+ CLOSURE_CONFLICT: "closure-conflict",
41
+ UNSUPPORTED_POLICY: "unsupported-policy",
42
+ EXECUTION_FAILED: "execution-failed",
43
+ INVALID_RESULT: "invalid-result",
44
+ });
45
+
46
+ /**
47
+ * Coded error for mechanism failures. The code must already exist in the
48
+ * frozen contracts registry; construction with anything else is a
49
+ * programming error and throws a TypeError immediately.
50
+ */
51
+ export class HarnessError extends ContractsError {
52
+ constructor(code, message, details) {
53
+ if (!isRegisteredErrorCode(code)) {
54
+ throw new TypeError(
55
+ `HarnessError refuses unregistered error code: ${String(code)}`,
56
+ );
57
+ }
58
+ super(code, message, details);
59
+ this.name = "HarnessError";
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Builds the canonical mechanism-failure error: SFC2004 with a stable
65
+ * details.kind. Extra structured evidence may be merged into details but can
66
+ * never override the kind.
67
+ */
68
+ export function mechanismError(kind, message, extraDetails) {
69
+ const values = Object.values(HARNESS_ERROR_KINDS);
70
+ if (!values.includes(kind)) {
71
+ throw new TypeError(`mechanismError: unknown harness error kind: ${String(kind)}`);
72
+ }
73
+ const details = { ...(extraDetails ?? {}), kind };
74
+ return new HarnessError("SFC2004", message, details);
75
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * skill-family-harness-node: the default Node mechanism runtime for Skill
3
+ * Family engineering contracts.
4
+ *
5
+ * The harness is a thin runtime: it consumes skill-family-contracts (envelopes,
6
+ * schemas, dialect routing, kernel protocol, stable error codes, fixtures)
7
+ * and implements mechanism only — validator caching, contained filesystem
8
+ * access, atomic writes, temporary workspaces, resource closure, and the
9
+ * operation-request -> operation-result pipeline. It owns no business
10
+ * semantics, no orchestration, no git, no network, and no second language.
11
+ */
12
+
13
+ export const HARNESS_CAPABILITIES = Object.freeze([
14
+ "schema-validation",
15
+ "atomic-write",
16
+ "path-containment",
17
+ "temporary-workspace",
18
+ "resource-closure",
19
+ "operation-envelope",
20
+ ]);
21
+
22
+ export const HARNESS_EXCLUSIONS = Object.freeze([
23
+ "business-semantics",
24
+ "workflow-orchestration",
25
+ "git-writes",
26
+ "model-calls",
27
+ "release-state",
28
+ "remote-network-access",
29
+ ]);
30
+
31
+ export { HarnessError, HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
32
+
33
+ export { classifyPathInput, resolveContained, readFileContained } from "./paths.mjs";
34
+
35
+ export { writeFileAtomic } from "./atomic.mjs";
36
+
37
+ export {
38
+ TemporaryWorkspace,
39
+ createTemporaryWorkspace,
40
+ withTemporaryWorkspace,
41
+ } from "./workspace.mjs";
42
+
43
+ export { digestBytes, computeResourceClosure, closureContains } from "./closure.mjs";
44
+
45
+ export {
46
+ resolveSchemaContext,
47
+ getValidator,
48
+ validatorCacheSize,
49
+ validateContractDocument,
50
+ } from "./validation.mjs";
51
+
52
+ export { parseRequest, processRequest } from "./request.mjs";
package/src/paths.mjs ADDED
@@ -0,0 +1,285 @@
1
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
5
+
6
+ /**
7
+ * Path containment.
8
+ *
9
+ * Every filesystem operation in the harness goes through resolveContained,
10
+ * which rejects three escape classes with stable, distinct kinds (all under
11
+ * the registered SFC2004 code):
12
+ *
13
+ * 1. path-traversal — the lexically resolved target leaves the root
14
+ * (`..` segments, or any absolute input).
15
+ * 2. symlink-escape — the final path component is a symbolic link whose
16
+ * real target lies outside the root.
17
+ * 3. realpath-escape — the canonical (realpath) resolution of the target,
18
+ * including intermediate directories, lies outside the
19
+ * canonical root; this catches symlinked intermediate
20
+ * directories and chained links.
21
+ *
22
+ * Inputs that only make sense on another operating system (Windows drive
23
+ * paths, UNC paths, backslash separators on POSIX) are rejected before any
24
+ * resolution: ambiguous inputs never reach the filesystem.
25
+ *
26
+ * The containment guarantee is evaluated at call time (TOCTOU note: a
27
+ * process that can mutate the workspace between the check and the follow-up
28
+ * filesystem call is outside this mechanism's control; the guarantee is that
29
+ * the harness itself never performs an unchecked access).
30
+ */
31
+
32
+ const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:/;
33
+ const WINDOWS_UNC_PATTERN = /^\\\\/;
34
+ const POSIX_UNC_PATTERN = /^\/\//;
35
+
36
+ /**
37
+ * Classifies one candidate relative path without touching the filesystem.
38
+ * Returns { ok: true } or { ok: false, kind } where kind is one of the
39
+ * stable HARNESS_ERROR_KINDS values. Pure function of (input, platform), so
40
+ * the Windows-facing rules are testable on any host.
41
+ */
42
+ export function classifyPathInput(input, platform = process.platform) {
43
+ if (typeof input !== "string") {
44
+ return { ok: false, kind: HARNESS_ERROR_KINDS.INVALID_PATH };
45
+ }
46
+ if (input.length === 0) {
47
+ return { ok: false, kind: HARNESS_ERROR_KINDS.INVALID_PATH };
48
+ }
49
+ if (input.includes("\0")) {
50
+ return { ok: false, kind: HARNESS_ERROR_KINDS.INVALID_PATH };
51
+ }
52
+ if (platform === "win32") {
53
+ // Native separators are fine on Windows; containment is decided by the
54
+ // lexical + real checks. UNC paths address remote machines and are never
55
+ // containable; rooted paths (`\x`, `/x`, `<drive>:\x`) are absolute.
56
+ if (WINDOWS_UNC_PATTERN.test(input)) {
57
+ return { ok: false, kind: HARNESS_ERROR_KINDS.UNC_PATH };
58
+ }
59
+ if (input.startsWith("\\") || input.startsWith("/")) {
60
+ return { ok: false, kind: HARNESS_ERROR_KINDS.ABSOLUTE_PATH };
61
+ }
62
+ if (WINDOWS_DRIVE_PATTERN.test(input)) {
63
+ // `<drive>:\x` is absolute; the root-relative check would reject it too, but
64
+ // the explicit kind keeps the interception observable.
65
+ if (input.charAt(1) === ":" && /[\\/]/.test(input.charAt(2))) {
66
+ return { ok: false, kind: HARNESS_ERROR_KINDS.ABSOLUTE_PATH };
67
+ }
68
+ // Drive-relative forms (`C:foo`) depend on a per-drive working
69
+ // directory that this runtime never tracks: ambiguous, therefore
70
+ // rejected.
71
+ return { ok: false, kind: HARNESS_ERROR_KINDS.WINDOWS_DRIVE_PATH };
72
+ }
73
+ return { ok: true };
74
+ }
75
+ // POSIX (and every non-Windows host): a path is root-relative or nothing.
76
+ if (WINDOWS_DRIVE_PATTERN.test(input)) {
77
+ return { ok: false, kind: HARNESS_ERROR_KINDS.WINDOWS_DRIVE_PATH };
78
+ }
79
+ if (WINDOWS_UNC_PATTERN.test(input)) {
80
+ return { ok: false, kind: HARNESS_ERROR_KINDS.UNC_PATH };
81
+ }
82
+ if (POSIX_UNC_PATTERN.test(input)) {
83
+ return { ok: false, kind: HARNESS_ERROR_KINDS.UNC_PATH };
84
+ }
85
+ if (input.includes("\\")) {
86
+ // Backslash is a Windows separator; on POSIX it would silently become
87
+ // part of a filename. Cross-platform inputs must be unambiguous.
88
+ return { ok: false, kind: HARNESS_ERROR_KINDS.WINDOWS_PATH };
89
+ }
90
+ if (path.posix.isAbsolute(input)) {
91
+ return { ok: false, kind: HARNESS_ERROR_KINDS.ABSOLUTE_PATH };
92
+ }
93
+ return { ok: true };
94
+ }
95
+
96
+ function insideRoot(rootReal, candidate) {
97
+ const rel = path.relative(rootReal, candidate);
98
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
99
+ }
100
+
101
+ // Anchors may coincide with the root itself (e.g. a top-level file whose
102
+ // deepest existing ancestor is the workspace root); targets may not.
103
+ function insideOrEqualRoot(rootReal, candidate) {
104
+ const rel = path.relative(rootReal, candidate);
105
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
106
+ }
107
+
108
+ async function canonicalRoot(root) {
109
+ let rootReal;
110
+ try {
111
+ rootReal = await realpath(root);
112
+ } catch {
113
+ throw mechanismError(
114
+ HARNESS_ERROR_KINDS.INVALID_ROOT,
115
+ "workspace root does not resolve to an existing path",
116
+ { root: "<opaque>" },
117
+ );
118
+ }
119
+ let rootStat;
120
+ try {
121
+ rootStat = await stat(rootReal);
122
+ } catch {
123
+ throw mechanismError(
124
+ HARNESS_ERROR_KINDS.INVALID_ROOT,
125
+ "workspace root cannot be inspected",
126
+ { root: "<opaque>" },
127
+ );
128
+ }
129
+ if (!rootStat.isDirectory()) {
130
+ throw mechanismError(
131
+ HARNESS_ERROR_KINDS.INVALID_ROOT,
132
+ "workspace root is not a directory",
133
+ { root: "<opaque>" },
134
+ );
135
+ }
136
+ return rootReal;
137
+ }
138
+
139
+ async function deepestExistingAncestor(rootReal, absPath) {
140
+ let current = absPath;
141
+ const missing = [];
142
+ while (current.length >= rootReal.length) {
143
+ try {
144
+ await lstat(current);
145
+ return { anchor: current, missing };
146
+ } catch {
147
+ missing.unshift(path.basename(current));
148
+ const parent = path.dirname(current);
149
+ if (parent === current) break;
150
+ current = parent;
151
+ }
152
+ }
153
+ // Unreachable for paths lexically inside an existing root; keep a coded
154
+ // failure instead of a crash.
155
+ throw mechanismError(
156
+ HARNESS_ERROR_KINDS.INVALID_ROOT,
157
+ "workspace root disappeared during containment resolution",
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Resolves `relPath` against `root` and proves the target stays inside.
163
+ * Returns the absolute, lexically resolved path (strictly inside the root).
164
+ * Throws HarnessError (SFC2004) with a stable details.kind on any violation.
165
+ */
166
+ export async function resolveContained(root, relPath) {
167
+ const classification = classifyPathInput(relPath);
168
+ if (!classification.ok) {
169
+ throw mechanismError(
170
+ classification.kind,
171
+ `path rejected before resolution (kind: ${classification.kind})`,
172
+ { input: typeof relPath === "string" ? relPath : typeof relPath },
173
+ );
174
+ }
175
+ const rootReal = await canonicalRoot(root);
176
+ const resolved = path.resolve(rootReal, relPath);
177
+ if (resolved === rootReal) {
178
+ throw mechanismError(
179
+ HARNESS_ERROR_KINDS.INVALID_PATH,
180
+ "path must address a resource strictly inside the workspace root",
181
+ );
182
+ }
183
+ if (!insideRoot(rootReal, resolved)) {
184
+ throw mechanismError(
185
+ HARNESS_ERROR_KINDS.PATH_TRAVERSAL,
186
+ "resolved path leaves the workspace root",
187
+ { input: relPath },
188
+ );
189
+ }
190
+
191
+ // Escape class 2: the final component itself is a symlink pointing out.
192
+ let finalStat;
193
+ try {
194
+ finalStat = await lstat(resolved);
195
+ } catch (cause) {
196
+ if (cause && cause.code !== "ENOENT" && cause.code !== "ENOTDIR") {
197
+ throw mechanismError(HARNESS_ERROR_KINDS.READ_FAILED, `path cannot be inspected: ${cause.code}`);
198
+ }
199
+ finalStat = null;
200
+ }
201
+ if (finalStat && finalStat.isSymbolicLink()) {
202
+ let realTarget;
203
+ try {
204
+ realTarget = await realpath(resolved);
205
+ } catch {
206
+ // A broken symlink has no determinable target: never usable.
207
+ throw mechanismError(
208
+ HARNESS_ERROR_KINDS.SYMLINK_ESCAPE,
209
+ "symbolic link target does not resolve; treated as an escape",
210
+ { input: relPath },
211
+ );
212
+ }
213
+ if (!insideRoot(rootReal, realTarget)) {
214
+ throw mechanismError(
215
+ HARNESS_ERROR_KINDS.SYMLINK_ESCAPE,
216
+ "final path component is a symbolic link escaping the workspace root",
217
+ { input: relPath },
218
+ );
219
+ }
220
+ }
221
+
222
+ // Escape class 3: canonical resolution (any intermediate symlink chain)
223
+ // must stay inside the canonical root.
224
+ const { anchor, missing } = await deepestExistingAncestor(rootReal, resolved);
225
+ let anchorReal;
226
+ try {
227
+ anchorReal = await realpath(anchor);
228
+ } catch {
229
+ throw mechanismError(
230
+ HARNESS_ERROR_KINDS.REALPATH_ESCAPE,
231
+ "anchor directory vanished during canonical resolution",
232
+ { input: relPath },
233
+ );
234
+ }
235
+ if (!insideOrEqualRoot(rootReal, anchorReal)) {
236
+ throw mechanismError(
237
+ HARNESS_ERROR_KINDS.REALPATH_ESCAPE,
238
+ "canonical anchor of the target lies outside the workspace root",
239
+ { input: relPath },
240
+ );
241
+ }
242
+ const canonical = path.join(anchorReal, ...missing);
243
+ if (!insideRoot(rootReal, canonical)) {
244
+ throw mechanismError(
245
+ HARNESS_ERROR_KINDS.REALPATH_ESCAPE,
246
+ "canonical target path leaves the workspace root",
247
+ { input: relPath },
248
+ );
249
+ }
250
+ return resolved;
251
+ }
252
+
253
+ /**
254
+ * Contained read. Returns the file content as a Buffer (utf8 string when
255
+ * encoding is "utf8"). Missing resources surface as SFC2004 with the stable
256
+ * missing-resource kind instead of a raw fs error.
257
+ */
258
+ export async function readFileContained(root, relPath, { encoding } = {}) {
259
+ const target = await resolveContained(root, relPath);
260
+ try {
261
+ return encoding === undefined
262
+ ? await readFile(target)
263
+ : await readFile(target, encoding);
264
+ } catch (cause) {
265
+ if (cause && cause.code === "ENOENT") {
266
+ throw mechanismError(
267
+ HARNESS_ERROR_KINDS.MISSING_RESOURCE,
268
+ "contained resource does not exist",
269
+ { input: relPath },
270
+ );
271
+ }
272
+ if (cause && cause.code === "EISDIR") {
273
+ throw mechanismError(
274
+ HARNESS_ERROR_KINDS.READ_FAILED,
275
+ "contained resource is a directory",
276
+ { input: relPath },
277
+ );
278
+ }
279
+ throw mechanismError(
280
+ HARNESS_ERROR_KINDS.READ_FAILED,
281
+ `contained read failed: ${cause && cause.code ? cause.code : "unknown"}`,
282
+ { input: relPath },
283
+ );
284
+ }
285
+ }