@vibe-agent-toolkit/utils 0.1.39-rc.8 → 0.1.39

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.
Files changed (41) hide show
  1. package/dist/fs/file-hash.d.ts +17 -0
  2. package/dist/fs/file-hash.d.ts.map +1 -0
  3. package/dist/fs/file-hash.js +23 -0
  4. package/dist/fs/file-hash.js.map +1 -0
  5. package/dist/glob/glob-pattern.d.ts +65 -0
  6. package/dist/glob/glob-pattern.d.ts.map +1 -0
  7. package/dist/glob/glob-pattern.js +133 -0
  8. package/dist/glob/glob-pattern.js.map +1 -0
  9. package/dist/index.d.ts +4 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +7 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/link-auth/resolve-token.d.ts +7 -1
  14. package/dist/link-auth/resolve-token.d.ts.map +1 -1
  15. package/dist/link-auth/resolve-token.js +8 -3
  16. package/dist/link-auth/resolve-token.js.map +1 -1
  17. package/dist/link-auth/resolve.d.ts +44 -0
  18. package/dist/link-auth/resolve.d.ts.map +1 -1
  19. package/dist/link-auth/resolve.js +12 -1
  20. package/dist/link-auth/resolve.js.map +1 -1
  21. package/dist/path-utils.d.ts +65 -3
  22. package/dist/path-utils.d.ts.map +1 -1
  23. package/dist/path-utils.js +97 -3
  24. package/dist/path-utils.js.map +1 -1
  25. package/dist/skill-test/env-scrub.d.ts +58 -0
  26. package/dist/skill-test/env-scrub.d.ts.map +1 -1
  27. package/dist/skill-test/env-scrub.js +148 -2
  28. package/dist/skill-test/env-scrub.js.map +1 -1
  29. package/dist/skill-test/index.d.ts +1 -1
  30. package/dist/skill-test/index.d.ts.map +1 -1
  31. package/dist/skill-test/index.js +1 -1
  32. package/dist/skill-test/index.js.map +1 -1
  33. package/dist/skill-test/spawn-claude.d.ts +34 -9
  34. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  35. package/dist/skill-test/spawn-claude.js +60 -14
  36. package/dist/skill-test/spawn-claude.js.map +1 -1
  37. package/dist/yaml/surgical-yaml.d.ts +71 -0
  38. package/dist/yaml/surgical-yaml.d.ts.map +1 -0
  39. package/dist/yaml/surgical-yaml.js +314 -0
  40. package/dist/yaml/surgical-yaml.js.map +1 -0
  41. package/package.json +1 -1
