llm-cli-gateway 2.14.1 → 2.16.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +35 -0
  3. package/dist/acp/process-manager.js +2 -1
  4. package/dist/acp/provider-registry.js +8 -8
  5. package/dist/async-job-manager.d.ts +4 -1
  6. package/dist/async-job-manager.js +15 -6
  7. package/dist/compressor/estimate.d.ts +5 -0
  8. package/dist/compressor/estimate.js +21 -0
  9. package/dist/compressor/index.d.ts +23 -0
  10. package/dist/compressor/index.js +77 -0
  11. package/dist/compressor/router.d.ts +2 -0
  12. package/dist/compressor/router.js +57 -0
  13. package/dist/compressor/transforms/ansi.d.ts +3 -0
  14. package/dist/compressor/transforms/ansi.js +89 -0
  15. package/dist/compressor/transforms/json.d.ts +1 -0
  16. package/dist/compressor/transforms/json.js +156 -0
  17. package/dist/compressor/transforms/log.d.ts +12 -0
  18. package/dist/compressor/transforms/log.js +55 -0
  19. package/dist/compressor/transforms/whitespace.d.ts +8 -0
  20. package/dist/compressor/transforms/whitespace.js +79 -0
  21. package/dist/config.d.ts +16 -0
  22. package/dist/config.js +57 -0
  23. package/dist/executor.d.ts +1 -0
  24. package/dist/executor.js +5 -2
  25. package/dist/flight-recorder.d.ts +9 -0
  26. package/dist/flight-recorder.js +42 -0
  27. package/dist/index.d.ts +18 -2
  28. package/dist/index.js +282 -88
  29. package/dist/job-store.d.ts +4 -0
  30. package/dist/job-store.js +16 -0
  31. package/dist/provider-definitions.d.ts +1 -0
  32. package/dist/provider-definitions.js +28 -10
  33. package/dist/provider-tool-capabilities.js +3 -3
  34. package/dist/request-helpers.js +1 -1
  35. package/dist/skill-loader.d.ts +24 -0
  36. package/dist/skill-loader.js +201 -0
  37. package/dist/spawn-env-isolation.d.ts +10 -0
  38. package/dist/spawn-env-isolation.js +55 -0
  39. package/dist/upstream-contracts.js +47 -27
  40. package/npm-shrinkwrap.json +2 -2
  41. package/package.json +7 -3
