llm-cli-gateway 2.8.0 → 2.10.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 +85 -0
- package/README.md +23 -0
- package/dist/approval-manager.d.ts +1 -0
- package/dist/approval-manager.js +14 -1
- package/dist/async-job-manager.d.ts +1 -0
- package/dist/async-job-manager.js +11 -0
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +16 -0
- package/dist/cache-stats.d.ts +1 -0
- package/dist/cache-stats.js +2 -1
- package/dist/cli-updater.js +5 -2
- package/dist/config.js +22 -1
- package/dist/flight-recorder.d.ts +7 -1
- package/dist/flight-recorder.js +33 -6
- package/dist/http-transport.js +19 -17
- package/dist/index.js +117 -29
- package/dist/job-store.d.ts +4 -0
- package/dist/job-store.js +16 -4
- package/dist/oauth.d.ts +2 -0
- package/dist/oauth.js +90 -8
- package/dist/request-context.d.ts +3 -0
- package/dist/request-context.js +16 -0
- package/dist/request-limits.d.ts +8 -0
- package/dist/request-limits.js +49 -0
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +24 -15
- package/dist/secret-redaction.d.ts +3 -0
- package/dist/secret-redaction.js +53 -0
- package/dist/session-manager-pg.js +8 -5
- package/dist/session-manager.d.ts +1 -0
- package/dist/session-manager.js +2 -0
- package/migrations/004_session_owner_principal.sql +10 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/resources.d.ts
CHANGED
|
@@ -33,5 +33,7 @@ export declare class ResourceProvider {
|
|
|
33
33
|
readCacheStateSession(sessionId: string): SessionCacheStats;
|
|
34
34
|
readCacheStateForPrefix(stablePrefixHash: string): PrefixCacheStats;
|
|
35
35
|
listResources(): ResourceDefinition[];
|
|
36
|
+
private ownedSessions;
|
|
37
|
+
private ownedActiveId;
|
|
36
38
|
readResource(uri: string): Promise<ResourceContents | null>;
|
|
37
39
|
}
|
package/dist/resources.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CLI_TYPES, PROVIDER_TYPES } from "./session-manager.js";
|
|
2
|
+
import { getRequestContext, principalCanAccess, resolveOwnerPrincipal } from "./request-context.js";
|
|
2
3
|
import { getAvailableCliInfo } from "./model-registry.js";
|
|
3
4
|
import { computeGlobalCacheStats, computePrefixCacheStats, computeSessionCacheStats, computeTtlRemaining, } from "./cache-stats.js";
|
|
4
5
|
import { buildProviderSubcommandsCompactCatalog, getCliSubcommandContract, serializeCliSubcommandContract, } from "./upstream-contracts.js";
|
|
@@ -201,13 +202,21 @@ export class ResourceProvider {
|
|
|
201
202
|
})),
|
|
202
203
|
];
|
|
203
204
|
}
|
|
205
|
+
ownedSessions(sessions) {
|
|
206
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
207
|
+
return sessions.filter(s => principalCanAccess(s.ownerPrincipal, caller));
|
|
208
|
+
}
|
|
209
|
+
async ownedActiveId(provider) {
|
|
210
|
+
const active = await Promise.resolve(this.sessionManager.getActiveSession(provider));
|
|
211
|
+
if (!active)
|
|
212
|
+
return null;
|
|
213
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
214
|
+
return principalCanAccess(active.ownerPrincipal, caller) ? active.id : null;
|
|
215
|
+
}
|
|
204
216
|
async readResource(uri) {
|
|
205
217
|
if (uri === "sessions://all") {
|
|
206
|
-
const sessions = await this.sessionManager.listSessions();
|
|
207
|
-
const activeSessions = Object.fromEntries(await Promise.all(PROVIDER_TYPES.map(async (provider) => [
|
|
208
|
-
provider,
|
|
209
|
-
(await this.sessionManager.getActiveSession(provider))?.id || null,
|
|
210
|
-
])));
|
|
218
|
+
const sessions = this.ownedSessions(await this.sessionManager.listSessions());
|
|
219
|
+
const activeSessions = Object.fromEntries(await Promise.all(PROVIDER_TYPES.map(async (provider) => [provider, await this.ownedActiveId(provider)])));
|
|
211
220
|
return {
|
|
212
221
|
uri,
|
|
213
222
|
mimeType: "application/json",
|
|
@@ -225,7 +234,7 @@ export class ResourceProvider {
|
|
|
225
234
|
};
|
|
226
235
|
}
|
|
227
236
|
if (uri === "sessions://claude") {
|
|
228
|
-
const sessions = await this.sessionManager.listSessions("claude");
|
|
237
|
+
const sessions = this.ownedSessions(await this.sessionManager.listSessions("claude"));
|
|
229
238
|
return {
|
|
230
239
|
uri,
|
|
231
240
|
mimeType: "application/json",
|
|
@@ -233,12 +242,12 @@ export class ResourceProvider {
|
|
|
233
242
|
cli: "claude",
|
|
234
243
|
total: sessions.length,
|
|
235
244
|
sessions,
|
|
236
|
-
activeSession:
|
|
245
|
+
activeSession: await this.ownedActiveId("claude"),
|
|
237
246
|
}, null, 2),
|
|
238
247
|
};
|
|
239
248
|
}
|
|
240
249
|
if (uri === "sessions://codex") {
|
|
241
|
-
const sessions = await this.sessionManager.listSessions("codex");
|
|
250
|
+
const sessions = this.ownedSessions(await this.sessionManager.listSessions("codex"));
|
|
242
251
|
return {
|
|
243
252
|
uri,
|
|
244
253
|
mimeType: "application/json",
|
|
@@ -246,12 +255,12 @@ export class ResourceProvider {
|
|
|
246
255
|
cli: "codex",
|
|
247
256
|
total: sessions.length,
|
|
248
257
|
sessions,
|
|
249
|
-
activeSession:
|
|
258
|
+
activeSession: await this.ownedActiveId("codex"),
|
|
250
259
|
}, null, 2),
|
|
251
260
|
};
|
|
252
261
|
}
|
|
253
262
|
if (uri === "sessions://gemini") {
|
|
254
|
-
const sessions = await this.sessionManager.listSessions("gemini");
|
|
263
|
+
const sessions = this.ownedSessions(await this.sessionManager.listSessions("gemini"));
|
|
255
264
|
return {
|
|
256
265
|
uri,
|
|
257
266
|
mimeType: "application/json",
|
|
@@ -259,12 +268,12 @@ export class ResourceProvider {
|
|
|
259
268
|
cli: "gemini",
|
|
260
269
|
total: sessions.length,
|
|
261
270
|
sessions,
|
|
262
|
-
activeSession:
|
|
271
|
+
activeSession: await this.ownedActiveId("gemini"),
|
|
263
272
|
}, null, 2),
|
|
264
273
|
};
|
|
265
274
|
}
|
|
266
275
|
if (uri === "sessions://grok") {
|
|
267
|
-
const sessions = await this.sessionManager.listSessions("grok");
|
|
276
|
+
const sessions = this.ownedSessions(await this.sessionManager.listSessions("grok"));
|
|
268
277
|
return {
|
|
269
278
|
uri,
|
|
270
279
|
mimeType: "application/json",
|
|
@@ -272,12 +281,12 @@ export class ResourceProvider {
|
|
|
272
281
|
cli: "grok",
|
|
273
282
|
total: sessions.length,
|
|
274
283
|
sessions,
|
|
275
|
-
activeSession:
|
|
284
|
+
activeSession: await this.ownedActiveId("grok"),
|
|
276
285
|
}, null, 2),
|
|
277
286
|
};
|
|
278
287
|
}
|
|
279
288
|
if (uri === "sessions://mistral") {
|
|
280
|
-
const sessions = await this.sessionManager.listSessions("mistral");
|
|
289
|
+
const sessions = this.ownedSessions(await this.sessionManager.listSessions("mistral"));
|
|
281
290
|
return {
|
|
282
291
|
uri,
|
|
283
292
|
mimeType: "application/json",
|
|
@@ -285,7 +294,7 @@ export class ResourceProvider {
|
|
|
285
294
|
cli: "mistral",
|
|
286
295
|
total: sessions.length,
|
|
287
296
|
sessions,
|
|
288
|
-
activeSession:
|
|
297
|
+
activeSession: await this.ownedActiveId("mistral"),
|
|
289
298
|
}, null, 2),
|
|
290
299
|
};
|
|
291
300
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const REDACTED = "[REDACTED]";
|
|
2
|
+
const RULES = [
|
|
3
|
+
{
|
|
4
|
+
label: "private-key",
|
|
5
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY-----/g,
|
|
6
|
+
},
|
|
7
|
+
{ label: "anthropic-key", pattern: /\bsk-ant-[A-Za-z0-9_-]{16,}/g },
|
|
8
|
+
{ label: "openai-key", pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}/g },
|
|
9
|
+
{ label: "xai-key", pattern: /\bxai-[A-Za-z0-9]{16,}/g },
|
|
10
|
+
{ label: "google-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
11
|
+
{ label: "aws-access-key-id", pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
12
|
+
{ label: "github-token", pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
|
|
13
|
+
{ label: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
14
|
+
{
|
|
15
|
+
label: "jwt",
|
|
16
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "bearer",
|
|
20
|
+
pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/g,
|
|
21
|
+
replace: () => `Bearer ${REDACTED}`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "url-credential",
|
|
25
|
+
pattern: /([a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi,
|
|
26
|
+
replace: (_m, prefix) => `${prefix}:${REDACTED}@`,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: "secret-assignment",
|
|
30
|
+
pattern: /\b(password|passwd|pwd|secret[_-]?key|client[_-]?secret|api[_-]?key|access[_-]?key|auth[_-]?token)\b(\s*[:=]\s*)(?:"[^"\n]{6,}"|'[^'\n]{6,}'|[^\s"'\n,;]{6,})/gi,
|
|
31
|
+
replace: (_m, key, sep) => `${key}${sep}${REDACTED}`,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
export function isRedactionEnabled(env = process.env) {
|
|
35
|
+
const raw = (env.LLM_GATEWAY_REDACT_LOGGED_SECRETS ?? "").trim().toLowerCase();
|
|
36
|
+
return raw !== "0" && raw !== "false" && raw !== "off" && raw !== "no";
|
|
37
|
+
}
|
|
38
|
+
export function redactSecrets(text) {
|
|
39
|
+
if (!text)
|
|
40
|
+
return text;
|
|
41
|
+
let out = text;
|
|
42
|
+
for (const rule of RULES) {
|
|
43
|
+
out = rule.replace
|
|
44
|
+
? out.replace(rule.pattern, rule.replace)
|
|
45
|
+
: out.replace(rule.pattern, REDACTED);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
export function redactIfEnabled(value, enabled) {
|
|
50
|
+
if (!enabled || !value)
|
|
51
|
+
return value;
|
|
52
|
+
return redactSecrets(value);
|
|
53
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
+
import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
|
|
2
3
|
const DEFAULT_SESSION_DESCRIPTIONS = {
|
|
3
4
|
claude: "Claude Session",
|
|
4
5
|
codex: "Codex Session",
|
|
@@ -16,11 +17,12 @@ export class PostgreSQLSessionManager {
|
|
|
16
17
|
const id = sessionId || randomUUID();
|
|
17
18
|
const sessionDescription = description ?? DEFAULT_SESSION_DESCRIPTIONS[cli];
|
|
18
19
|
const now = new Date().toISOString();
|
|
20
|
+
const ownerPrincipal = resolveOwnerPrincipal(getRequestContext());
|
|
19
21
|
const client = await this.pool.connect();
|
|
20
22
|
try {
|
|
21
23
|
await client.query("BEGIN");
|
|
22
|
-
await client.query(`INSERT INTO sessions (id, cli, description, created_at, last_used_at)
|
|
23
|
-
VALUES ($1, $2, $3, $4, $5)`, [id, cli, sessionDescription, now, now]);
|
|
24
|
+
await client.query(`INSERT INTO sessions (id, cli, description, created_at, last_used_at, owner_principal)
|
|
25
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [id, cli, sessionDescription, now, now, ownerPrincipal]);
|
|
24
26
|
await client.query(`INSERT INTO active_sessions (cli, session_id, updated_at)
|
|
25
27
|
VALUES ($1, $2, $3)
|
|
26
28
|
ON CONFLICT (cli) DO NOTHING`, [cli, id, now]);
|
|
@@ -31,6 +33,7 @@ export class PostgreSQLSessionManager {
|
|
|
31
33
|
createdAt: now,
|
|
32
34
|
lastUsedAt: now,
|
|
33
35
|
description: sessionDescription,
|
|
36
|
+
ownerPrincipal,
|
|
34
37
|
};
|
|
35
38
|
}
|
|
36
39
|
catch (error) {
|
|
@@ -42,18 +45,18 @@ export class PostgreSQLSessionManager {
|
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
async getSession(sessionId) {
|
|
45
|
-
const result = await this.pool.query(`SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
|
|
48
|
+
const result = await this.pool.query(`SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt", owner_principal AS "ownerPrincipal"
|
|
46
49
|
FROM sessions
|
|
47
50
|
WHERE id = $1`, [sessionId]);
|
|
48
51
|
return result.rows[0] ?? null;
|
|
49
52
|
}
|
|
50
53
|
async listSessions(cli) {
|
|
51
54
|
const query = cli
|
|
52
|
-
? `SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
|
|
55
|
+
? `SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt", owner_principal AS "ownerPrincipal"
|
|
53
56
|
FROM sessions
|
|
54
57
|
WHERE cli = $1
|
|
55
58
|
ORDER BY last_used_at DESC`
|
|
56
|
-
: `SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
|
|
59
|
+
: `SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt", owner_principal AS "ownerPrincipal"
|
|
57
60
|
FROM sessions
|
|
58
61
|
ORDER BY last_used_at DESC`;
|
|
59
62
|
const result = cli
|
package/dist/session-manager.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, dirname } from "path";
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, openSync, fsyncSync, closeSync, chmodSync, } from "fs";
|
|
5
5
|
import { DEFAULT_SESSION_TTL_SECONDS } from "./config.js";
|
|
6
6
|
import { noopLogger } from "./logger.js";
|
|
7
|
+
import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
|
|
7
8
|
export const CLI_TYPES = ["claude", "codex", "gemini", "grok", "mistral"];
|
|
8
9
|
export const API_PROVIDER_TYPES = ["grok-api"];
|
|
9
10
|
export const PROVIDER_TYPES = [...CLI_TYPES, ...API_PROVIDER_TYPES];
|
|
@@ -113,6 +114,7 @@ export class FileSessionManager {
|
|
|
113
114
|
createdAt: new Date().toISOString(),
|
|
114
115
|
lastUsedAt: new Date().toISOString(),
|
|
115
116
|
description: sessionDescription,
|
|
117
|
+
ownerPrincipal: resolveOwnerPrincipal(getRequestContext()),
|
|
116
118
|
};
|
|
117
119
|
this.storage.sessions[id] = session;
|
|
118
120
|
if (!this.storage.activeSession[cli]) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- F3: per-principal isolation. Add an ownership principal to PostgreSQL-backed
|
|
2
|
+
-- sessions, mirroring the file backend's `ownerPrincipal` and the job store's
|
|
3
|
+
-- `owner_principal`. Additive and nullable: rows created before this migration
|
|
4
|
+
-- keep NULL and are treated as legacy-unowned by F3b enforcement.
|
|
5
|
+
|
|
6
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS owner_principal TEXT;
|
|
7
|
+
|
|
8
|
+
INSERT INTO schema_migrations (version, name)
|
|
9
|
+
VALUES (4, '004_session_owner_principal')
|
|
10
|
+
ON CONFLICT (version) DO NOTHING;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "llm-cli-gateway",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.10.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"mcpName": "io.github.verivus-oss/llm-cli-gateway",
|
|
5
5
|
"description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
|
|
6
6
|
"license": "MIT",
|