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.
- package/CHANGELOG.md +61 -1
- package/README.md +35 -0
- package/dist/acp/process-manager.js +2 -1
- package/dist/acp/provider-registry.js +8 -8
- package/dist/async-job-manager.d.ts +4 -1
- package/dist/async-job-manager.js +15 -6
- package/dist/compressor/estimate.d.ts +5 -0
- package/dist/compressor/estimate.js +21 -0
- package/dist/compressor/index.d.ts +23 -0
- package/dist/compressor/index.js +77 -0
- package/dist/compressor/router.d.ts +2 -0
- package/dist/compressor/router.js +57 -0
- package/dist/compressor/transforms/ansi.d.ts +3 -0
- package/dist/compressor/transforms/ansi.js +89 -0
- package/dist/compressor/transforms/json.d.ts +1 -0
- package/dist/compressor/transforms/json.js +156 -0
- package/dist/compressor/transforms/log.d.ts +12 -0
- package/dist/compressor/transforms/log.js +55 -0
- package/dist/compressor/transforms/whitespace.d.ts +8 -0
- package/dist/compressor/transforms/whitespace.js +79 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +57 -0
- package/dist/executor.d.ts +1 -0
- package/dist/executor.js +5 -2
- package/dist/flight-recorder.d.ts +9 -0
- package/dist/flight-recorder.js +42 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +282 -88
- package/dist/job-store.d.ts +4 -0
- package/dist/job-store.js +16 -0
- package/dist/provider-definitions.d.ts +1 -0
- package/dist/provider-definitions.js +28 -10
- package/dist/provider-tool-capabilities.js +3 -3
- package/dist/request-helpers.js +1 -1
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +201 -0
- package/dist/spawn-env-isolation.d.ts +10 -0
- package/dist/spawn-env-isolation.js +55 -0
- package/dist/upstream-contracts.js +47 -27
- package/npm-shrinkwrap.json +2 -2
- package/package.json +7 -3
package/dist/job-store.d.ts
CHANGED
|
@@ -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:
|
|
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: {
|
|
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:
|
|
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:
|
|
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:
|
|
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: {
|
|
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:
|
|
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: {
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
},
|
package/dist/request-helpers.js
CHANGED
|
@@ -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 = "
|
|
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
|
+
}
|