@@ -10,6 +10,7 @@ export interface JobRecord {
10
10
  cli: string;
11
11
  argsJson: string;
12
12
  outputFormat?: string | null;
13
+ compressResponse?: boolean | null;
13
14
  status: JobStoreStatus;
14
15
  exitCode: number | null;
15
16
  stdout: string;
@@ -52,6 +53,7 @@ export interface JobStore {
52
53
  cli: string;
53
54
  args: string[];
54
55
  outputFormat?: string;
56
+ compressResponse?: boolean;
55
57
  startedAt: string;
56
58
  pid: number | null;
57
59
  ownerPrincipal?: string | null;
@@ -173,6 +175,7 @@ export declare class SqliteJobStore implements JobStore, ValidationRunStore {
173
175
  cli: string;
174
176
  args: string[];
175
177
  outputFormat?: string;
178
+ compressResponse?: boolean;
176
179
  startedAt: string;
177
180
  pid: number | null;
178
181
  ownerPrincipal?: string | null;
@@ -236,6 +239,7 @@ export declare class MemoryJobStore implements JobStore {
236
239
  cli: string;
237
240
  args: string[];
238
241
  outputFormat?: string;
242
+ compressResponse?: boolean;
239
243
  startedAt: string;
240
244
  pid: number | null;
241
245
  ownerPrincipal?: string | null;
package/dist/job-store.js CHANGED
@@ -51,6 +51,9 @@ function rowToRecord(row) {
51
51
  cli: row.cli,
52
52
  argsJson: row.args_json,
53
53
  outputFormat: row.output_format ?? null,
54
+ compressResponse: row.compress_response === null || row.compress_response === undefined
55
+ ? null
56
+ : Boolean(row.compress_response),
54
57
  status: row.status,
55
58
  exitCode: row.exit_code,
56
59
  stdout: row.stdout ?? "",
@@ -99,6 +102,13 @@ function ensureJobsLeaseColumns(db) {
99
102
  db.exec("ALTER TABLE jobs ADD COLUMN lease_deadline INTEGER");
100
103
  }
101
104
  }
105
+ function ensureJobsCompressResponseColumn(db) {
106
+ const cols = db.prepare("PRAGMA table_info(jobs)").all();
107
+ const names = new Set(cols.map(col => col?.name));
108
+ if (!names.has("compress_response")) {
109
+ db.exec("ALTER TABLE jobs ADD COLUMN compress_response INTEGER");
110
+ }
111
+ }
102
112
  export function isValidationRunStore(store) {
103
113
  return (typeof store === "object" &&
104
114
  store !== null &&
@@ -143,6 +153,7 @@ export class SqliteJobStore {
143
153
  cli TEXT NOT NULL,
144
154
  args_json TEXT NOT NULL,
145
155
  output_format TEXT,
156
+ compress_response INTEGER,
146
157
  status TEXT NOT NULL,
147
158
  exit_code INTEGER,
148
159
  stdout TEXT,
@@ -216,6 +227,7 @@ export class SqliteJobStore {
216
227
  ensureJobsTransportColumns(this.db);
217
228
  ensureJobsLeaseColumns(this.db);
218
229
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_jobs_owner_status ON jobs(owner_instance, status)");
230
+ ensureJobsCompressResponseColumn(this.db);
219
231
  if (process.platform !== "win32") {
220
232
  try {
221
233
  chmodSync(dbPath, 0o600);
@@ -228,10 +240,12 @@ export class SqliteJobStore {
228
240
  this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_INSTANCE_LEASE_TTL_MS;
229
241
  this.insertStmt = this.db.prepare(`
230
242
  INSERT INTO jobs (id, correlation_id, request_key, cli, args_json, output_format,
243
+ compress_response,
231
244
  status, exit_code, stdout, stderr, output_truncated, error,
232
245
  started_at, finished_at, pid, expires_at, owner_principal,
233
246
  transport, http_status, payload_json, owner_instance, lease_deadline)
234
247
  VALUES (@id, @correlation_id, @request_key, @cli, @args_json, @output_format,
248
+ @compress_response,
235
249
  'queued', @exit_code, @stdout, @stderr, @output_truncated, @error,
236
250
  @started_at, @finished_at, @pid, @expires_at, @owner_principal,
237
251
  @transport, @http_status, @payload_json, @owner_instance,
@@ -332,6 +346,7 @@ export class SqliteJobStore {
332
346
  cli: input.cli,
333
347
  args_json: JSON.stringify(input.args),
334
348
  output_format: input.outputFormat ?? null,
349
+ compress_response: input.compressResponse === undefined ? null : input.compressResponse ? 1 : 0,
335
350
  exit_code: null,
336
351
  stdout: "",
337
352
  stderr: "",
@@ -624,6 +639,7 @@ export class MemoryJobStore {
624
639
  cli: input.cli,
625
640
  argsJson: JSON.stringify(input.args),
626
641
  outputFormat: input.outputFormat ?? null,
642
+ compressResponse: input.compressResponse ?? null,
627
643
  status: "queued",
628
644
  exitCode: null,
629
645
  stdout: "",
@@ -119,6 +119,7 @@ export interface ProviderDefinition {
119
119
  readonly upstreamContract: ProviderUpstreamLinkage;
120
120
  readonly capabilityScope: CapabilityScope;
121
121
  }
122
+ export declare const PROVIDER_TARGET_VERSIONS: Record<CliType, string>;
122
123
  export declare const PROVIDER_DEFINITIONS_BY_ID: Readonly<Record<CliType, ProviderDefinition>>;
123
124
  export declare function getAllProviderDefinitions(): readonly ProviderDefinition[];
124
125
  export declare function getProviderDefinition(id: CliType): ProviderDefinition;
@@ -4,6 +4,15 @@ export function adminSurfaceKind(family) {
4
4
  return family.kind ?? "cli-subcommand";
5
5
  }
6
6
  export const DEVIN_ACP_AGENT_TYPES = ["summarizer", "review"];
7
+ export const PROVIDER_TARGET_VERSIONS = {
8
+ claude: "claude 2.1.206",
9
+ codex: "codex-cli 0.144.1",
10
+ gemini: "agy 1.1.0",
11
+ grok: "grok 0.2.93 (f00f96316d)",
12
+ mistral: "vibe 2.19.1",
13
+ devin: "devin 3000.1.27 (0d4bf12e)",
14
+ cursor: "cursor-agent 2026.07.09-c59fd9a",
15
+ };
7
16
  const PROVIDER_DEFINITIONS = {
8
17
  claude: {
9
18
  id: "claude",
@@ -86,7 +95,7 @@ const PROVIDER_DEFINITIONS = {
86
95
  nativeEntrypoint: null,
87
96
  entrypoint: null,
88
97
  probeArgv: [],
89
- evidence: "No native Claude Code ACP subcommand or flag in installed help (claude 2.1.198) or the official CLI reference. Coverage is CLI-first; ACP reporting says no native entrypoint is advertised.",
98
+ evidence: `No native Claude Code ACP subcommand or flag in installed help (${PROVIDER_TARGET_VERSIONS.claude}) or the official CLI reference. Coverage is CLI-first; ACP reporting says no native entrypoint is advertised.`,
90
99
  },
91
100
  safetyModes: {
92
101
  sandbox: false,
@@ -98,7 +107,10 @@ const PROVIDER_DEFINITIONS = {
98
107
  outputFormats: ["text", "json", "stream-json"],
99
108
  streamingFormats: ["stream-json"],
100
109
  resourcePolicy: { exposesModelsResource: true, exposesSessionsResource: true },
101
- upstreamContract: { targetVersion: "Claude Code 2.1.198", helpChecksumRef: "claude--help.txt" },
110
+ upstreamContract: {
111
+ targetVersion: PROVIDER_TARGET_VERSIONS.claude,
112
+ helpChecksumRef: "claude--help.txt",
113
+ },
102
114
  capabilityScope: "full",
103
115
  },
104
116
  codex: {
@@ -215,7 +227,7 @@ const PROVIDER_DEFINITIONS = {
215
227
  nativeEntrypoint: null,
216
228
  entrypoint: null,
217
229
  probeArgv: [],
218
- evidence: "codex-cli 0.142.4 advertises mcp-server and app-server transports, not a native ACP agent entrypoint. Third-party adapters exist but are documentation only and are never treated as native gateway ACP.",
230
+ evidence: `${PROVIDER_TARGET_VERSIONS.codex} advertises mcp-server and app-server transports, not a native ACP agent entrypoint. Third-party adapters exist but are documentation only and are never treated as native gateway ACP.`,
219
231
  },
220
232
  safetyModes: {
221
233
  sandbox: true,
@@ -233,7 +245,7 @@ const PROVIDER_DEFINITIONS = {
233
245
  streamingFormats: ["jsonl"],
234
246
  resourcePolicy: { exposesModelsResource: true, exposesSessionsResource: true },
235
247
  upstreamContract: {
236
- targetVersion: "codex-cli 0.142.4",
248
+ targetVersion: PROVIDER_TARGET_VERSIONS.codex,
237
249
  helpChecksumRef: "codex-exec--help.txt",
238
250
  },
239
251
  capabilityScope: "full",
@@ -319,7 +331,7 @@ const PROVIDER_DEFINITIONS = {
319
331
  nativeEntrypoint: null,
320
332
  entrypoint: null,
321
333
  probeArgv: [],
322
- evidence: "agy 1.0.14 has no ACP flag or subcommand in installed help or the Antigravity CLI docs. Legacy Gemini CLI ACP evidence does not transfer. No native entrypoint advertised.",
334
+ evidence: `${PROVIDER_TARGET_VERSIONS.gemini} has no ACP flag or subcommand in installed help or the Antigravity CLI docs. Legacy Gemini CLI ACP evidence does not transfer. No native entrypoint advertised.`,
323
335
  },
324
336
  safetyModes: {
325
337
  sandbox: true,
@@ -331,7 +343,10 @@ const PROVIDER_DEFINITIONS = {
331
343
  outputFormats: ["text"],
332
344
  streamingFormats: [],
333
345
  resourcePolicy: { exposesModelsResource: true, exposesSessionsResource: true },
334
- upstreamContract: { targetVersion: "agy 1.0.14", helpChecksumRef: "agy--help.txt" },
346
+ upstreamContract: {
347
+ targetVersion: PROVIDER_TARGET_VERSIONS.gemini,
348
+ helpChecksumRef: "agy--help.txt",
349
+ },
335
350
  capabilityScope: "full",
336
351
  },
337
352
  grok: {
@@ -436,7 +451,7 @@ const PROVIDER_DEFINITIONS = {
436
451
  streamingFormats: ["json"],
437
452
  resourcePolicy: { exposesModelsResource: true, exposesSessionsResource: true },
438
453
  upstreamContract: {
439
- targetVersion: "grok 0.2.77 (44e77bec3a)",
454
+ targetVersion: PROVIDER_TARGET_VERSIONS.grok,
440
455
  helpChecksumRef: "grok--help.txt",
441
456
  },
442
457
  capabilityScope: "full",
@@ -551,7 +566,10 @@ const PROVIDER_DEFINITIONS = {
551
566
  outputFormats: ["text", "json"],
552
567
  streamingFormats: [],
553
568
  resourcePolicy: { exposesModelsResource: true, exposesSessionsResource: true },
554
- upstreamContract: { targetVersion: "vibe 2.18.3", helpChecksumRef: "vibe--help.txt" },
569
+ upstreamContract: {
570
+ targetVersion: PROVIDER_TARGET_VERSIONS.mistral,
571
+ helpChecksumRef: "vibe--help.txt",
572
+ },
555
573
  capabilityScope: "full",
556
574
  },
557
575
  devin: {
@@ -660,7 +678,7 @@ const PROVIDER_DEFINITIONS = {
660
678
  streamingFormats: [],
661
679
  resourcePolicy: { exposesModelsResource: true, exposesSessionsResource: true },
662
680
  upstreamContract: {
663
- targetVersion: "devin 2026.8.18 (16737566)",
681
+ targetVersion: PROVIDER_TARGET_VERSIONS.devin,
664
682
  helpChecksumRef: "devin--help.txt",
665
683
  },
666
684
  capabilityScope: "full",
@@ -734,7 +752,7 @@ const PROVIDER_DEFINITIONS = {
734
752
  streamingFormats: ["stream-json"],
735
753
  resourcePolicy: { exposesModelsResource: true, exposesSessionsResource: true },
736
754
  upstreamContract: {
737
- targetVersion: "cursor-agent 2026.06.29-2ad2186",
755
+ targetVersion: PROVIDER_TARGET_VERSIONS.cursor,
738
756
  helpChecksumRef: "cursor-agent--help.txt",
739
757
  },
740
758
  capabilityScope: "maintain-only",
@@ -4,7 +4,7 @@ import path from "path";
4
4
  import { parse as parseToml } from "smol-toml";
5
5
  import { CLAUDE_MCP_SERVER_NAMES } from "./claude-mcp-config.js";
6
6
  import { getAvailableCliInfo } from "./model-registry.js";
7
- import { CLI_TYPES, getProviderDefinition } from "./provider-definitions.js";
7
+ import { CLI_TYPES, getProviderDefinition, PROVIDER_TARGET_VERSIONS, } from "./provider-definitions.js";
8
8
  import { enabledApiProviders, isXaiProviderEnabled, loadProvidersConfig, } from "./config.js";
9
9
  import { apiContinuityForKind } from "./api-provider.js";
10
10
  const MAX_SKILLS_PER_DIR = 100;
@@ -50,7 +50,7 @@ export const ACP_CONTRACT = {
50
50
  },
51
51
  gemini: {
52
52
  classification: "absent_watchlist",
53
- summary: "Google Antigravity agy 1.0.14 has no ACP surface; watchlist item only.",
53
+ summary: `Google Antigravity ${PROVIDER_TARGET_VERSIONS.gemini} has no ACP surface; watchlist item only.`,
54
54
  },
55
55
  grok_api: {
56
56
  classification: "absent_watchlist",
@@ -110,7 +110,7 @@ const ACP_RESIDUAL = {
110
110
  smokeSupported: false,
111
111
  smokeStatus: "unsupported",
112
112
  caveats: [
113
- "Antigravity agy 1.0.14 has no ACP flag or subcommand.",
113
+ `Antigravity ${PROVIDER_TARGET_VERSIONS.gemini} has no ACP flag or subcommand.`,
114
114
  "Legacy Gemini CLI ACP evidence does not transfer to agy; kept on the upstream drift watchlist.",
115
115
  ],
116
116
  },
@@ -99,7 +99,7 @@ export const MISTRAL_BUILTIN_AGENT_MODES = [
99
99
  "accept-edits",
100
100
  "auto-approve",
101
101
  ];
102
- export const MISTRAL_DEFAULT_AGENT_MODE = "auto-approve";
102
+ export const MISTRAL_DEFAULT_AGENT_MODE = "accept-edits";
103
103
  export function prepareMistralRequest(input) {
104
104
  const args = ["-p", input.prompt];
105
105
  const env = {};
@@ -0,0 +1,24 @@
1
+ import type { Logger } from "./logger.js";
2
+ export interface SkillEntry {
3
+ name: string;
4
+ content: string;
5
+ description: string;
6
+ source: "bundled" | "external";
7
+ path: string;
8
+ pack: {
9
+ name: string;
10
+ version: string;
11
+ manifestPath: string;
12
+ verified: boolean;
13
+ } | null;
14
+ }
15
+ export interface LoadGatewaySkillsOptions {
16
+ bundledSkillsDir: string;
17
+ configuredPaths?: string[];
18
+ envSkillsPath?: string | undefined;
19
+ userSkillsDir?: string;
20
+ logger?: Logger;
21
+ }
22
+ export declare function defaultUserSkillsDir(): string;
23
+ export declare function parseSkillPathList(raw: string | undefined): string[];
24
+ export declare function loadGatewaySkills(options: LoadGatewaySkillsOptions): SkillEntry[];
@@ -0,0 +1,201 @@
1
+ import { createHash } from "crypto";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
3
+ import { homedir } from "os";
4
+ import path from "path";
5
+ import { z } from "zod/v3";
6
+ import { logWarn, noopLogger } from "./logger.js";
7
+ const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
8
+ const SkillPackManifestSchema = z
9
+ .object({
10
+ name: z.string().regex(SKILL_NAME_PATTERN),
11
+ version: z.string().min(1),
12
+ skills: z
13
+ .array(z
14
+ .object({
15
+ name: z.string().regex(SKILL_NAME_PATTERN),
16
+ sha256: z.string().regex(/^[a-f0-9]{64}$/i),
17
+ })
18
+ .strict())
19
+ .default([]),
20
+ })
21
+ .strict();
22
+ export function defaultUserSkillsDir() {
23
+ return path.join(homedir(), ".llm-cli-gateway", "skills");
24
+ }
25
+ export function parseSkillPathList(raw) {
26
+ if (!raw)
27
+ return [];
28
+ return raw
29
+ .split(path.delimiter)
30
+ .map(part => part.trim())
31
+ .filter(Boolean);
32
+ }
33
+ export function loadGatewaySkills(options) {
34
+ const logger = options.logger ?? noopLogger;
35
+ const roots = [
36
+ { path: options.bundledSkillsDir, source: "bundled", label: "bundled" },
37
+ ...normalizeExternalRoots(options.configuredPaths ?? [], "configured"),
38
+ ...normalizeExternalRoots(parseSkillPathList(options.envSkillsPath), "LLM_GATEWAY_SKILLS_PATH"),
39
+ ];
40
+ const userSkillsDir = options.userSkillsDir ?? defaultUserSkillsDir();
41
+ if (existsSync(userSkillsDir)) {
42
+ roots.push({ path: userSkillsDir, source: "external", label: "user" });
43
+ }
44
+ const byName = new Map();
45
+ for (const root of roots) {
46
+ for (const skill of readSkillRoot(root, logger)) {
47
+ const prior = byName.get(skill.name);
48
+ if (prior) {
49
+ logWarn(logger, `Skill '${skill.name}' from ${skill.path} overrides ${prior.source} skill at ${prior.path}`);
50
+ }
51
+ byName.set(skill.name, skill);
52
+ }
53
+ }
54
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
55
+ }
56
+ function normalizeExternalRoots(paths, label) {
57
+ return paths.map(p => ({
58
+ path: expandHome(p),
59
+ source: "external",
60
+ label,
61
+ }));
62
+ }
63
+ function expandHome(p) {
64
+ return p === "~" ? homedir() : p.startsWith("~/") ? path.join(homedir(), p.slice(2)) : p;
65
+ }
66
+ function readSkillRoot(root, logger) {
67
+ if (!existsSync(root.path)) {
68
+ if (root.source === "external") {
69
+ logWarn(logger, `Configured skill path does not exist; skipping: ${root.path}`);
70
+ }
71
+ return [];
72
+ }
73
+ let stat;
74
+ try {
75
+ stat = statSync(root.path);
76
+ }
77
+ catch (err) {
78
+ logWarn(logger, `Cannot stat skill path; skipping: ${root.path}`, err);
79
+ return [];
80
+ }
81
+ if (!stat.isDirectory()) {
82
+ logWarn(logger, `Skill path is not a directory; skipping: ${root.path}`);
83
+ return [];
84
+ }
85
+ const manifest = readManifest(root, logger);
86
+ if (manifest === "invalid")
87
+ return [];
88
+ if (existsSync(path.join(root.path, "SKILL.md"))) {
89
+ const skill = readSkillDir(root.path, root, manifest, logger);
90
+ return skill ? [skill] : [];
91
+ }
92
+ let entries;
93
+ try {
94
+ entries = readdirSync(root.path, { withFileTypes: true });
95
+ }
96
+ catch (err) {
97
+ logWarn(logger, `Cannot read skill path; skipping: ${root.path}`, err);
98
+ return [];
99
+ }
100
+ const skills = [];
101
+ for (const entry of entries) {
102
+ if (!entry.isDirectory())
103
+ continue;
104
+ const skill = readSkillDir(path.join(root.path, entry.name), root, manifest, logger);
105
+ if (skill)
106
+ skills.push(skill);
107
+ }
108
+ if (manifest && manifest.skills.length > 0) {
109
+ const loaded = new Set(skills.map(skill => skill.name));
110
+ for (const expected of manifest.skills) {
111
+ if (!loaded.has(expected.name)) {
112
+ logWarn(logger, `Skill pack '${manifest.name}' declares '${expected.name}', but no verified SKILL.md was loaded`);
113
+ }
114
+ }
115
+ }
116
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
117
+ }
118
+ function readManifest(root, logger) {
119
+ const manifestPath = path.join(root.path, "skill-pack.json");
120
+ if (!existsSync(manifestPath))
121
+ return null;
122
+ try {
123
+ const raw = JSON.parse(readFileSync(manifestPath, "utf8"));
124
+ return SkillPackManifestSchema.parse(raw);
125
+ }
126
+ catch (err) {
127
+ logWarn(logger, `Invalid skill-pack.json at ${manifestPath}; skipping skill pack`, err);
128
+ return "invalid";
129
+ }
130
+ }
131
+ function readSkillDir(skillDir, root, manifest, logger) {
132
+ const skillPath = path.join(skillDir, "SKILL.md");
133
+ if (!existsSync(skillPath))
134
+ return null;
135
+ let content;
136
+ try {
137
+ content = readFileSync(skillPath, "utf8");
138
+ }
139
+ catch (err) {
140
+ logWarn(logger, `Cannot read skill file; skipping: ${skillPath}`, err);
141
+ return null;
142
+ }
143
+ const frontmatter = parseSkillFrontmatter(content);
144
+ const dirName = path.basename(skillDir);
145
+ const name = frontmatter.name || dirName;
146
+ if (!SKILL_NAME_PATTERN.test(name)) {
147
+ logWarn(logger, `Invalid skill name '${name}' in ${skillPath}; skipping`);
148
+ return null;
149
+ }
150
+ if (frontmatter.name && frontmatter.name !== dirName) {
151
+ logWarn(logger, `Skill frontmatter name '${frontmatter.name}' does not match directory '${dirName}'; skipping ${skillPath}`);
152
+ return null;
153
+ }
154
+ const packInfo = verifyManifestEntry(name, content, skillPath, root, manifest, logger);
155
+ if (packInfo === "skip")
156
+ return null;
157
+ return {
158
+ name,
159
+ content,
160
+ description: frontmatter.description || name,
161
+ source: root.source,
162
+ path: skillPath,
163
+ pack: packInfo,
164
+ };
165
+ }
166
+ function verifyManifestEntry(name, content, skillPath, root, manifest, logger) {
167
+ if (!manifest)
168
+ return null;
169
+ const expected = manifest.skills.find(skill => skill.name === name);
170
+ if (!expected) {
171
+ logWarn(logger, `Skill pack '${manifest.name}' does not list '${name}' in skill-pack.json; skipping ${skillPath}`);
172
+ return "skip";
173
+ }
174
+ const actual = createHash("sha256").update(content, "utf8").digest("hex");
175
+ if (actual.toLowerCase() !== expected.sha256.toLowerCase()) {
176
+ logWarn(logger, `Skill pack '${manifest.name}' hash mismatch for '${name}'; skipping ${skillPath}`);
177
+ return "skip";
178
+ }
179
+ return {
180
+ name: manifest.name,
181
+ version: manifest.version,
182
+ manifestPath: path.join(root.path, "skill-pack.json"),
183
+ verified: true,
184
+ };
185
+ }
186
+ function parseSkillFrontmatter(content) {
187
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
188
+ if (!match)
189
+ return { name: null, description: null };
190
+ return {
191
+ name: scalarFrontmatterValue(match[1], "name"),
192
+ description: scalarFrontmatterValue(match[1], "description"),
193
+ };
194
+ }
195
+ function scalarFrontmatterValue(frontmatter, key) {
196
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
197
+ const match = frontmatter.match(new RegExp(`^${escapedKey}:\\s*(.+?)\\s*$`, "m"));
198
+ if (!match)
199
+ return null;
200
+ return match[1].replace(/^["']|["']$/g, "").trim();
201
+ }
@@ -0,0 +1,10 @@
1
+ import type { Logger } from "./logger.js";
2
+ export declare function isRedirectionEnvKey(key: string): boolean;
3
+ export declare function isSpawnEnvIsolationEnabled(env?: NodeJS.ProcessEnv): boolean;
4
+ export interface SpawnEnvSanitizeResult {
5
+ env: NodeJS.ProcessEnv;
6
+ stripped: string[];
7
+ }
8
+ export declare function sanitizeSpawnEnv(baseEnv: NodeJS.ProcessEnv): SpawnEnvSanitizeResult;
9
+ export declare function resetSpawnEnvIsolationWarning(): void;
10
+ export declare function applySpawnEnvIsolation(finalEnv: NodeJS.ProcessEnv, logger?: Logger, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
@@ -0,0 +1,55 @@
1
+ import { logWarn } from "./logger.js";
2
+ const REDIRECTION_SUFFIXES = [
3
+ /_BASE_URL$/,
4
+ /_API_URL$/,
5
+ /_API_BASE$/,
6
+ /_ENDPOINT$/,
7
+ /_ENDPOINT_URL$/,
8
+ /_SERVER_URL$/,
9
+ ];
10
+ const REDIRECTION_EXACT = new Set([
11
+ "ANTHROPIC_UNIX_SOCKET",
12
+ "GROK_LEADER_SOCKET",
13
+ ]);
14
+ export function isRedirectionEnvKey(key) {
15
+ const upper = key.toUpperCase();
16
+ if (upper === "NO_PROXY")
17
+ return false;
18
+ if (upper.endsWith("_PROXY"))
19
+ return true;
20
+ if (REDIRECTION_EXACT.has(upper))
21
+ return true;
22
+ return REDIRECTION_SUFFIXES.some(re => re.test(upper));
23
+ }
24
+ export function isSpawnEnvIsolationEnabled(env = process.env) {
25
+ const raw = (env.LLM_GATEWAY_ISOLATE_SPAWN_ENV ?? "").trim().toLowerCase();
26
+ return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
27
+ }
28
+ export function sanitizeSpawnEnv(baseEnv) {
29
+ const env = {};
30
+ const stripped = [];
31
+ for (const key of Object.keys(baseEnv)) {
32
+ if (isRedirectionEnvKey(key)) {
33
+ stripped.push(key);
34
+ continue;
35
+ }
36
+ env[key] = baseEnv[key];
37
+ }
38
+ return { env, stripped };
39
+ }
40
+ let warnedOnce = false;
41
+ export function resetSpawnEnvIsolationWarning() {
42
+ warnedOnce = false;
43
+ }
44
+ export function applySpawnEnvIsolation(finalEnv, logger, env = process.env) {
45
+ if (!isSpawnEnvIsolationEnabled(env))
46
+ return finalEnv;
47
+ const { env: sanitized, stripped } = sanitizeSpawnEnv(finalEnv);
48
+ if (stripped.length > 0 && logger && !warnedOnce) {
49
+ warnedOnce = true;
50
+ logWarn(logger, `spawn-env isolation: withheld ${stripped.length} endpoint/proxy redirection variable(s) ` +
51
+ `from provider child processes (${stripped.join(", ")}). ` +
52
+ `Unset LLM_GATEWAY_ISOLATE_SPAWN_ENV to disable.`);
53
+ }
54
+ return sanitized;
55
+ }