@gobing-ai/ts-runtime 0.4.28 → 0.4.31

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/README.md CHANGED
@@ -261,16 +261,17 @@ the most secure and performant default:
261
261
  | ------- | --------- | ------------ | ------- |
262
262
  | **Package specifier** (recommended) | `$schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"` | Resolved through `node_modules` via the module resolver, then read from disk | No network, no path guessing; survives hoisting/pnpm/monorepo layouts. Schemas ship in each package's `schemas/` (declared in `files`). **Quote the value** — YAML treats a leading `@` as reserved. |
263
263
  | Relative path | `$schema: ./schemas/rule-file.schema.json` | Resolved against the config file's directory | Fine for repo-local schemas; brittle if the config moves. |
264
- | Remote URL | `$schema: https://json-schema.org/.../rule-file.schema.json` | Fetched over HTTP(S) — **off by default** | SSRF/DoS surface for third-party configs. Opt in with `{ allowRemote: true }` (5s timeout) or supply your own `fetch`. |
264
+ | Remote URL | `$schema: https://json-schema.org/.../rule-file.schema.json` | Fetched over HTTP(S) — **off by default** | SSRF/DoS surface for third-party configs. Opt in with **both** `{ allowRemote: true }` **and** a `fetch` implementation there is no built-in fetch; the caller owns timeout/credentials. |
265
265
 
266
266
  ```ts
267
267
  // Bundled package schema (default path — no extra options needed):
268
268
  // $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
269
269
  await loadStructuredConfig('rules.yaml');
270
270
 
271
- // Remote schema is refused unless explicitly enabled:
272
- await loadStructuredConfig('rules.yaml', { allowRemote: true }); // built-in fetch, time-bounded
273
- await loadStructuredConfig('rules.yaml', { fetch: myFetch }); // or inject your own
271
+ // Remote schema is refused unless BOTH flags are present:
272
+ await loadStructuredConfig('rules.yaml', { allowRemote: true, fetch: myFetch }); // caller-supplied fetch
273
+ // { allowRemote: true } alone -> refused (no built-in fetch)
274
+ // { fetch: myFetch } alone -> refused (no explicit opt-in)
274
275
  ```
275
276
 
276
277
  > **Security:** remote schema fetching is disabled by default. Resolving a bundled schema from
package/dist/fs.d.ts CHANGED
@@ -5,13 +5,21 @@ export declare function ensureDirForFile(path: string, fs?: CanonicalFileSystem)
5
5
  export declare function atomicWriteFile(path: string, content: string, fs?: CanonicalFileSystem): Promise<void>;
6
6
  /** Atomically writes a value as JSON with trailing newline. */
7
7
  export declare function atomicWriteJson(path: string, value: unknown, fs?: CanonicalFileSystem): Promise<void>;
8
- /** Reads and parses a JSON file. */
8
+ /** Reads and parses a JSON file. Throws with the file named when the content is not valid JSON. */
9
9
  export declare function readJsonFile<T = unknown>(path: string, fs?: CanonicalFileSystem): Promise<T>;
10
10
  /** Writes a value as JSON with 2-space indentation and trailing newline. */
11
11
  export declare function writeJsonFile(path: string, value: unknown, fs?: CanonicalFileSystem): Promise<void>;
12
12
  /**
13
13
  * Recursively walks a directory, returning sorted paths to all files,
14
14
  * optionally excluding entries by name.
15
+ *
16
+ * Cycle-safe and root-confined (task 0060 F3): directory symlinks are tracked
17
+ * by their canonical real path, so a link back to an ancestor (or the start
18
+ * root itself) is walked once at most, and a link escaping the start root is
19
+ * skipped. In-root directory symlinks are still traversed — importer and
20
+ * rule-engine walk `$HOME/...` trees that are often symlinks into a dotfiles
21
+ * repo. When the filesystem has no `realPath` (in-memory / CF stubs), today's
22
+ * follow-`stat` behaviour is kept (no ambient capability).
15
23
  */
16
24
  export declare function walkDir(path: string, fs?: CanonicalFileSystem, exclude?: Set<string>): Promise<string[]>;
