llm-cli-gateway 2.7.0 → 2.9.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 +82 -0
- package/README.md +28 -1
- package/dist/acp/client.d.ts +78 -0
- package/dist/acp/client.js +201 -0
- package/dist/acp/errors.d.ts +63 -0
- package/dist/acp/errors.js +139 -0
- package/dist/acp/json-rpc-stdio.d.ts +71 -0
- package/dist/acp/json-rpc-stdio.js +375 -0
- package/dist/acp/process-manager.d.ts +66 -0
- package/dist/acp/process-manager.js +364 -0
- package/dist/acp/provider-registry.d.ts +24 -0
- package/dist/acp/provider-registry.js +82 -0
- package/dist/acp/types.d.ts +557 -0
- package/dist/acp/types.js +335 -0
- package/dist/approval-manager.d.ts +1 -0
- package/dist/approval-manager.js +14 -1
- package/dist/async-job-manager.d.ts +3 -0
- package/dist/async-job-manager.js +56 -16
- 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 +19 -11
- package/dist/cli-updater.js +5 -2
- package/dist/codex-json-parser.d.ts +3 -0
- package/dist/codex-json-parser.js +17 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +140 -0
- package/dist/flight-recorder.d.ts +7 -1
- package/dist/flight-recorder.js +33 -6
- package/dist/http-transport.js +21 -18
- package/dist/index.js +104 -34
- 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/pricing.d.ts +1 -1
- package/dist/pricing.js +67 -2
- package/dist/provider-tool-capabilities.d.ts +38 -0
- package/dist/provider-tool-capabilities.js +142 -0
- package/dist/request-context.d.ts +4 -0
- package/dist/request-context.js +16 -0
- package/dist/request-helpers.d.ts +4 -4
- package/dist/request-limits.d.ts +8 -0
- package/dist/request-limits.js +49 -0
- 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/dist/upstream-contracts.d.ts +27 -0
- package/dist/upstream-contracts.js +131 -0
- package/migrations/004_session_owner_principal.sql +10 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/cli-updater.js
CHANGED
|
@@ -237,10 +237,13 @@ export async function runCliUpgrade(params) {
|
|
|
237
237
|
exitCode: result.code,
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
|
+
const UPGRADE_TARGET_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
240
241
|
function normalizeTarget(target) {
|
|
241
242
|
const normalized = target.trim();
|
|
242
|
-
if (!
|
|
243
|
-
throw new Error("Upgrade target must be a
|
|
243
|
+
if (!UPGRADE_TARGET_PATTERN.test(normalized)) {
|
|
244
|
+
throw new Error("Upgrade target must be a bare version or dist-tag (letters, digits, '.', '_', '-'; " +
|
|
245
|
+
"1-64 chars; must start alphanumeric). Package specifiers, aliases, URLs, and paths " +
|
|
246
|
+
"(containing ':', '/', or '@') are not allowed.");
|
|
244
247
|
}
|
|
245
248
|
return normalized;
|
|
246
249
|
}
|
|
@@ -10,5 +10,8 @@ export interface CodexJsonParseResult {
|
|
|
10
10
|
error?: string;
|
|
11
11
|
threadId?: string;
|
|
12
12
|
finalMessage?: string;
|
|
13
|
+
sawEvent?: boolean;
|
|
13
14
|
}
|
|
14
15
|
export declare function parseCodexJsonStream(stdout: string): CodexJsonParseResult;
|
|
16
|
+
export declare function codexDisplayText(stdout: string): string;
|
|
17
|
+
export declare function codexFrResponse(outputFormat: string | undefined, stdout: string): string;
|
|
@@ -13,6 +13,7 @@ export function parseCodexJsonStream(stdout) {
|
|
|
13
13
|
if (!parsed || typeof parsed !== "object") {
|
|
14
14
|
continue;
|
|
15
15
|
}
|
|
16
|
+
result.sawEvent = true;
|
|
16
17
|
switch (parsed.type) {
|
|
17
18
|
case "thread.started":
|
|
18
19
|
if (typeof parsed.thread_id === "string") {
|
|
@@ -85,3 +86,19 @@ export function parseCodexJsonStream(stdout) {
|
|
|
85
86
|
}
|
|
86
87
|
return result;
|
|
87
88
|
}
|
|
89
|
+
export function codexDisplayText(stdout) {
|
|
90
|
+
const parsed = parseCodexJsonStream(stdout);
|
|
91
|
+
if (parsed.finalMessage !== undefined) {
|
|
92
|
+
return parsed.finalMessage;
|
|
93
|
+
}
|
|
94
|
+
if (parsed.error !== undefined) {
|
|
95
|
+
return parsed.error;
|
|
96
|
+
}
|
|
97
|
+
if (parsed.sawEvent) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
return stdout;
|
|
101
|
+
}
|
|
102
|
+
export function codexFrResponse(outputFormat, stdout) {
|
|
103
|
+
return outputFormat === "json" ? stdout : codexDisplayText(stdout);
|
|
104
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -76,4 +76,34 @@ export interface ProvidersConfig {
|
|
|
76
76
|
}
|
|
77
77
|
export declare function loadProvidersConfig(logger?: Logger): ProvidersConfig;
|
|
78
78
|
export declare function isXaiProviderEnabled(config: ProvidersConfig, env?: NodeJS.ProcessEnv): boolean;
|
|
79
|
+
export declare const ACP_TRANSPORTS: readonly ["cli", "acp"];
|
|
80
|
+
export type AcpTransport = (typeof ACP_TRANSPORTS)[number];
|
|
81
|
+
export declare const DEFAULT_ACP_PROCESS_IDLE_TIMEOUT_MS = 600000;
|
|
82
|
+
export declare const DEFAULT_ACP_INITIALIZE_TIMEOUT_MS = 10000;
|
|
83
|
+
export declare const DEFAULT_ACP_SESSION_NEW_TIMEOUT_MS = 10000;
|
|
84
|
+
export declare const DEFAULT_ACP_PROMPT_TIMEOUT_MS = 600000;
|
|
85
|
+
export interface AcpProviderConfig {
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
command: string;
|
|
88
|
+
args: string[];
|
|
89
|
+
runtimeEnabled: boolean;
|
|
90
|
+
isolatedLeaderSocket: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface AcpConfig {
|
|
93
|
+
enabled: boolean;
|
|
94
|
+
defaultTransport: AcpTransport;
|
|
95
|
+
smokeOnStartup: boolean;
|
|
96
|
+
processIdleTimeoutMs: number;
|
|
97
|
+
initializeTimeoutMs: number;
|
|
98
|
+
sessionNewTimeoutMs: number;
|
|
99
|
+
promptTimeoutMs: number;
|
|
100
|
+
allowWriteHostServices: boolean;
|
|
101
|
+
allowTerminalHostServices: boolean;
|
|
102
|
+
fallbackToCliWhenUnhealthy: boolean;
|
|
103
|
+
providers: Record<string, AcpProviderConfig>;
|
|
104
|
+
sources: {
|
|
105
|
+
configFile: string | null;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export declare function loadAcpConfig(logger?: Logger): AcpConfig;
|
|
79
109
|
export declare function loadRemoteOAuthConfig(logger?: Logger, env?: NodeJS.ProcessEnv): RemoteOAuthConfig;
|
package/dist/config.js
CHANGED
|
@@ -348,6 +348,125 @@ export function isXaiProviderEnabled(config, env = process.env) {
|
|
|
348
348
|
return false;
|
|
349
349
|
return typeof env[keyEnv] === "string" && env[keyEnv].trim().length > 0;
|
|
350
350
|
}
|
|
351
|
+
export const ACP_TRANSPORTS = ["cli", "acp"];
|
|
352
|
+
export const DEFAULT_ACP_PROCESS_IDLE_TIMEOUT_MS = 600000;
|
|
353
|
+
export const DEFAULT_ACP_INITIALIZE_TIMEOUT_MS = 10000;
|
|
354
|
+
export const DEFAULT_ACP_SESSION_NEW_TIMEOUT_MS = 10000;
|
|
355
|
+
export const DEFAULT_ACP_PROMPT_TIMEOUT_MS = 600000;
|
|
356
|
+
const SHELL_METACHARACTERS = /[\s|&;<>(){}$`"'\\*?[\]~#!\0]/;
|
|
357
|
+
function isSafeExecutable(value) {
|
|
358
|
+
if (value.length === 0)
|
|
359
|
+
return false;
|
|
360
|
+
return !SHELL_METACHARACTERS.test(value);
|
|
361
|
+
}
|
|
362
|
+
const SafeExecutableSchema = z
|
|
363
|
+
.string()
|
|
364
|
+
.min(1)
|
|
365
|
+
.refine(isSafeExecutable, {
|
|
366
|
+
message: "ACP provider command must be a bare executable name or path with no shell metacharacters " +
|
|
367
|
+
"(no spaces, quotes, pipes, redirects, globs, or command substitution); pass arguments via 'args'",
|
|
368
|
+
});
|
|
369
|
+
const SafeArgSchema = z.string();
|
|
370
|
+
const AcpProviderSchema = z
|
|
371
|
+
.object({
|
|
372
|
+
enabled: z.boolean().default(false),
|
|
373
|
+
command: SafeExecutableSchema,
|
|
374
|
+
args: z.array(SafeArgSchema).default([]),
|
|
375
|
+
runtime_enabled: z.boolean().default(false),
|
|
376
|
+
isolated_leader_socket: z.boolean().default(false),
|
|
377
|
+
})
|
|
378
|
+
.strict();
|
|
379
|
+
const AcpConfigSchema = z
|
|
380
|
+
.object({
|
|
381
|
+
enabled: z.boolean().default(false),
|
|
382
|
+
default_transport: z.enum(ACP_TRANSPORTS).default("cli"),
|
|
383
|
+
smoke_on_startup: z.boolean().default(false),
|
|
384
|
+
process_idle_timeout_ms: z
|
|
385
|
+
.number()
|
|
386
|
+
.int()
|
|
387
|
+
.positive()
|
|
388
|
+
.default(DEFAULT_ACP_PROCESS_IDLE_TIMEOUT_MS),
|
|
389
|
+
initialize_timeout_ms: z.number().int().positive().default(DEFAULT_ACP_INITIALIZE_TIMEOUT_MS),
|
|
390
|
+
session_new_timeout_ms: z.number().int().positive().default(DEFAULT_ACP_SESSION_NEW_TIMEOUT_MS),
|
|
391
|
+
prompt_timeout_ms: z.number().int().positive().default(DEFAULT_ACP_PROMPT_TIMEOUT_MS),
|
|
392
|
+
allow_write_host_services: z.boolean().default(false),
|
|
393
|
+
allow_terminal_host_services: z.boolean().default(false),
|
|
394
|
+
fallback_to_cli_when_unhealthy: z.boolean().default(true),
|
|
395
|
+
providers: z.record(z.string(), AcpProviderSchema).default({}),
|
|
396
|
+
})
|
|
397
|
+
.strict();
|
|
398
|
+
function defaultAcpConfig(sourcePath) {
|
|
399
|
+
return {
|
|
400
|
+
enabled: false,
|
|
401
|
+
defaultTransport: "cli",
|
|
402
|
+
smokeOnStartup: false,
|
|
403
|
+
processIdleTimeoutMs: DEFAULT_ACP_PROCESS_IDLE_TIMEOUT_MS,
|
|
404
|
+
initializeTimeoutMs: DEFAULT_ACP_INITIALIZE_TIMEOUT_MS,
|
|
405
|
+
sessionNewTimeoutMs: DEFAULT_ACP_SESSION_NEW_TIMEOUT_MS,
|
|
406
|
+
promptTimeoutMs: DEFAULT_ACP_PROMPT_TIMEOUT_MS,
|
|
407
|
+
allowWriteHostServices: false,
|
|
408
|
+
allowTerminalHostServices: false,
|
|
409
|
+
fallbackToCliWhenUnhealthy: true,
|
|
410
|
+
providers: {},
|
|
411
|
+
sources: { configFile: sourcePath },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function readAcpFile(configPath, logger) {
|
|
415
|
+
if (!existsSync(configPath)) {
|
|
416
|
+
return { raw: undefined, sourcePath: null };
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const require = createRequire(import.meta.url);
|
|
420
|
+
const TOML = require("smol-toml");
|
|
421
|
+
const text = readFileSync(configPath, "utf-8");
|
|
422
|
+
const parsed = TOML.parse(text);
|
|
423
|
+
return { raw: parsed?.acp, sourcePath: configPath };
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
logger.error(`Failed to parse gateway config at ${configPath}; using acp defaults`, err);
|
|
427
|
+
return { raw: undefined, sourcePath: null };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
export function loadAcpConfig(logger = noopLogger) {
|
|
431
|
+
const configPath = defaultGatewayConfigPath();
|
|
432
|
+
const { raw, sourcePath } = readAcpFile(configPath, logger);
|
|
433
|
+
if (raw === undefined) {
|
|
434
|
+
return defaultAcpConfig(sourcePath);
|
|
435
|
+
}
|
|
436
|
+
let parsed;
|
|
437
|
+
try {
|
|
438
|
+
parsed = AcpConfigSchema.parse(raw);
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
throw new Error(`Invalid [acp] config: ${err instanceof Error ? err.message : String(err)}`, {
|
|
442
|
+
cause: err,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const providers = {};
|
|
446
|
+
for (const [name, p] of Object.entries(parsed.providers)) {
|
|
447
|
+
providers[name] = {
|
|
448
|
+
enabled: p.enabled,
|
|
449
|
+
command: p.command,
|
|
450
|
+
args: p.args,
|
|
451
|
+
runtimeEnabled: p.runtime_enabled,
|
|
452
|
+
isolatedLeaderSocket: p.isolated_leader_socket,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
enabled: parsed.enabled,
|
|
457
|
+
defaultTransport: parsed.default_transport,
|
|
458
|
+
smokeOnStartup: parsed.smoke_on_startup,
|
|
459
|
+
processIdleTimeoutMs: parsed.process_idle_timeout_ms,
|
|
460
|
+
initializeTimeoutMs: parsed.initialize_timeout_ms,
|
|
461
|
+
sessionNewTimeoutMs: parsed.session_new_timeout_ms,
|
|
462
|
+
promptTimeoutMs: parsed.prompt_timeout_ms,
|
|
463
|
+
allowWriteHostServices: parsed.allow_write_host_services,
|
|
464
|
+
allowTerminalHostServices: parsed.allow_terminal_host_services,
|
|
465
|
+
fallbackToCliWhenUnhealthy: parsed.fallback_to_cli_when_unhealthy,
|
|
466
|
+
providers,
|
|
467
|
+
sources: { configFile: sourcePath },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
351
470
|
const OAuthRegistrationPolicySchema = z.enum(["static_clients", "shared_secret", "open_dev"]);
|
|
352
471
|
const OAuthClientSchema = z
|
|
353
472
|
.object({
|
|
@@ -373,6 +492,8 @@ const OAuthConfigSchema = z
|
|
|
373
492
|
registration_policy: OAuthRegistrationPolicySchema.default("static_clients"),
|
|
374
493
|
allow_public_clients: z.boolean().default(false),
|
|
375
494
|
token_ttl_seconds: z.number().int().positive().default(3600),
|
|
495
|
+
require_consent: z.boolean().default(false),
|
|
496
|
+
consent_secret_hash: z.string().optional(),
|
|
376
497
|
clients: z.array(OAuthClientSchema).default([]),
|
|
377
498
|
shared_secret: OAuthSharedSecretSchema.optional(),
|
|
378
499
|
})
|
|
@@ -386,6 +507,8 @@ function disabledOAuthConfig(sourcePath = null, envOverrides = []) {
|
|
|
386
507
|
registrationPolicy: "static_clients",
|
|
387
508
|
allowPublicClients: false,
|
|
388
509
|
tokenTtlSeconds: 3600,
|
|
510
|
+
requireConsent: false,
|
|
511
|
+
consentSecretHash: null,
|
|
389
512
|
clients: [],
|
|
390
513
|
sharedSecret: null,
|
|
391
514
|
sources: { configFile: sourcePath, envOverrides },
|
|
@@ -417,6 +540,15 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
|
|
|
417
540
|
? "LLM_GATEWAY_OAUTH_REGISTRATION_SECRET"
|
|
418
541
|
: "LLM_GATEWAY_OAUTH_SHARED_SECRET");
|
|
419
542
|
}
|
|
543
|
+
if (env.LLM_GATEWAY_OAUTH_REQUIRE_CONSENT !== undefined) {
|
|
544
|
+
merged.require_consent = env.LLM_GATEWAY_OAUTH_REQUIRE_CONSENT === "1";
|
|
545
|
+
envOverrides.push("LLM_GATEWAY_OAUTH_REQUIRE_CONSENT");
|
|
546
|
+
}
|
|
547
|
+
if (env.LLM_GATEWAY_OAUTH_CONSENT_SECRET) {
|
|
548
|
+
merged.consent_secret_hash = hashSecret(env.LLM_GATEWAY_OAUTH_CONSENT_SECRET);
|
|
549
|
+
merged.require_consent = merged.require_consent ?? true;
|
|
550
|
+
envOverrides.push("LLM_GATEWAY_OAUTH_CONSENT_SECRET");
|
|
551
|
+
}
|
|
420
552
|
const parsed = OAuthConfigSchema.safeParse(merged);
|
|
421
553
|
if (!parsed.success) {
|
|
422
554
|
logWarn(logger, "Invalid [http.oauth] config; remote OAuth disabled", {
|
|
@@ -459,6 +591,12 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
|
|
|
459
591
|
if (data.registration_policy === "open_dev" && env.LLM_GATEWAY_OAUTH_OPEN_DEV !== "1") {
|
|
460
592
|
logWarn(logger, "[http.oauth].registration_policy='open_dev' is intended for localhost/dev only");
|
|
461
593
|
}
|
|
594
|
+
if (data.require_consent) {
|
|
595
|
+
if (!data.consent_secret_hash || !isSecretHash(data.consent_secret_hash)) {
|
|
596
|
+
logWarn(logger, "[http.oauth].require_consent is set but consent_secret_hash is missing/invalid; remote OAuth disabled");
|
|
597
|
+
return disabledOAuthConfig(sourcePath, envOverrides);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
462
600
|
return {
|
|
463
601
|
enabled: data.enabled,
|
|
464
602
|
issuer: data.issuer,
|
|
@@ -467,6 +605,8 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
|
|
|
467
605
|
registrationPolicy: data.registration_policy,
|
|
468
606
|
allowPublicClients: data.allow_public_clients,
|
|
469
607
|
tokenTtlSeconds: data.token_ttl_seconds,
|
|
608
|
+
requireConsent: data.require_consent,
|
|
609
|
+
consentSecretHash: data.consent_secret_hash ?? null,
|
|
470
610
|
clients: data.clients.map(client => ({
|
|
471
611
|
clientId: client.client_id,
|
|
472
612
|
clientSecretHash: client.client_secret_hash ?? null,
|
|
@@ -11,6 +11,7 @@ export interface FlightLogStart {
|
|
|
11
11
|
stablePrefixTokens?: number;
|
|
12
12
|
cacheControlBlocks?: number;
|
|
13
13
|
cacheControlTtlSeconds?: number;
|
|
14
|
+
ownerPrincipal?: string | null;
|
|
14
15
|
}
|
|
15
16
|
export interface FlightLogResult {
|
|
16
17
|
response: string;
|
|
@@ -39,11 +40,16 @@ export declare class FlightRecorder {
|
|
|
39
40
|
private readOnlyDb;
|
|
40
41
|
private closed;
|
|
41
42
|
private readonly dbPath;
|
|
43
|
+
private readonly redactEnabled;
|
|
42
44
|
private insertStartTxn;
|
|
43
45
|
private updateCompleteTxn;
|
|
44
|
-
constructor(dbPath: string
|
|
46
|
+
constructor(dbPath: string, options?: {
|
|
47
|
+
redactSecrets?: boolean;
|
|
48
|
+
});
|
|
45
49
|
logStart(entry: FlightLogStart): void;
|
|
46
50
|
logComplete(correlationId: string, result: FlightLogResult): void;
|
|
51
|
+
private redactStart;
|
|
52
|
+
private redactResult;
|
|
47
53
|
queryRequests<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T[];
|
|
48
54
|
flush(): void;
|
|
49
55
|
close(): void;
|
package/dist/flight-recorder.js
CHANGED
|
@@ -2,6 +2,8 @@ import { chmodSync } from "fs";
|
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { openDatabase, openReadOnly } from "./sqlite-driver.js";
|
|
5
|
+
import { redactSecrets, isRedactionEnabled } from "./secret-redaction.js";
|
|
6
|
+
import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
|
|
5
7
|
const MAX_THINKING_BYTES = 1_000_000;
|
|
6
8
|
function ensureRequestsCacheColumns(db) {
|
|
7
9
|
const rows = db.prepare("PRAGMA table_info(requests)").all();
|
|
@@ -13,6 +15,13 @@ function ensureRequestsCacheColumns(db) {
|
|
|
13
15
|
db.exec("ALTER TABLE requests ADD COLUMN cache_creation_tokens INTEGER");
|
|
14
16
|
}
|
|
15
17
|
}
|
|
18
|
+
function ensureRequestsOwnerColumn(db) {
|
|
19
|
+
const rows = db.prepare("PRAGMA table_info(requests)").all();
|
|
20
|
+
const names = new Set(rows.map((row) => (row && typeof row.name === "string" ? row.name : "")));
|
|
21
|
+
if (!names.has("owner_principal")) {
|
|
22
|
+
db.exec("ALTER TABLE requests ADD COLUMN owner_principal TEXT");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
16
25
|
function ensureStablePrefixColumns(db) {
|
|
17
26
|
const rows = db.prepare("PRAGMA table_info(requests)").all();
|
|
18
27
|
const names = new Set(rows.map((row) => (row && typeof row.name === "string" ? row.name : "")));
|
|
@@ -87,10 +96,12 @@ export class FlightRecorder {
|
|
|
87
96
|
readOnlyDb = null;
|
|
88
97
|
closed = false;
|
|
89
98
|
dbPath;
|
|
99
|
+
redactEnabled;
|
|
90
100
|
insertStartTxn;
|
|
91
101
|
updateCompleteTxn;
|
|
92
|
-
constructor(dbPath) {
|
|
102
|
+
constructor(dbPath, options = {}) {
|
|
93
103
|
this.dbPath = dbPath;
|
|
104
|
+
this.redactEnabled = options.redactSecrets ?? isRedactionEnabled();
|
|
94
105
|
this.db = openDatabase(dbPath);
|
|
95
106
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
96
107
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -113,7 +124,8 @@ export class FlightRecorder {
|
|
|
113
124
|
input_tokens INTEGER,
|
|
114
125
|
output_tokens INTEGER,
|
|
115
126
|
cache_read_tokens INTEGER,
|
|
116
|
-
cache_creation_tokens INTEGER
|
|
127
|
+
cache_creation_tokens INTEGER,
|
|
128
|
+
owner_principal TEXT
|
|
117
129
|
);
|
|
118
130
|
|
|
119
131
|
CREATE TABLE IF NOT EXISTS gateway_metadata (
|
|
@@ -155,6 +167,10 @@ export class FlightRecorder {
|
|
|
155
167
|
this.db
|
|
156
168
|
.prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(5, ?)")
|
|
157
169
|
.run(new Date().toISOString());
|
|
170
|
+
ensureRequestsOwnerColumn(this.db);
|
|
171
|
+
this.db
|
|
172
|
+
.prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(6, ?)")
|
|
173
|
+
.run(new Date().toISOString());
|
|
158
174
|
if (process.platform !== "win32") {
|
|
159
175
|
try {
|
|
160
176
|
chmodSync(dbPath, 0o600);
|
|
@@ -165,10 +181,10 @@ export class FlightRecorder {
|
|
|
165
181
|
const insertRequest = this.db.prepare(`
|
|
166
182
|
INSERT INTO requests (id, cli, model, prompt, system, session_id, datetime_utc,
|
|
167
183
|
stable_prefix_hash, stable_prefix_tokens,
|
|
168
|
-
cache_control_blocks, cache_control_ttl_seconds)
|
|
184
|
+
cache_control_blocks, cache_control_ttl_seconds, owner_principal)
|
|
169
185
|
VALUES (@id, @cli, @model, @prompt, @system, @session_id, @datetime_utc,
|
|
170
186
|
@stable_prefix_hash, @stable_prefix_tokens,
|
|
171
|
-
@cache_control_blocks, @cache_control_ttl_seconds)
|
|
187
|
+
@cache_control_blocks, @cache_control_ttl_seconds, @owner_principal)
|
|
172
188
|
`);
|
|
173
189
|
const insertMetadata = this.db.prepare(`
|
|
174
190
|
INSERT INTO gateway_metadata (request_id, async_job_id, status)
|
|
@@ -187,6 +203,7 @@ export class FlightRecorder {
|
|
|
187
203
|
stable_prefix_tokens: entry.stablePrefixTokens ?? null,
|
|
188
204
|
cache_control_blocks: entry.cacheControlBlocks ?? null,
|
|
189
205
|
cache_control_ttl_seconds: entry.cacheControlTtlSeconds ?? null,
|
|
206
|
+
owner_principal: entry.ownerPrincipal ?? resolveOwnerPrincipal(getRequestContext()),
|
|
190
207
|
});
|
|
191
208
|
insertMetadata.run({
|
|
192
209
|
request_id: entry.correlationId,
|
|
@@ -244,10 +261,20 @@ export class FlightRecorder {
|
|
|
244
261
|
});
|
|
245
262
|
}
|
|
246
263
|
logStart(entry) {
|
|
247
|
-
this.insertStartTxn(entry);
|
|
264
|
+
this.insertStartTxn(this.redactEnabled ? this.redactStart(entry) : entry);
|
|
248
265
|
}
|
|
249
266
|
logComplete(correlationId, result) {
|
|
250
|
-
this.updateCompleteTxn(correlationId, result);
|
|
267
|
+
this.updateCompleteTxn(correlationId, this.redactEnabled ? this.redactResult(result) : result);
|
|
268
|
+
}
|
|
269
|
+
redactStart(entry) {
|
|
270
|
+
return {
|
|
271
|
+
...entry,
|
|
272
|
+
prompt: redactSecrets(entry.prompt),
|
|
273
|
+
system: entry.system ? redactSecrets(entry.system) : entry.system,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
redactResult(result) {
|
|
277
|
+
return { ...result, response: redactSecrets(result.response) };
|
|
251
278
|
}
|
|
252
279
|
queryRequests(sql, ...params) {
|
|
253
280
|
if (this.closed) {
|
package/dist/http-transport.js
CHANGED
|
@@ -2,10 +2,11 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
4
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { authorizeBearerRequest, getRequiredBearerToken, writeAuthFailure } from "./auth.js";
|
|
5
|
+
import { authorizeBearerRequest, getRequiredBearerToken, resolveTrustedPrincipal, writeAuthFailure, } from "./auth.js";
|
|
6
6
|
import { loadRemoteOAuthConfig } from "./config.js";
|
|
7
7
|
import { OAuthServer, oauthBaseUrlFromRequest } from "./oauth.js";
|
|
8
8
|
import { runWithRequestContext } from "./request-context.js";
|
|
9
|
+
import { readCappedRawBody, maxHttpBodyBytes } from "./request-limits.js";
|
|
9
10
|
const noopLogger = {
|
|
10
11
|
info: (..._args) => { },
|
|
11
12
|
error: (..._args) => { },
|
|
@@ -14,22 +15,8 @@ const noopLogger = {
|
|
|
14
15
|
function firstHeader(value) {
|
|
15
16
|
return Array.isArray(value) ? value[0] : value;
|
|
16
17
|
}
|
|
17
|
-
function readRawBody(req) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
const chunks = [];
|
|
20
|
-
req.on("data", chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
21
|
-
req.on("error", reject);
|
|
22
|
-
req.on("end", () => {
|
|
23
|
-
if (chunks.length === 0) {
|
|
24
|
-
resolve("");
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
18
|
async function readBody(req) {
|
|
32
|
-
const raw = await
|
|
19
|
+
const raw = await readCappedRawBody(req, maxHttpBodyBytes());
|
|
33
20
|
if (!raw)
|
|
34
21
|
return undefined;
|
|
35
22
|
try {
|
|
@@ -94,6 +81,13 @@ export async function startHttpGateway(options) {
|
|
|
94
81
|
const sessions = new Map();
|
|
95
82
|
const token = getRequiredBearerToken();
|
|
96
83
|
const oauthConfig = loadRemoteOAuthConfig(logger);
|
|
84
|
+
if (oauthConfig.enabled &&
|
|
85
|
+
(oauthConfig.allowPublicClients || oauthConfig.registrationPolicy === "open_dev") &&
|
|
86
|
+
!isLocalHost(host)) {
|
|
87
|
+
throw new Error(`Refusing to start: remote OAuth with ${oauthConfig.allowPublicClients ? "public clients" : "open_dev registration"} is exposed on a non-loopback bind (host=${host}). Bind LLM_GATEWAY_HTTP_HOST to 127.0.0.1 ` +
|
|
88
|
+
`and front the gateway with an authenticating proxy, or switch to ` +
|
|
89
|
+
`registration_policy=static_clients with confidential client secrets.`);
|
|
90
|
+
}
|
|
97
91
|
const oauthServer = oauthConfig.enabled
|
|
98
92
|
? new OAuthServer({ protectedPath: path, config: oauthConfig, logger })
|
|
99
93
|
: null;
|
|
@@ -155,17 +149,20 @@ export async function startHttpGateway(options) {
|
|
|
155
149
|
jsonError(res, 404, "Not found");
|
|
156
150
|
return;
|
|
157
151
|
}
|
|
158
|
-
let requestContext = { authScopes: [] };
|
|
152
|
+
let requestContext = { authScopes: [], transport: "http" };
|
|
159
153
|
if (!noAuthPath) {
|
|
160
154
|
const auth = authorizeBearerRequest(req, token);
|
|
161
155
|
if (!auth.ok) {
|
|
162
156
|
writeAuthFailure(res, auth, resourceMetadataUrl ? { resourceMetadataUrl } : {});
|
|
163
157
|
return;
|
|
164
158
|
}
|
|
159
|
+
const trustedPrincipal = resolveTrustedPrincipal(req, auth);
|
|
165
160
|
requestContext = {
|
|
161
|
+
transport: "http",
|
|
166
162
|
authKind: auth.kind,
|
|
167
163
|
authScopes: auth.scopes ?? [],
|
|
168
164
|
authClientId: auth.clientId,
|
|
165
|
+
authPrincipal: trustedPrincipal ?? auth.clientId,
|
|
169
166
|
};
|
|
170
167
|
}
|
|
171
168
|
if (req.method !== "GET" && req.method !== "POST" && req.method !== "DELETE") {
|
|
@@ -213,7 +210,13 @@ export async function startHttpGateway(options) {
|
|
|
213
210
|
catch (error) {
|
|
214
211
|
logger.error("HTTP transport request failed", error);
|
|
215
212
|
if (!res.headersSent) {
|
|
216
|
-
|
|
213
|
+
const statusCode = error?.statusCode;
|
|
214
|
+
if (statusCode === 413) {
|
|
215
|
+
jsonError(res, 413, "Payload too large");
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
jsonError(res, 500, "Internal server error");
|
|
219
|
+
}
|
|
217
220
|
}
|
|
218
221
|
else {
|
|
219
222
|
res.end();
|