@@ -179,6 +179,40 @@ export function mkdirSyncReal(dirPath, options) {
179
179
  export function isAbsolutePath(p) {
180
180
  return path.isAbsolute(p);
181
181
  }
182
+ /**
183
+ * True if `p` is absolute on ANY platform — a POSIX root path (`/etc`), a
184
+ * Windows drive-letter path (`C:\…` or `C:/…`), or a UNC path (`\\host\share`).
185
+ *
186
+ * Unlike {@link isAbsolutePath} (host-platform only), this is host-independent,
187
+ * so config-containment checks reject Windows-absolute paths even when run on
188
+ * POSIX CI, and vice versa. Used to keep config-supplied relative paths (skill
189
+ * `files:` dest) from escaping their anchor directory (zip-slip class).
190
+ *
191
+ * @example
192
+ * isAbsoluteAnyPlatform('/etc/passwd') // true (POSIX)
193
+ * isAbsoluteAnyPlatform('C:\\Users') // true (Windows drive)
194
+ * isAbsoluteAnyPlatform('scripts/cli') // false (relative)
195
+ */
196
+ export function isAbsoluteAnyPlatform(p) {
197
+ return path.posix.isAbsolute(p) || path.win32.isAbsolute(p);
198
+ }
199
+ /**
200
+ * True if `p` contains a `..` parent-directory traversal segment.
201
+ *
202
+ * Forward-slash-normalized, then inspects each `/`-delimited segment — so a
203
+ * `..` is caught regardless of the original OS separator. A containment guard
204
+ * for config-supplied relative paths (skill `files:` dest values, glob magic
205
+ * remainders) that must never climb above their anchor directory.
206
+ *
207
+ * @example
208
+ * hasParentTraversalSegment('a/../b') // true
209
+ * hasParentTraversalSegment('a/b/c') // false
210
+ * hasParentTraversalSegment('..\\evil') // true (backslash normalized)
211
+ * hasParentTraversalSegment('a..b/c') // false (".." must be a whole segment)
212
+ */
213
+ export function hasParentTraversalSegment(p) {
214
+ return toForwardSlash(p).split('/').includes('..');
215
+ }
182
216
  /**
183
217
  * Convert a relative path to absolute
184
218
  *
@@ -260,9 +294,11 @@ export function toForwardSlash(p) {
260
294
  * import { safePath } from '@vibe-agent-toolkit/utils';
261
295
  *
262
296
  * // Always forward slashes, even on Windows
263
- * safePath.join('C:\\Users', 'docs', 'file.md') // → 'C:/Users/docs/file.md'
264
- * safePath.resolve('/project', './docs') // → '/project/docs'
265
- * safePath.relative('/project/docs', '/project') // → '..'
297
+ * safePath.join('C:\\Users', 'docs', 'file.md') // → 'C:/Users/docs/file.md'
298
+ * safePath.resolve('/project', './docs') // → '/project/docs'
299
+ * safePath.relative('/project/docs', '/project') // → '..'
300
+ * safePath.joinUnderRoot('/harness', 'skill-abc') // → '/harness/skill-abc'
301
+ * safePath.joinUnderRoot('/harness', '../escape') // throws Error
266
302
  * ```
267
303
  */
268
304
  export const safePath = {
@@ -278,6 +314,64 @@ export const safePath = {
278
314
  relative(from, to) {
279
315
  return toForwardSlash(path.relative(from, to));
280
316
  },
317
+ /**
318
+ * Join path segments under a security root, throwing if the result would escape.
319
+ *
320
+ * Resolves `root + segments` and verifies the result is strictly inside `root`
321
+ * (or equal to it). Throws when any segment would cause the result to escape:
322
+ *
323
+ * - A `..` traversal that climbs above root
324
+ * - An absolute POSIX path segment (e.g. `/etc/passwd`)
325
+ * - A Windows drive-letter segment (e.g. `C:\Users\evil`)
326
+ *
327
+ * On success returns a forward-slash-normalized absolute path (consistent with
328
+ * the other `safePath` helpers).
329
+ *
330
+ * **Use this instead of `safePath.join(root, segment)` whenever `segment` may
331
+ * contain caller-controlled input** — this is the bug class that the original
332
+ * skill-test staging code was vulnerable to on Windows.
333
+ *
334
+ * @returns Forward-slash absolute path guaranteed to be inside `root`.
335
+ * @throws {Error} If the resolved path would escape `root`.
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * // ✅ Safe — throws if caller passes '../../../etc'
340
+ * const dest = safePath.joinUnderRoot(harnessRoot, stagedDirName(item.name));
341
+ *
342
+ * // ❌ Unsafe — silently escapes on Windows with absolute segment
343
+ * const dest = safePath.join(harnessRoot, item.name);
344
+ * ```
345
+ */
346
+ joinUnderRoot(root, ...segments) {
347
+ // Eagerly reject any segment that is absolute (POSIX or Windows drive-letter)
348
+ // BEFORE resolving, so the error message can name the offending segment.
349
+ for (const seg of segments) {
350
+ if (path.isAbsolute(seg)) {
351
+ throw new Error(`safePath.joinUnderRoot: segment "${seg}" is absolute and escapes root "${root}".`);
352
+ }
353
+ // Windows drive-letter check for POSIX hosts (path.isAbsolute won't catch
354
+ // 'C:\...' on POSIX, but node's path.win32.isAbsolute does).
355
+ if (path.win32.isAbsolute(seg)) {
356
+ throw new Error(`safePath.joinUnderRoot: segment "${seg}" contains a Windows drive letter and escapes root "${root}".`);
357
+ }
358
+ }
359
+ const resolvedRoot = path.resolve(root);
360
+ const resolvedResult = segments.length > 0
361
+ ? path.resolve(resolvedRoot, ...segments)
362
+ : resolvedRoot;
363
+ // Containment check: normalize both to forward slashes so the comparison
364
+ // is platform-independent and no path.sep is needed in string operations.
365
+ const fwdRoot = toForwardSlash(resolvedRoot);
366
+ const fwdResult = toForwardSlash(resolvedResult);
367
+ // Result must equal root or start with root + '/' (not just startsWith(root)
368
+ // which would match '/rootEvil' when root is '/root').
369
+ const rootPrefix = fwdRoot.endsWith('/') ? fwdRoot : `${fwdRoot}/`;
370
+ if (fwdResult !== fwdRoot && !fwdResult.startsWith(rootPrefix)) {
371
+ throw new Error(`safePath.joinUnderRoot: result "${fwdResult}" escapes root "${fwdRoot}".`);
372
+ }
373
+ return fwdResult;
374
+ },
281
375
  };
282
376
  /**
283
377
  * Resolve an OS-native absolute path from an ESM module's `import.meta.url` and
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"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"}
@@ -11,4 +11,62 @@ export interface ForwardEnvOptions {
11
11
  modelVars?: string[];
12
12
  }
13
13
  export declare function buildForwardedEnv(source: NodeJS.ProcessEnv, opts: ForwardEnvOptions): NodeJS.ProcessEnv;
14
+ /**
15
+ * The full set of env var names protected from declared override. A declared
16
+ * passEnv/injectEnv entry naming one of these is ignored (the protected value
17
+ * wins) and a warning is surfaced — a test must never clobber PATH, the auth
18
+ * credentials, or the admin key. `modelVars` (run-specific model env var names)
19
+ * join the set so a declared var can't shadow them either.
20
+ */
21
+ export declare function protectedEnvNames(modelVars?: readonly string[]): Set<string>;
22
+ /**
23
+ * Returns true if `name` is in `protectedSet`.
24
+ *
25
+ * On Windows (win32) env names are case-insensitive — `path` and `PATH` are the
26
+ * same variable — so we upper-case both sides before comparing. On POSIX, names
27
+ * are genuinely case-distinct, so the comparison stays exact.
28
+ *
29
+ * The `platform` parameter defaults to `process.platform` but is exposed so
30
+ * unit tests can exercise the win32 branch on any host OS.
31
+ */
32
+ export declare function isProtectedName(name: string, protectedSet: Set<string>, platform?: string): boolean;
33
+ /** Declared test env (Features A + B) to union onto a forwarded env. */
34
+ export interface DeclaredEnvInput {
35
+ /** Parent env to read Feature-A pass-through values from. */
36
+ source: NodeJS.ProcessEnv;
37
+ /** Feature A: names to forward from `source` if present. */
38
+ passEnv?: readonly string[];
39
+ /** Feature B: explicit key→value injections (already interpolated). */
40
+ injectEnv?: Record<string, string>;
41
+ /** Run-specific model env var names that are also protected. */
42
+ modelVars?: readonly string[];
43
+ }
44
+ export interface DeclaredEnvResult {
45
+ /** The forwarded env with declared additions unioned in. */
46
+ env: NodeJS.ProcessEnv;
47
+ /** Human-readable warnings (protected-key collisions). */
48
+ warnings: string[];
49
+ /** Names injected via Feature B (shown in the transparency line). */
50
+ injected: string[];
51
+ /** Names passed through via Feature A (redacted in the transparency line). */
52
+ passedThrough: string[];
53
+ }
54
+ /**
55
+ * Union declared test env (Features A + B) onto an already-built forwarded env.
56
+ * Protected keys always win: a declared name colliding with a process-essential,
57
+ * auth, model, or admin var is ignored and a warning emitted. Feature-B injection
58
+ * (explicit value) takes precedence over Feature-A pass-through for the same key.
59
+ * The input `base` object is never mutated.
60
+ */
61
+ export declare function applyDeclaredEnv(base: NodeJS.ProcessEnv, input: DeclaredEnvInput): DeclaredEnvResult;
62
+ /**
63
+ * Render the single-line stderr transparency summary of the forwarded env. Key
64
+ * names are always shown. Auth/secret values are redacted; Feature-A pass-through
65
+ * values are redacted (host-sourced, may be a secret); Feature-B injected values
66
+ * are shown (they come from committed config).
67
+ */
68
+ export declare function formatForwardedEnvLine(env: NodeJS.ProcessEnv, classified: {
69
+ injected: readonly string[];
70
+ passedThrough: readonly string[];
71
+ }): string;
14
72
  //# sourceMappingURL=env-scrub.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"env-scrub.d.ts","sourceRoot":"","sources":["../../src/skill-test/env-scrub.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,iBAAiB;IAChC,sHAAsH;IACtH,iBAAiB,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAWD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,UAAU,EACzB,IAAI,EAAE,iBAAiB,GACtB,MAAM,CAAC,UAAU,CAuBnB"}
1
+ {"version":3,"file":"env-scrub.d.ts","sourceRoot":"","sources":["../../src/skill-test/env-scrub.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,iBAAiB;IAChC,sHAAsH;IACtH,iBAAiB,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AA2DD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,UAAU,EACzB,IAAI,EAAE,iBAAiB,GACtB,MAAM,CAAC,UAAU,CAuBnB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,SAAS,MAAM,EAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAShF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,EACzB,QAAQ,GAAE,MAAyB,GAClC,OAAO,CAST;AAED,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC;IAC1B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,gEAAgE;IAChE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8EAA8E;IAC9E,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAiCpG;AAKD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,UAAU,EAAE;IAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC5E,MAAM,CAUR"}
@@ -4,12 +4,59 @@
4
4
  * prefix — that would silently leak ANTHROPIC_ADMIN_API_KEY, proxy overrides,
5
5
  * etc. into untrusted skill code. Anything not enumerated is dropped.
6
6
  */
7
- /** Process-essential vars that must survive for the child to even start. */
8
- const PROCESS_ESSENTIALS = ['PATH', 'Path', 'HOME', 'USERPROFILE', 'SystemRoot', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL'];
7
+ /**
8
+ * Process-essential vars that must survive for the child to even start. Includes
9
+ * the standard Windows essentials (APPDATA/LOCALAPPDATA — Claude's default config
10
+ * dir derives from these when CLAUDE_CONFIG_DIR is unset; SystemDrive, windir,
11
+ * PATHEXT, COMSPEC — needed to locate and launch executables). None are
12
+ * secret-bearing; the allowlist stays strict/exact-name.
13
+ *
14
+ * USER/LOGNAME (the POSIX username vars) are load-bearing for subscription auth
15
+ * on macOS: the `claude` CLI resolves the OAuth/subscription token from the login
16
+ * Keychain, and that lookup needs $USER — strip it and `claude auth status`
17
+ * reports loggedIn:false even with an active subscription, so `--auth
18
+ * subscription` (and inherit's subscription fallback) wrongly fail preflight, and
19
+ * the experimenter child can't authenticate. Neither is secret-bearing (the
20
+ * username is already derivable from the forwarded HOME), so they fit the policy.
21
+ * Not needed for api-key auth (key is self-contained) or Linux subscription auth
22
+ * (token is a HOME-relative file) — which is why this gap went unnoticed.
23
+ */
24
+ const PROCESS_ESSENTIALS = [
25
+ 'PATH', 'Path', 'HOME', 'USER', 'LOGNAME', 'USERPROFILE', 'SystemRoot', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL',
26
+ 'APPDATA', 'LOCALAPPDATA', 'SystemDrive', 'windir', 'PATHEXT', 'COMSPEC',
27
+ ];
9
28
  /** The only auth/config vars ever forwarded. */
10
29
  const AUTH_CONFIG_ALLOWLIST = ['CLAUDE_CONFIG_DIR'];
11
30
  /** The active inference credential candidates (forwarded unless scrubbed). */
12
31
  const INFERENCE_CREDENTIALS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'];
32
+ /**
33
+ * Vars that a declared env entry (injectEnv / passEnv) may NEVER override.
34
+ * These are deny-only — they are not in the forward allowlist either.
35
+ *
36
+ * WHY: Any of these names let a committed config attack without skill code:
37
+ * • ANTHROPIC_*_BASE_URL / ANTHROPIC_API_URL — redirect the inference endpoint
38
+ * so the forwarded ANTHROPIC_API_KEY is sent to an attacker-controlled server.
39
+ * • *_PROXY / *_proxy — redirect all HTTPS traffic (including API calls) through
40
+ * a MITM proxy even when the base URL looks correct.
41
+ * • NODE_OPTIONS — inject arbitrary code into the claude child process via
42
+ * --require or --import before any userland code runs.
43
+ * • NODE_EXTRA_CA_CERTS — add a rogue CA certificate, enabling TLS interception.
44
+ * • LD_PRELOAD / LD_LIBRARY_PATH / DYLD_INSERT_LIBRARIES / DYLD_LIBRARY_PATH —
45
+ * the OS-linker siblings of NODE_OPTIONS: load an attacker .so/.dylib into the
46
+ * child before any userland code runs (native code injection, bundler-agnostic).
47
+ * • NODE_PATH — prepend an attacker directory to Node's module resolution so a
48
+ * rogue package shadows a legitimate `require`.
49
+ * • GIT_SSH_COMMAND — run an arbitrary command on any git operation the harness
50
+ * performs (e.g. a git: source clone).
51
+ */
52
+ const CREDENTIAL_ROUTING_DENY = [
53
+ 'ANTHROPIC_BASE_URL', 'ANTHROPIC_API_URL', 'ANTHROPIC_BEDROCK_BASE_URL', 'ANTHROPIC_VERTEX_BASE_URL',
54
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
55
+ 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
56
+ 'NODE_OPTIONS', 'NODE_EXTRA_CA_CERTS', 'NODE_PATH',
57
+ 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH',
58
+ 'GIT_SSH_COMMAND',
59
+ ];
13
60
  export function buildForwardedEnv(source, opts) {
14
61
  const out = {};
15
62
  const allow = new Set([
@@ -32,4 +79,103 @@ export function buildForwardedEnv(source, opts) {
32
79
  delete out['ANTHROPIC_ADMIN_API_KEY'];
33
80
  return out;
34
81
  }
82
+ /**
83
+ * The full set of env var names protected from declared override. A declared
84
+ * passEnv/injectEnv entry naming one of these is ignored (the protected value
85
+ * wins) and a warning is surfaced — a test must never clobber PATH, the auth
86
+ * credentials, or the admin key. `modelVars` (run-specific model env var names)
87
+ * join the set so a declared var can't shadow them either.
88
+ */
89
+ export function protectedEnvNames(modelVars = []) {
90
+ return new Set([
91
+ ...PROCESS_ESSENTIALS,
92
+ ...AUTH_CONFIG_ALLOWLIST,
93
+ ...INFERENCE_CREDENTIALS,
94
+ 'ANTHROPIC_ADMIN_API_KEY',
95
+ ...CREDENTIAL_ROUTING_DENY,
96
+ ...modelVars,
97
+ ]);
98
+ }
99
+ /**
100
+ * Returns true if `name` is in `protectedSet`.
101
+ *
102
+ * On Windows (win32) env names are case-insensitive — `path` and `PATH` are the
103
+ * same variable — so we upper-case both sides before comparing. On POSIX, names
104
+ * are genuinely case-distinct, so the comparison stays exact.
105
+ *
106
+ * The `platform` parameter defaults to `process.platform` but is exposed so
107
+ * unit tests can exercise the win32 branch on any host OS.
108
+ */
109
+ export function isProtectedName(name, protectedSet, platform = process.platform) {
110
+ if (platform === 'win32') {
111
+ const upper = name.toUpperCase();
112
+ for (const p of protectedSet) {
113
+ if (p.toUpperCase() === upper)
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ return protectedSet.has(name);
119
+ }
120
+ /**
121
+ * Union declared test env (Features A + B) onto an already-built forwarded env.
122
+ * Protected keys always win: a declared name colliding with a process-essential,
123
+ * auth, model, or admin var is ignored and a warning emitted. Feature-B injection
124
+ * (explicit value) takes precedence over Feature-A pass-through for the same key.
125
+ * The input `base` object is never mutated.
126
+ */
127
+ export function applyDeclaredEnv(base, input) {
128
+ const out = { ...base };
129
+ const protectedNames = protectedEnvNames(input.modelVars);
130
+ const warnings = [];
131
+ const injected = [];
132
+ const passedThrough = [];
133
+ const injectKeys = new Set(Object.keys(input.injectEnv ?? {}));
134
+ // Feature A: pass-through by name.
135
+ for (const name of input.passEnv ?? []) {
136
+ if (isProtectedName(name, protectedNames)) {
137
+ warnings.push(`passEnv "${name}" ignored: it collides with a protected variable.`);
138
+ continue;
139
+ }
140
+ if (injectKeys.has(name))
141
+ continue; // Feature B wins for the same key.
142
+ const value = input.source[name];
143
+ if (value === undefined)
144
+ continue; // absent host var → simply not forwarded.
145
+ out[name] = value;
146
+ passedThrough.push(name);
147
+ }
148
+ // Feature B: explicit value injection.
149
+ for (const [name, value] of Object.entries(input.injectEnv ?? {})) {
150
+ if (isProtectedName(name, protectedNames)) {
151
+ warnings.push(`env "${name}" ignored: it collides with a protected variable.`);
152
+ continue;
153
+ }
154
+ out[name] = value;
155
+ injected.push(name);
156
+ }
157
+ return { env: out, warnings, injected, passedThrough };
158
+ }
159
+ /** Names whose VALUES are secrets and must be redacted in the transparency line. */
160
+ const SECRET_NAMES = new Set(INFERENCE_CREDENTIALS);
161
+ /**
162
+ * Render the single-line stderr transparency summary of the forwarded env. Key
163
+ * names are always shown. Auth/secret values are redacted; Feature-A pass-through
164
+ * values are redacted (host-sourced, may be a secret); Feature-B injected values
165
+ * are shown (they come from committed config).
166
+ */
167
+ export function formatForwardedEnvLine(env, classified) {
168
+ const injected = new Set(classified.injected);
169
+ const passedThrough = new Set(classified.passedThrough);
170
+ const parts = Object.keys(env).map((name) => {
171
+ if (injected.has(name))
172
+ return `${name}=${env[name] ?? ''}`;
173
+ if (passedThrough.has(name))
174
+ return `${name}(passed-through, redacted)`;
175
+ if (SECRET_NAMES.has(name))
176
+ return `${name}(redacted)`;
177
+ return name;
178
+ });
179
+ return `forwarded env: ${parts.join(', ')}`;
180
+ }
35
181
  //# sourceMappingURL=env-scrub.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"env-scrub.js","sourceRoot":"","sources":["../../src/skill-test/env-scrub.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,4EAA4E;AAC5E,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAC;AAErI,gDAAgD;AAChD,MAAM,qBAAqB,GAAG,CAAC,mBAAmB,CAAU,CAAC;AAE7D,8EAA8E;AAC9E,MAAM,qBAAqB,GAAG,CAAC,mBAAmB,EAAE,sBAAsB,CAAU,CAAC;AAErF,MAAM,UAAU,iBAAiB,CAC/B,MAAyB,EACzB,IAAuB;IAEvB,MAAM,GAAG,GAAsB,EAAE,CAAC;IAElC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS;QAC5B,GAAG,kBAAkB;QACrB,GAAG,qBAAqB;QACxB,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;KAC1B,CAAC,CAAC;IACH,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,qBAAqB;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IAC7C,CAAC;IAED,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,OAAO,GAAG,CAAC,yBAAyB,CAAC,CAAC;IAEtC,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"env-scrub.js","sourceRoot":"","sources":["../../src/skill-test/env-scrub.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export { buildForwardedEnv, type ForwardEnvOptions } from './env-scrub.js';
1
+ export { applyDeclaredEnv, buildForwardedEnv, formatForwardedEnvLine, protectedEnvNames, type DeclaredEnvInput, type DeclaredEnvResult, type ForwardEnvOptions, } from './env-scrub.js';
2
2
  export { AuthPreflightError, probeAuthStatus, resolveAuth, type AuthMechanism, type AuthMode, type AuthStatusProbe, type AuthStatusResult, type ResolveAuthOptions, type ResolvedAuth, } from './auth-resolver.js';
3
3
  export { assembleClaudeArgs, spawnHeadlessClaude, type ClaudeSpawnArgs, type SpawnHeadlessOptions, type SpawnResult, } from './spawn-claude.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/skill-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,WAAW,GACjB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/skill-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,WAAW,GACjB,MAAM,mBAAmB,CAAC"}
@@ -1,4 +1,4 @@
1
- export { buildForwardedEnv } from './env-scrub.js';
1
+ export { applyDeclaredEnv, buildForwardedEnv, formatForwardedEnvLine, protectedEnvNames, } from './env-scrub.js';
2
2
  export { AuthPreflightError, probeAuthStatus, resolveAuth, } from './auth-resolver.js';
3
3
  export { assembleClaudeArgs, spawnHeadlessClaude, } from './spawn-claude.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/skill-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAA0B,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,WAAW,GAOZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GAIpB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/skill-test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,iBAAiB,GAIlB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,WAAW,GAOZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GAIpB,MAAM,mBAAmB,CAAC"}
@@ -18,14 +18,32 @@ export interface SpawnResult {
18
18
  timedOut: boolean;
19
19
  stalled: boolean;
20
20
  }
21
+ /**
22
+ * SIGKILL the child AND every process in its group. The child is spawned
23
+ * `detached` (POSIX) so it leads a new process group; killing the negated pid
24
+ * reaps backgrounded grandchildren (e.g. a skill that runs `nohup … &`) and
25
+ * orphaned MCP subprocesses that a direct `child.kill()` would leave running.
26
+ *
27
+ * - Guards an undefined pid (child never spawned — nothing to kill).
28
+ * - Swallows the throw (ESRCH) when the group is already gone.
29
+ * - Windows has no POSIX process groups: fall back to `taskkill /T /F`, which
30
+ * terminates the whole process tree.
31
+ */
32
+ export declare function killProcessTree(child: {
33
+ pid?: number | undefined;
34
+ }): void;
21
35
  export interface SpawnHeadlessOptions extends ClaudeSpawnArgs {
22
36
  /**
23
- * Path to the experimenter-prompt file. Its contents are streamed to the
24
- * child's stdin (claude 2.x reads the `-p` prompt from stdin; there is no
25
- * `--prompt-file` flag). Kept off argv so the prompt is never inlined
26
- * (Windows cmd-quoting safety).
37
+ * The experimenter prompt, held IN MEMORY and streamed to the child's stdin
38
+ * (claude 2.x reads the `-p` prompt from stdin; there is no `--prompt-file`
39
+ * flag). Kept off argv so the prompt is never inlined (Windows cmd-quoting
40
+ * safety) AND, deliberately, off disk: the harness stamps a per-run integrity
41
+ * nonce into this prompt, and writing it to a file inside (or beside) the
42
+ * skill-writable sandbox would let untrusted skill code read the nonce back and
43
+ * forge a passing grading.json. Passing it in memory keeps the nonce out of any
44
+ * file the skill can read.
27
45
  */
28
- promptFile: string;
46
+ prompt: string;
29
47
  cwd: string;
30
48
  env: NodeJS.ProcessEnv;
31
49
  timeoutMs: number;
@@ -33,12 +51,19 @@ export interface SpawnHeadlessOptions extends ClaudeSpawnArgs {
33
51
  stallMs?: number;
34
52
  onStdout?: (chunk: string) => void;
35
53
  onStderr?: (chunk: string) => void;
54
+ /**
55
+ * INTERNAL test seam. When set, this executable is spawned instead of resolving
56
+ * `claude` on PATH — letting tests exercise the wall-timeout / stall watchdog
57
+ * against a tiny fake child without a real `claude` install. Production callers
58
+ * never set this; `claude` is resolved via `which`.
59
+ */
60
+ binPath?: string;
36
61
  }
37
62
  /**
38
- * Spawn the headless `claude`. The prompt file is streamed to the child's stdin
39
- * so the session reads its prompt and then sees EOF (it cannot block on input,
40
- * §16). Wall-clock kill on timeout. If `stallMs` is set, a resettable stall
41
- * watchdog kills the child when no output is received for that duration
63
+ * Spawn the headless `claude`. The in-memory prompt is streamed to the child's
64
+ * stdin so the session reads its prompt and then sees EOF (it cannot block on
65
+ * input, §16). Wall-clock kill on timeout. If `stallMs` is set, a resettable
66
+ * stall watchdog kills the child when no output is received for that duration
42
67
  * (stalled: true in result).
43
68
  */
44
69
  export declare function spawnHeadlessClaude(opts: SpawnHeadlessOptions): Promise<SpawnResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-claude.d.ts","sourceRoot":"","sources":["../../src/skill-test/spawn-claude.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,EAAE,CAmBlE;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAqB,SAAQ,eAAe;IAC3D;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsD1F"}
1
+ {"version":3,"file":"spawn-claude.d.ts","sourceRoot":"","sources":["../../src/skill-test/spawn-claude.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,EAAE,CAmBlE;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAAG,IAAI,CAczE;AAED,MAAM,WAAW,oBAAqB,SAAQ,eAAe;IAC3D;;;;;;;;;OASG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,WAAW,CAAC,CAwE1F"}
@@ -1,5 +1,5 @@
1
- import { spawn } from 'node:child_process';
2
- import { createReadStream } from 'node:fs';
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { Readable } from 'node:stream';
3
3
  import which from 'which';
4
4
  /**
5
5
  * Pure assembly of the headless `claude -p` argv (spec §9). The prompt is never
@@ -32,22 +32,67 @@ export function assembleClaudeArgs(opts) {
32
32
  return args;
33
33
  }
34
34
  /**
35
- * Spawn the headless `claude`. The prompt file is streamed to the child's stdin
36
- * so the session reads its prompt and then sees EOF (it cannot block on input,
37
- * §16). Wall-clock kill on timeout. If `stallMs` is set, a resettable stall
38
- * watchdog kills the child when no output is received for that duration
35
+ * SIGKILL the child AND every process in its group. The child is spawned
36
+ * `detached` (POSIX) so it leads a new process group; killing the negated pid
37
+ * reaps backgrounded grandchildren (e.g. a skill that runs `nohup &`) and
38
+ * orphaned MCP subprocesses that a direct `child.kill()` would leave running.
39
+ *
40
+ * - Guards an undefined pid (child never spawned — nothing to kill).
41
+ * - Swallows the throw (ESRCH) when the group is already gone.
42
+ * - Windows has no POSIX process groups: fall back to `taskkill /T /F`, which
43
+ * terminates the whole process tree.
44
+ */
45
+ export function killProcessTree(child) {
46
+ const { pid } = child;
47
+ if (pid === undefined)
48
+ return;
49
+ try {
50
+ if (process.platform === 'win32') {
51
+ // eslint-disable-next-line sonarjs/no-os-command-from-path -- taskkill is a fixed Windows system command (no PATH-injection surface); it is the only POSIX-process-group-free way to terminate the child's tree.
52
+ spawnSync('taskkill', ['/pid', String(pid), '/T', '/F']);
53
+ }
54
+ else {
55
+ // Negated pid → deliver the signal to the whole process group.
56
+ process.kill(-pid, 'SIGKILL');
57
+ }
58
+ }
59
+ catch {
60
+ // ESRCH (no such process/group): the child and its group are already dead.
61
+ }
62
+ }
63
+ /**
64
+ * Spawn the headless `claude`. The in-memory prompt is streamed to the child's
65
+ * stdin so the session reads its prompt and then sees EOF (it cannot block on
66
+ * input, §16). Wall-clock kill on timeout. If `stallMs` is set, a resettable
67
+ * stall watchdog kills the child when no output is received for that duration
39
68
  * (stalled: true in result).
40
69
  */
41
70
  export async function spawnHeadlessClaude(opts) {
42
- const bin = which.sync('claude');
71
+ // opts.binPath is an internal test seam (see SpawnHeadlessOptions); production
72
+ // callers leave it unset and `claude` is resolved on PATH.
73
+ const bin = opts.binPath ?? which.sync('claude');
43
74
  const args = assembleClaudeArgs(opts);
44
75
  return await new Promise((resolve, reject) => {
45
- const child = spawn(bin, args, { cwd: opts.cwd, env: opts.env, stdio: ['pipe', 'pipe', 'pipe'] });
76
+ const child = spawn(bin, args, {
77
+ cwd: opts.cwd,
78
+ env: opts.env,
79
+ stdio: ['pipe', 'pipe', 'pipe'],
80
+ // POSIX: lead a new process group so the kill paths can SIGKILL the whole
81
+ // group (backgrounded grandchildren + MCP subprocesses), not just the
82
+ // direct child. Windows has no process groups (taskkill handles the tree).
83
+ detached: process.platform !== 'win32',
84
+ });
46
85
  let timedOut = false;
47
86
  let stalled = false;
87
+ // Feed the prompt to the child via stdin (claude 2.x has no --prompt-file).
88
+ // Built from the in-memory string (never a file — the prompt carries a secret
89
+ // per-run nonce that must not land on the skill-readable filesystem). Created
90
+ // here (before the timers) so every kill path can destroy it if the child is
91
+ // SIGKILL'd before stdin is fully consumed.
92
+ const promptStream = Readable.from([opts.prompt], { objectMode: false });
48
93
  const wallTimer = setTimeout(() => {
49
94
  timedOut = true;
50
- child.kill('SIGKILL');
95
+ killProcessTree(child);
51
96
  }, opts.timeoutMs);
52
97
  // Stall watchdog: reset on every stdout/stderr chunk; fire if silent for stallMs.
53
98
  let stallTimer;
@@ -58,7 +103,7 @@ export async function spawnHeadlessClaude(opts) {
58
103
  clearTimeout(stallTimer);
59
104
  stallTimer = setTimeout(() => {
60
105
  stalled = true;
61
- child.kill('SIGKILL');
106
+ killProcessTree(child);
62
107
  }, stallMs);
63
108
  };
64
109
  resetStallTimer();
@@ -69,13 +114,14 @@ export async function spawnHeadlessClaude(opts) {
69
114
  clearTimeout(wallTimer);
70
115
  if (stallTimer !== undefined)
71
116
  clearTimeout(stallTimer);
117
+ // Destroy the prompt stream on every terminal path (timeout/stall reach
118
+ // here via 'close'; error paths call it directly) so an unconsumed in-memory
119
+ // stream is torn down. On a clean run the stream has already ended — no-op.
120
+ promptStream.destroy();
72
121
  };
73
- // Feed the prompt to the child via stdin (claude 2.x has no --prompt-file).
74
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- our own derived prompt path
75
- const promptStream = createReadStream(opts.promptFile);
76
122
  promptStream.on('error', err => {
77
123
  clearAllTimers();
78
- child.kill('SIGKILL');
124
+ killProcessTree(child);
79
125
  reject(err);
80
126
  });
81
127
  // Swallow EPIPE: if the child exits before consuming all stdin, the write
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-claude.js","sourceRoot":"","sources":["../../src/skill-test/spawn-claude.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"spawn-claude.js","sourceRoot":"","sources":["../../src/skill-test/spawn-claude.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,KAAK,MAAM,OAAO,CAAC;AAU1B;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAqB;IACtD,MAAM,IAAI,GAAa;QACrB,IAAI;QACJ,0EAA0E;QAC1E,sEAAsE;QACtE,4DAA4D;QAC5D,iBAAiB,EAAE,aAAa;QAChC,WAAW;QACX,mBAAmB,EAAE,EAAE;QACvB,mBAAmB,EAAE,mBAAmB;QACxC,WAAW,EAAE,IAAI,CAAC,UAAU;KAC7B,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/D,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjF,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAC9F,OAAO,IAAI,CAAC;AACd,CAAC;AAQD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,KAAmC;IACjE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;IACtB,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO;IAC9B,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,iNAAiN;YACjN,SAAS,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAC3D,CAAC;aAAM,CAAC;YACN,+DAA+D;YAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;IAC7E,CAAC;AACH,CAAC;AA8BD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAA0B;IAClE,+EAA+E;IAC/E,2DAA2D;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAEtC,OAAO,MAAM,IAAI,OAAO,CAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE;YAC7B,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC/B,0EAA0E;YAC1E,sEAAsE;YACtE,2EAA2E;YAC3E,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;SACvC,CAAC,CAAC;QACH,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,4EAA4E;QAC5E,8EAA8E;QAC9E,8EAA8E;QAC9E,6EAA6E;QAC7E,4CAA4C;QAC5C,MAAM,YAAY,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QAEzE,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YAChC,QAAQ,GAAG,IAAI,CAAC;YAChB,eAAe,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAEnB,kFAAkF;QAClF,IAAI,UAAqD,CAAC;QAC1D,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;YAC7B,MAAM,eAAe,GAAG,GAAS,EAAE;gBACjC,IAAI,UAAU,KAAK,SAAS;oBAAE,YAAY,CAAC,UAAU,CAAC,CAAC;gBACvD,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE;oBAC3B,OAAO,GAAG,IAAI,CAAC;oBACf,eAAe,CAAC,KAAK,CAAC,CAAC;gBACzB,CAAC,EAAE,OAAO,CAAC,CAAC;YACd,CAAC,CAAC;YACF,eAAe,EAAE,CAAC;YAElB,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,eAAe,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACtD,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,eAAe,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC;QAED,MAAM,cAAc,GAAG,GAAS,EAAE;YAChC,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,IAAI,UAAU,KAAK,SAAS;gBAAE,YAAY,CAAC,UAAU,CAAC,CAAC;YACvD,wEAAwE;YACxE,6EAA6E;YAC7E,4EAA4E;YAC5E,YAAY,CAAC,OAAO,EAAE,CAAC;QACzB,CAAC,CAAC;QAEF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE;YAC7B,cAAc,EAAE,CAAC;YACjB,eAAe,CAAC,KAAK,CAAC,CAAC;YACvB,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CAAC,CAAC;QACH,0EAA0E;QAC1E,4EAA4E;QAC5E,6BAA6B;QAC7B,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAA0C,CAAC,CAAC,CAAC;QAC1E,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/B,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3E,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3E,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,GAAG,cAAc,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvG,CAAC,CAAC,CAAC;AACL,CAAC"}