17
25
  /**
package/dist/fs.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,IAAI,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGvE,iEAAiE;AACjE,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAE,mBAA4C,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpH;AAED,0GAA0G;AAC1G,wBAAsB,eAAe,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,EAAE,GAAE,mBAA4C,GACjD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,+DAA+D;AAC/D,wBAAsB,eAAe,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,EAAE,GAAE,mBAA4C,GACjD,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,oCAAoC;AACpC,wBAAsB,YAAY,CAAC,CAAC,GAAG,OAAO,EAC1C,IAAI,EAAE,MAAM,EACZ,EAAE,GAAE,mBAA4C,GACjD,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED,4EAA4E;AAC5E,wBAAsB,aAAa,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,EAAE,GAAE,mBAA4C,GACjD,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;GAGG;AACH,wBAAsB,OAAO,CACzB,IAAI,EAAE,MAAM,EACZ,EAAE,GAAE,mBAA4C,EAChD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACtB,OAAO,CAAC,MAAM,EAAE,CAAC,CAcnB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC3B,IAAI,EAAE,MAAM,EACZ,EAAE,GAAE,mBAA4C,GACjD;IAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,IAAI,IAAI,CAAA;CAAE,CAE7C"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,IAAI,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGvE,iEAAiE;AACjE,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAE,mBAA4C,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpH;AAED,0GAA0G;AAC1G,wBAAsB,eAAe,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,EAAE,GAAE,mBAA4C,GACjD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,+DAA+D;AAC/D,wBAAsB,eAAe,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,EAAE,GAAE,mBAA4C,GACjD,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,mGAAmG;AACnG,wBAAsB,YAAY,CAAC,CAAC,GAAG,OAAO,EAC1C,IAAI,EAAE,MAAM,EACZ,EAAE,GAAE,mBAA4C,GACjD,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,4EAA4E;AAC5E,wBAAsB,aAAa,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,EAAE,GAAE,mBAA4C,GACjD,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,OAAO,CACzB,IAAI,EAAE,MAAM,EACZ,EAAE,GAAE,mBAA4C,EAChD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACtB,OAAO,CAAC,MAAM,EAAE,CAAC,CAMnB;AAkCD;;GAEG;AACH,wBAAgB,eAAe,CAC3B,IAAI,EAAE,MAAM,EACZ,EAAE,GAAE,mBAA4C,GACjD;IAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,IAAI,IAAI,CAAA;CAAE,CAE7C"}
package/dist/fs.js CHANGED
@@ -15,9 +15,15 @@ export async function atomicWriteFile(path, content, fs = createNodeFileSystem()
15
15
  export async function atomicWriteJson(path, value, fs = createNodeFileSystem()) {
16
16
  await atomicWriteFile(path, `${JSON.stringify(value, null, 2)}\n`, fs);
17
17
  }
18
- /** Reads and parses a JSON file. */
18
+ /** Reads and parses a JSON file. Throws with the file named when the content is not valid JSON. */
19
19
  export async function readJsonFile(path, fs = createNodeFileSystem()) {
20
- return JSON.parse(await fs.readFile(path));
20
+ const content = await fs.readFile(path);
21
+ try {
22
+ return JSON.parse(content);
23
+ }
24
+ catch (error) {
25
+ throw new Error(`Failed to parse JSON file "${path}": ${error.message}`);
26
+ }
21
27
  }
22
28
  /** Writes a value as JSON with 2-space indentation and trailing newline. */
