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.
@@ -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: (await this.sessionManager.getActiveSession("claude"))?.id || null,
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: (await this.sessionManager.getActiveSession("codex"))?.id || null,
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: (await this.sessionManager.getActiveSession("gemini"))?.id || null,
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: (await this.sessionManager.getActiveSession("grok"))?.id || null,
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: (await this.sessionManager.getActiveSession("mistral"))?.id || null,
297
+ activeSession: await this.ownedActiveId("mistral"),
289
298
  }, null, 2),
290
299
  };
291
300
  }
@@ -0,0 +1,3 @@
1
+ export declare function isRedactionEnabled(env?: NodeJS.ProcessEnv): boolean;
2
+ export declare function redactSecrets(text: string): string;
3
+ export declare function redactIfEnabled(value: string | null | undefined, enabled: boolean): typeof value;
@@ -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
@@ -14,6 +14,7 @@ export interface Session {
14
14
  lastUsedAt: string;
15
15
  description?: string;
16
16
  metadata?: Record<string, any>;
17
+ ownerPrincipal?: string | null;
17
18
  }
18
19
  export interface SessionStorage {
19
20
  sessions: Record<string, Session>;
@@ -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;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "2.8.0",
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.8.0",
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.8.0",
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",