23
29
  export async function writeJsonFile(path, value, fs = createNodeFileSystem()) {
@@ -26,23 +32,46 @@ export async function writeJsonFile(path, value, fs = createNodeFileSystem()) {
26
32
  /**
27
33
  * Recursively walks a directory, returning sorted paths to all files,
28
34
  * optionally excluding entries by name.
35
+ *
36
+ * Cycle-safe and root-confined (task 0060 F3): directory symlinks are tracked
37
+ * by their canonical real path, so a link back to an ancestor (or the start
38
+ * root itself) is walked once at most, and a link escaping the start root is
39
+ * skipped. In-root directory symlinks are still traversed — importer and
40
+ * rule-engine walk `$HOME/...` trees that are often symlinks into a dotfiles
41
+ * repo. When the filesystem has no `realPath` (in-memory / CF stubs), today's
42
+ * follow-`stat` behaviour is kept (no ambient capability).
29
43
  */
30
44
  export async function walkDir(path, fs = createNodeFileSystem(), exclude) {
31
- const entries = (await fs.readDir(path)).sort();
45
+ const startReal = fs.realPath?.(path) ?? path;
46
+ const visited = new Set([startReal]);
32
47
  const result = [];
48
+ await walkDirInto(path, fs, exclude, startReal, visited, result);
49
+ return result;
50
+ }
51
+ async function walkDirInto(path, fs, exclude, startReal, visited, result) {
52
+ const entries = (await fs.readDir(path)).sort();
33
53
  for (const entry of entries) {
34
54
  if (exclude?.has(entry))
35
55
  continue;
36
56
  const fullPath = joinPath(path, entry);
37
57
  const entryStat = await fs.stat(fullPath);
38
58
  if (entryStat?.isDirectory()) {
39
- result.push(...(await walkDir(fullPath, fs, exclude)));
59
+ const entryReal = fs.realPath?.(fullPath) ?? fullPath;
60
+ if (visited.has(entryReal))
61
+ continue; // cycle or already-walked real dir
62
+ if (fs.realPath !== undefined &&
63
+ entryReal !== startReal &&
64
+ !entryReal.startsWith(`${startReal}/`) &&
65
+ !entryReal.startsWith(`${startReal}\\`)) {
66
+ continue; // symlink escapes the start root
67
+ }
68
+ visited.add(entryReal);
69
+ await walkDirInto(fullPath, fs, exclude, startReal, visited, result);
40
70
  }
41
71
  else if (entryStat?.isFile()) {
42
72
  result.push(fullPath);
43
73
  }
44
74
  }
45
- return result;
46
75
  }
47
76
  /**
48
77
  * Creates a writable stream for append-only output at the given path.
@@ -48,6 +48,13 @@ export interface ProcessOptions {
48
48
  args?: string[];
49
49
  cwd?: string;
50
50
  env?: Record<string, string>;
51
+ /**
52
+ * `'merge'` (default): a partial `env` extends the parent environment (execa
53
+ * `extendEnv: true` semantics). `'replace'`: the child gets exactly `env` and
54
+ * none of the parent variables. Task 0060 R5 unified the default to merge so
55
+ * `run` and `runStreaming` share one contract.
56
+ */
57
+ envMode?: 'merge' | 'replace';
51
58
  timeout?: number;
52
59
  maxOutput?: number;
53
60
  label?: string;
@@ -114,6 +121,8 @@ export interface ProcessEventDetail {
114
121
  timestamp: string;
115
122
  label?: string;
116
123
  error?: string;
124
+ /** Producer-owned observability severity. */
125
+ severity: 'info' | 'warning' | 'error';
117
126
  }
118
127
  /** Zero-dependency structural event sink for process observability. */
119
128
  export interface ProcessEventSink {
@@ -134,6 +143,12 @@ export interface PipeProcessOptions {
134
143
  args?: string[];
135
144
  cwd?: string;
136
145
  env?: Record<string, string>;
146
+ /**
147
+ * `'merge'` (default): a partial `env` extends the parent environment, matching
148
+ * {@link ProcessExecutor.run}. `'replace'`: the child gets exactly `env`.
149
+ * Previously `runStreaming` always replaced; task 0060 R5 made merge the default.
150
+ */
151
+ envMode?: 'merge' | 'replace';
137
152
  label?: string;
138
153
  /**
139
154
  * Registry metadata (spur#0264). Defaults: source `'other'` for streaming.
@@ -1 +1 @@
1
- {"version":3,"file":"process-executor.d.ts","sourceRoot":"","sources":["../src/process-executor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"process-executor.d.ts","sourceRoot":"","sources":["../src/process-executor.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,30 @@
1
1
  import { isatty } from 'node:tty';
2
2
  import { execa } from 'execa';
3
+ import { getProcessEnv } from './config.js';
4
+ function processEventSeverity(reason, exitCode) {
5
+ if (reason === 'error')
6
+ return 'error';
7
+ if (reason === 'timeout')
8
+ return 'warning';
9
+ if (exitCode !== null && exitCode !== 0)
10
+ return 'warning';
11
+ return 'info';
12
+ }
13
+ /**
14
+ * Resolve the child env per the unified contract (task 0060 R5): a partial env
15
+ * extends the parent environment unless `envMode: 'replace'` opts out. Parent
16
+ * values can be `undefined` (deleted keys) — Bun's env is string-only, so filter them.
17
+ */
18
+ function resolveChildEnv(env, envMode) {
19
+ if (envMode === 'replace')
20
+ return env;
21
+ const merged = {};
22
+ for (const [key, value] of Object.entries(getProcessEnv())) {
23
+ if (value !== undefined)
24
+ merged[key] = value;
25
+ }
26
+ return { ...merged, ...env };
27
+ }
3
28
  // ── NodeProcessExecutor (concrete Node/Bun implementation) ───────────────
4
29
  /**
5
30
  * Concrete Node/Bun implementation of {@link ProcessExecutor}, wrapping `execa`
@@ -49,6 +74,7 @@ export class NodeProcessExecutor {
49
74
  durationMs: 0,
50
75
  reason: 'exit',
51
76
  timestamp: startedIso,
77
+ severity: 'info',
52
78
  ...(options.label !== undefined ? { label: options.label } : {}),
53
79
  });
54
80
  try {
@@ -133,6 +159,7 @@ export class NodeProcessExecutor {
133
159
  durationMs: 0,
134
160
  reason: 'exit',
135
161
  timestamp: startedIso,
162
+ severity: 'info',
136
163
  ...(options.label !== undefined ? { label: options.label } : {}),
137
164
  });
138
165
  const subprocess = Bun.spawn({
@@ -141,7 +168,7 @@ export class NodeProcessExecutor {
141
168
  stdout: 'pipe',
142
169
  stderr: 'pipe',
143
170
  ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
144
- ...(options.env !== undefined ? { env: options.env } : {}),
171
+ ...(options.env !== undefined ? { env: resolveChildEnv(options.env, options.envMode) } : {}),
145
172
  });
146
173
  const pipe = new BunPipeProcess(subprocess);
147
174
  if (pipe.pid !== null) {
@@ -165,6 +192,7 @@ export class NodeProcessExecutor {
165
192
  durationMs: 0,
166
193
  reason: 'error',
167
194
  timestamp: new Date().toISOString(),
195
+ severity: processEventSeverity('error', null),
168
196
  ...(options.label !== undefined ? { label: options.label } : {}),
169
197
  error: errorMessage(error),
170
198
  });
@@ -214,6 +242,7 @@ export class NodeProcessExecutor {
214
242
  durationMs: result.durationMs,
215
243
  reason,
216
244
  timestamp: new Date().toISOString(),
245
+ severity: processEventSeverity(reason, result.exitCode),
217
246
  ...(options.label !== undefined ? { label: options.label } : {}),
218
247
  ...(error !== undefined ? { error: errorMessage(error) } : {}),
219
248
  });
@@ -243,6 +272,7 @@ class ObservedPipeProcess {
243
272
  durationMs: Date.now() - context.startedAt,
244
273
  reason: this.killedWith !== undefined ? 'signal' : 'exit',
245
274
  timestamp: new Date().toISOString(),
275
+ severity: processEventSeverity(this.killedWith !== undefined ? 'signal' : 'exit', exitCode),
246
276
  ...(context.label !== undefined ? { label: context.label } : {}),
247
277
  });
248
278
  return exitCode;
@@ -1 +1 @@
1
- {"version":3,"file":"schema-validation.d.ts","sourceRoot":"","sources":["../src/schema-validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAOhD,4GAA4G;AAC5G,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,oBAAoB,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC5C,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACtC;AAED,oGAAoG;AACpG,MAAM,WAAW,2BAA2B;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7C;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;CAC7C;AAED,+GAA+G;AAC/G,qBAAa,2BAA4B,SAAQ,KAAK;IAG9C,QAAQ,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE;gBADnD,OAAO,EAAE,MAAM,EACN,UAAU,GAAE,SAAS,mBAAmB,EAAO;CAK/D;AAED,6GAA6G;AAC7G,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,2BAAgC,GAAG,OAAO,CAAC,OAAO,CAAC,CAGpH;AAED,qGAAqG;AACrG,wBAAsB,qBAAqB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,qHAAqH;AACrH,wBAAsB,0BAA0B,CAC5C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,wHAAwH;AACxH,wBAAgB,kBAAkB,CAC9B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,IAAI,SAAK,EACT,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAM,EACrC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GAC1C,mBAAmB,EAAE,CAwDvB"}
1
+ {"version":3,"file":"schema-validation.d.ts","sourceRoot":"","sources":["../src/schema-validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAOhD,4GAA4G;AAC5G,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,oBAAoB,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC5C,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACtC;AAED,oGAAoG;AACpG,MAAM,WAAW,2BAA2B;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7C;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;CAC7C;AAED,+GAA+G;AAC/G,qBAAa,2BAA4B,SAAQ,KAAK;IAG9C,QAAQ,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE;gBADnD,OAAO,EAAE,MAAM,EACN,UAAU,GAAE,SAAS,mBAAmB,EAAO;CAK/D;AAED,6GAA6G;AAC7G,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,2BAAgC,GAAG,OAAO,CAAC,OAAO,CAAC,CAGpH;AAED,qGAAqG;AACrG,wBAAsB,qBAAqB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,OAAO,CAAC,CAMlB;AAWD,qHAAqH;AACrH,wBAAsB,0BAA0B,CAC5C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,wHAAwH;AACxH,wBAAgB,kBAAkB,CAC9B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,IAAI,SAAK,EACT,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAM,EACrC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GAC1C,mBAAmB,EAAE,CAwDvB"}
@@ -19,12 +19,21 @@ export async function loadStructuredConfig(path, options = {}) {
19
19
  }
20
20
  /** Parses a config string (YAML or JSON) and validates against its declared `$schema` if present. */
21
21
  export async function parseStructuredConfig(content, source, options = {}) {
22
- const parsed = source.endsWith('.json') ? JSON.parse(content) : parseYaml(content);
22
+ const parsed = source.endsWith('.json') ? parseJsonContent(content, source) : parseYaml(content);
23
23
  if (options.validateSchema !== false) {
24
24
  await validateDeclaredJsonSchema(parsed, source, options);
25
25
  }
26
26
  return parsed;
27
27
  }
28
+ /** Strict JSON parse that rethrows with the offending source named; config parse failures stay fail-loud. */
29
+ function parseJsonContent(content, source) {
30
+ try {
31
+ return JSON.parse(content);
32
+ }
33
+ catch (error) {
34
+ throw new StructuredConfigSchemaError(`Failed to parse JSON config "${source}": ${error.message}`);
35
+ }
36
+ }
28
37
  /** Extracts the `$schema` reference from a parsed config object, resolves and fetches the schema, then validates. */
29
38
  export async function validateDeclaredJsonSchema(value, source, options = {}) {
30
39
  if (!isObject(value))
@@ -204,11 +213,14 @@ function splitPackageSpecifier(specifier) {
204
213
  }
205
214
  async function readSchema(schemaLocation, options) {
206
215
  if (isRemoteRef(schemaLocation)) {
207
- const fetchFn = options.fetch;
208
- if (fetchFn === undefined) {
209
- throw new StructuredConfigSchemaError(`Refusing to fetch remote JSON schema "${schemaLocation}": pass a fetch implementation to opt in`);
216
+ // Dual gate (task 0060 F2): remote fetch requires BOTH the explicit allowRemote opt-in
217
+ // AND an injected fetch implementation. `allowRemote` alone must not imply a built-in
218
+ // fetch (that path never existed and would be an unbounded SSRF/DoS surface); `fetch`
219
+ // alone must not silently enable network I/O the caller never authorised.
220
+ if (options.allowRemote !== true || options.fetch === undefined) {
221
+ throw new StructuredConfigSchemaError(`Refusing to fetch remote JSON schema "${schemaLocation}": pass both allowRemote: true and a fetch implementation to opt in`);
210
222
  }
211
- const response = await fetchFn(schemaLocation);
223
+ const response = await options.fetch(schemaLocation);
212
224
  if (!response.ok) {
213
225
  throw new StructuredConfigSchemaError(`Failed to fetch JSON schema "${schemaLocation}": HTTP ${response.status}`);
214
226
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-runtime",
3
- "version": "0.4.28",
3
+ "version": "0.4.31",
4
4
  "description": "@gobing-ai/ts-runtime — Runtime abstractions for Bun, Node, and Cloudflare Workers.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -58,17 +58,17 @@
58
58
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-runtime-v<version> && git push --tags' && exit 1"
59
59
  },
60
60
  "dependencies": {
61
- "@gobing-ai/ts-utils": "^0.4.28",
61
+ "@gobing-ai/ts-utils": "^0.4.31",
62
62
  "execa": "^9.5.0",
63
63
  "yaml": "^2.7.0",
64
64
  "zod": "^4.1.0"
65
65
  },
66
66
  "devDependencies": {
67
- "@gobing-ai/ts-db": "^0.4.28",
67
+ "@gobing-ai/ts-db": "^0.4.31",
68
68
  "@types/bun": "1.3.14"
69
69
  },
70
70
  "peerDependencies": {
71
- "@gobing-ai/ts-db": "^0.4.28"
71
+ "@gobing-ai/ts-db": "^0.4.31"
72
72
  },
73
73
  "peerDependenciesMeta": {
74
74
  "@gobing-ai/ts-db": {
package/src/fs.ts CHANGED
@@ -27,12 +27,17 @@ export async function atomicWriteJson(
27
27
  await atomicWriteFile(path, `${JSON.stringify(value, null, 2)}\n`, fs);
28
28
  }
29
29
 
30
- /** Reads and parses a JSON file. */
30
+ /** Reads and parses a JSON file. Throws with the file named when the content is not valid JSON. */
31
31
  export async function readJsonFile<T = unknown>(
32
32
  path: string,
33
33
  fs: CanonicalFileSystem = createNodeFileSystem(),
34
34
  ): Promise<T> {
35
- return JSON.parse(await fs.readFile(path)) as T;
35
+ const content = await fs.readFile(path);
36
+ try {
37
+ return JSON.parse(content) as T;
38
+ } catch (error) {
39
+ throw new Error(`Failed to parse JSON file "${path}": ${(error as Error).message}`);
40
+ }
36
41
  }
37
42
 
38
43
  /** Writes a value as JSON with 2-space indentation and trailing newline. */
@@ -47,25 +52,57 @@ export async function writeJsonFile(
47
52
  /**
48
53
  * Recursively walks a directory, returning sorted paths to all files,
49
54
  * optionally excluding entries by name.
55
+ *
56
+ * Cycle-safe and root-confined (task 0060 F3): directory symlinks are tracked
57
+ * by their canonical real path, so a link back to an ancestor (or the start
58
+ * root itself) is walked once at most, and a link escaping the start root is
59
+ * skipped. In-root directory symlinks are still traversed — importer and
60
+ * rule-engine walk `$HOME/...` trees that are often symlinks into a dotfiles
61
+ * repo. When the filesystem has no `realPath` (in-memory / CF stubs), today's
62
+ * follow-`stat` behaviour is kept (no ambient capability).
50
63
  */
51
64
  export async function walkDir(
52
65
  path: string,
53
66
  fs: CanonicalFileSystem = createNodeFileSystem(),
54
67
  exclude?: Set<string>,
55
68
  ): Promise<string[]> {
56
- const entries = (await fs.readDir(path)).sort();
69
+ const startReal = fs.realPath?.(path) ?? path;
70
+ const visited = new Set<string>([startReal]);
57
71
  const result: string[] = [];
72
+ await walkDirInto(path, fs, exclude, startReal, visited, result);
73
+ return result;
74
+ }
75
+
76
+ async function walkDirInto(
77
+ path: string,
78
+ fs: CanonicalFileSystem,
79
+ exclude: Set<string> | undefined,
80
+ startReal: string,
81
+ visited: Set<string>,
82
+ result: string[],
83
+ ): Promise<void> {
84
+ const entries = (await fs.readDir(path)).sort();
58
85
  for (const entry of entries) {
59
86
  if (exclude?.has(entry)) continue;
60
87
  const fullPath = joinPath(path, entry);
61
88
  const entryStat = await fs.stat(fullPath);
62
89
  if (entryStat?.isDirectory()) {
63
- result.push(...(await walkDir(fullPath, fs, exclude)));
90
+ const entryReal = fs.realPath?.(fullPath) ?? fullPath;
91
+ if (visited.has(entryReal)) continue; // cycle or already-walked real dir
92
+ if (
93
+ fs.realPath !== undefined &&
94
+ entryReal !== startReal &&
95
+ !entryReal.startsWith(`${startReal}/`) &&
96
+ !entryReal.startsWith(`${startReal}\\`)
97
+ ) {
98
+ continue; // symlink escapes the start root
99
+ }
100
+ visited.add(entryReal);
101
+ await walkDirInto(fullPath, fs, exclude, startReal, visited, result);
64
102
  } else if (entryStat?.isFile()) {
65
103
  result.push(fullPath);
66
104
  }
67
105
  }
68
- return result;
69
106
  }
70
107
 
71
108
  /**
@@ -1,5 +1,6 @@
1
1
  import { isatty } from 'node:tty';
2
2
  import { type Options as ExecaOptions, execa } from 'execa';
3
+ import { getProcessEnv } from './config';
3
4
  import type { ProcessExecutionSource, ProcessRegistry } from './process-registry';
4
5
  import type { RuntimePaths } from './runtime-paths';
5
6
 
@@ -48,6 +49,13 @@ export interface ProcessOptions {
48
49
  args?: string[];
49
50
  cwd?: string;
50
51
  env?: Record<string, string>;
52
+ /**
53
+ * `'merge'` (default): a partial `env` extends the parent environment (execa
54
+ * `extendEnv: true` semantics). `'replace'`: the child gets exactly `env` and
55
+ * none of the parent variables. Task 0060 R5 unified the default to merge so
56
+ * `run` and `runStreaming` share one contract.
57
+ */
58
+ envMode?: 'merge' | 'replace';
51
59
  timeout?: number;
52
60
  maxOutput?: number;
53
61
  label?: string;
@@ -107,6 +115,13 @@ export interface ProcessResult {
107
115
  /** Reason a process completion event was emitted. */
108
116
  export type ProcessExitReason = 'exit' | 'signal' | 'timeout' | 'error';
109
117
 
118
+ function processEventSeverity(reason: ProcessExitReason, exitCode: number | null): ProcessEventDetail['severity'] {
119
+ if (reason === 'error') return 'error';
120
+ if (reason === 'timeout') return 'warning';
121
+ if (exitCode !== null && exitCode !== 0) return 'warning';
122
+ return 'info';
123
+ }
124
+
110
125
  /** Payload emitted for process execution observability. */
111
126
  export interface ProcessEventDetail {
112
127
  command: string;
@@ -118,6 +133,8 @@ export interface ProcessEventDetail {
118
133
  timestamp: string;
119
134
  label?: string;
120
135
  error?: string;
136
+ /** Producer-owned observability severity. */
137
+ severity: 'info' | 'warning' | 'error';
121
138
  }
122
139
 
123
140
  /** Zero-dependency structural event sink for process observability. */
@@ -142,6 +159,12 @@ export interface PipeProcessOptions {
142
159
  args?: string[];
143
160
  cwd?: string;
144
161
  env?: Record<string, string>;
162
+ /**
163
+ * `'merge'` (default): a partial `env` extends the parent environment, matching
164
+ * {@link ProcessExecutor.run}. `'replace'`: the child gets exactly `env`.
165
+ * Previously `runStreaming` always replaced; task 0060 R5 made merge the default.
166
+ */
167
+ envMode?: 'merge' | 'replace';
145
168
  label?: string;
146
169
  /**
147
170
  * Registry metadata (spur#0264). Defaults: source `'other'` for streaming.
@@ -155,6 +178,23 @@ export interface PipeProcessOptions {
155
178
  /** Signal values accepted by subprocess kill. */
156
179
  type BunSubprocess = ReturnType<typeof Bun.spawn>;
157
180
 
181
+ /**
182
+ * Resolve the child env per the unified contract (task 0060 R5): a partial env
183
+ * extends the parent environment unless `envMode: 'replace'` opts out. Parent
184
+ * values can be `undefined` (deleted keys) — Bun's env is string-only, so filter them.
185
+ */
186
+ function resolveChildEnv(
187
+ env: Record<string, string>,
188
+ envMode: 'merge' | 'replace' | undefined,
189
+ ): Record<string, string> {
190
+ if (envMode === 'replace') return env;
191
+ const merged: Record<string, string> = {};
192
+ for (const [key, value] of Object.entries(getProcessEnv())) {
193
+ if (value !== undefined) merged[key] = value;
194
+ }
195
+ return { ...merged, ...env };
196
+ }
197
+
158
198
  /** Signal values accepted by subprocess kill (e.g. 'SIGTERM', 'SIGKILL'). */
159
199
  export type ProcessSignal = Parameters<BunSubprocess['kill']>[0];
160
200
 
@@ -249,6 +289,7 @@ export class NodeProcessExecutor implements ProcessExecutor {
249
289
  durationMs: 0,
250
290
  reason: 'exit',
251
291
  timestamp: startedIso,
292
+ severity: 'info',
252
293
  ...(options.label !== undefined ? { label: options.label } : {}),
253
294
  });
254
295
 
@@ -341,6 +382,7 @@ export class NodeProcessExecutor implements ProcessExecutor {
341
382
  durationMs: 0,
342
383
  reason: 'exit',
343
384
  timestamp: startedIso,
385
+ severity: 'info',
344
386
  ...(options.label !== undefined ? { label: options.label } : {}),
345
387
  });
346
388
  const subprocess = Bun.spawn({
@@ -349,7 +391,7 @@ export class NodeProcessExecutor implements ProcessExecutor {
349
391
  stdout: 'pipe',
350
392
  stderr: 'pipe',
351
393
  ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
352
- ...(options.env !== undefined ? { env: options.env } : {}),
394
+ ...(options.env !== undefined ? { env: resolveChildEnv(options.env, options.envMode) } : {}),
353
395
  });
354
396
  const pipe = new BunPipeProcess(subprocess);
355
397
  if (pipe.pid !== null) {
@@ -372,6 +414,7 @@ export class NodeProcessExecutor implements ProcessExecutor {
372
414
  durationMs: 0,
373
415
  reason: 'error',
374
416
  timestamp: new Date().toISOString(),
417
+ severity: processEventSeverity('error', null),
375
418
  ...(options.label !== undefined ? { label: options.label } : {}),
376
419
  error: errorMessage(error),
377
420
  });
@@ -437,6 +480,7 @@ export class NodeProcessExecutor implements ProcessExecutor {
437
480
  durationMs: result.durationMs,
438
481
  reason,
439
482
  timestamp: new Date().toISOString(),
483
+ severity: processEventSeverity(reason, result.exitCode),
440
484
  ...(options.label !== undefined ? { label: options.label } : {}),
441
485
  ...(error !== undefined ? { error: errorMessage(error) } : {}),
442
486
  });
@@ -479,6 +523,7 @@ class ObservedPipeProcess implements PipeProcess {
479
523
  durationMs: Date.now() - context.startedAt,
480
524
  reason: this.killedWith !== undefined ? 'signal' : 'exit',
481
525
  timestamp: new Date().toISOString(),
526
+ severity: processEventSeverity(this.killedWith !== undefined ? 'signal' : 'exit', exitCode),
482
527
  ...(context.label !== undefined ? { label: context.label } : {}),
483
528
  });
484
529
  return exitCode;
@@ -76,13 +76,22 @@ export async function parseStructuredConfig(
76
76
  source: string,
77
77
  options: StructuredConfigLoadOptions = {},
78
78
  ): Promise<unknown> {
79
- const parsed = source.endsWith('.json') ? JSON.parse(content) : parseYaml(content);
79
+ const parsed = source.endsWith('.json') ? parseJsonContent(content, source) : parseYaml(content);
80
80
  if (options.validateSchema !== false) {
81
81
  await validateDeclaredJsonSchema(parsed, source, options);
82
82
  }
83
83
  return parsed;
84
84
  }
85
85
 
86
+ /** Strict JSON parse that rethrows with the offending source named; config parse failures stay fail-loud. */
87
+ function parseJsonContent(content: string, source: string): unknown {
88
+ try {
89
+ return JSON.parse(content);
90
+ } catch (error) {
91
+ throw new StructuredConfigSchemaError(`Failed to parse JSON config "${source}": ${(error as Error).message}`);
92
+ }
93
+ }
94
+
86
95
  /** Extracts the `$schema` reference from a parsed config object, resolves and fetches the schema, then validates. */
87
96
  export async function validateDeclaredJsonSchema(
88
97
  value: unknown,
@@ -344,13 +353,16 @@ function splitPackageSpecifier(specifier: string): { pkg: string; subpath: strin
344
353
 
345
354
  async function readSchema(schemaLocation: string, options: StructuredConfigLoadOptions): Promise<string> {
346
355
  if (isRemoteRef(schemaLocation)) {
347
- const fetchFn = options.fetch;
348
- if (fetchFn === undefined) {
356
+ // Dual gate (task 0060 F2): remote fetch requires BOTH the explicit allowRemote opt-in
357
+ // AND an injected fetch implementation. `allowRemote` alone must not imply a built-in
358
+ // fetch (that path never existed and would be an unbounded SSRF/DoS surface); `fetch`
359
+ // alone must not silently enable network I/O the caller never authorised.
360
+ if (options.allowRemote !== true || options.fetch === undefined) {
349
361
  throw new StructuredConfigSchemaError(
350
- `Refusing to fetch remote JSON schema "${schemaLocation}": pass a fetch implementation to opt in`,
362
+ `Refusing to fetch remote JSON schema "${schemaLocation}": pass both allowRemote: true and a fetch implementation to opt in`,
351
363
  );
352
364
  }
353
- const response = await fetchFn(schemaLocation);
365
+ const response = await options.fetch(schemaLocation);
354
366
  if (!response.ok) {
355
367
  throw new StructuredConfigSchemaError(
356
368
  `Failed to fetch JSON schema "${schemaLocation}": HTTP ${response.status}`,