llm-cli-gateway 2.9.0 → 2.11.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 +92 -0
- package/README.md +7 -5
- package/dist/acp/event-normalizer.d.ts +42 -0
- package/dist/acp/event-normalizer.js +71 -0
- package/dist/acp/flight-redaction.d.ts +25 -0
- package/dist/acp/flight-redaction.js +40 -0
- package/dist/acp/host-services.d.ts +16 -0
- package/dist/acp/host-services.js +29 -0
- package/dist/acp/permission-bridge.d.ts +15 -0
- package/dist/acp/permission-bridge.js +90 -0
- package/dist/acp/process-manager.js +7 -1
- package/dist/acp/provider-registry.d.ts +1 -1
- package/dist/acp/provider-registry.js +13 -0
- package/dist/acp/runtime.d.ts +35 -0
- package/dist/acp/runtime.js +125 -0
- package/dist/acp/session-map.d.ts +42 -0
- package/dist/acp/session-map.js +67 -0
- package/dist/acp/smoke-harness.d.ts +28 -0
- package/dist/acp/smoke-harness.js +90 -0
- package/dist/api-http.d.ts +18 -0
- package/dist/api-http.js +122 -0
- package/dist/api-provider.d.ts +83 -0
- package/dist/api-provider.js +258 -0
- package/dist/api-request.d.ts +30 -0
- package/dist/api-request.js +51 -0
- package/dist/approval-manager.d.ts +1 -1
- package/dist/approval-manager.js +6 -7
- package/dist/async-job-manager.d.ts +19 -4
- package/dist/async-job-manager.js +211 -35
- package/dist/claude-mcp-config.d.ts +2 -2
- package/dist/claude-mcp-config.js +42 -52
- package/dist/cli-updater.js +16 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.js +93 -35
- package/dist/doctor.d.ts +1 -1
- package/dist/flight-recorder.d.ts +1 -0
- package/dist/flight-recorder.js +11 -0
- package/dist/index.d.ts +56 -5
- package/dist/index.js +670 -48
- package/dist/job-store.d.ts +15 -0
- package/dist/job-store.js +39 -5
- package/dist/mcp-registry.d.ts +17 -0
- package/dist/mcp-registry.js +5 -0
- package/dist/metrics.js +7 -2
- package/dist/model-registry.js +11 -0
- package/dist/prompt-parts.d.ts +6 -6
- package/dist/provider-login-guidance.js +21 -0
- package/dist/provider-status.js +4 -1
- package/dist/provider-tool-capabilities.d.ts +4 -3
- package/dist/provider-tool-capabilities.js +93 -6
- package/dist/request-helpers.d.ts +6 -6
- package/dist/request-helpers.js +1 -4
- package/dist/resources.d.ts +2 -0
- package/dist/resources.js +24 -15
- package/dist/session-manager-pg.js +2 -9
- package/dist/session-manager.d.ts +9 -4
- package/dist/session-manager.js +13 -4
- package/dist/upstream-contracts.js +112 -2
- package/dist/validation-normalizer.d.ts +2 -2
- package/dist/validation-orchestrator.d.ts +2 -0
- package/dist/validation-orchestrator.js +28 -7
- package/dist/validation-tools.d.ts +61 -0
- package/dist/validation-tools.js +36 -21
- package/migrations/005_provider_type_open_api_names.sql +28 -0
- package/npm-shrinkwrap.json +6 -5
- package/package.json +12 -9
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
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
+
import { defaultSessionDescription } from "./session-manager.js";
|
|
2
3
|
import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
|
|
3
|
-
const DEFAULT_SESSION_DESCRIPTIONS = {
|
|
4
|
-
claude: "Claude Session",
|
|
5
|
-
codex: "Codex Session",
|
|
6
|
-
gemini: "Gemini Session",
|
|
7
|
-
grok: "Grok Session",
|
|
8
|
-
mistral: "Mistral Session",
|
|
9
|
-
"grok-api": "Grok API Session",
|
|
10
|
-
};
|
|
11
4
|
export class PostgreSQLSessionManager {
|
|
12
5
|
pool;
|
|
13
6
|
constructor(pool) {
|
|
@@ -15,7 +8,7 @@ export class PostgreSQLSessionManager {
|
|
|
15
8
|
}
|
|
16
9
|
async createSession(cli, description, sessionId) {
|
|
17
10
|
const id = sessionId || randomUUID();
|
|
18
|
-
const sessionDescription = description ??
|
|
11
|
+
const sessionDescription = description ?? defaultSessionDescription(cli);
|
|
19
12
|
const now = new Date().toISOString();
|
|
20
13
|
const ownerPrincipal = resolveOwnerPrincipal(getRequestContext());
|
|
21
14
|
const client = await this.pool.connect();
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { Config } from "./config.js";
|
|
2
2
|
import type { DatabaseConnection } from "./db.js";
|
|
3
3
|
import type { Logger } from "./logger.js";
|
|
4
|
-
export declare const CLI_TYPES: readonly ["claude", "codex", "gemini", "grok", "mistral"];
|
|
4
|
+
export declare const CLI_TYPES: readonly ["claude", "codex", "gemini", "grok", "mistral", "devin"];
|
|
5
5
|
export type CliType = (typeof CLI_TYPES)[number];
|
|
6
6
|
export declare const API_PROVIDER_TYPES: readonly ["grok-api"];
|
|
7
|
-
export type
|
|
8
|
-
export
|
|
9
|
-
export type ProviderType =
|
|
7
|
+
export type KnownApiProviderType = (typeof API_PROVIDER_TYPES)[number];
|
|
8
|
+
export type ApiProviderType = KnownApiProviderType | (string & {});
|
|
9
|
+
export type ProviderType = CliType | ApiProviderType;
|
|
10
|
+
export type ProviderKind = "cli" | "api";
|
|
11
|
+
export declare const PROVIDER_TYPES: readonly ["claude", "codex", "gemini", "grok", "mistral", "devin", "grok-api"];
|
|
12
|
+
export declare function isCliType(provider: string): provider is CliType;
|
|
13
|
+
export declare function providerKind(provider: ProviderType): ProviderKind;
|
|
14
|
+
export declare function defaultSessionDescription(provider: ProviderType): string;
|
|
10
15
|
export interface Session {
|
|
11
16
|
id: string;
|
|
12
17
|
cli: ProviderType;
|
package/dist/session-manager.js
CHANGED
|
@@ -5,11 +5,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, openSyn
|
|
|
5
5
|
import { DEFAULT_SESSION_TTL_SECONDS } from "./config.js";
|
|
6
6
|
import { noopLogger } from "./logger.js";
|
|
7
7
|
import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
|
|
8
|
-
export const CLI_TYPES = ["claude", "codex", "gemini", "grok", "mistral"];
|
|
8
|
+
export const CLI_TYPES = ["claude", "codex", "gemini", "grok", "mistral", "devin"];
|
|
9
9
|
export const API_PROVIDER_TYPES = ["grok-api"];
|
|
10
10
|
export const PROVIDER_TYPES = [...CLI_TYPES, ...API_PROVIDER_TYPES];
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
export function isCliType(provider) {
|
|
12
|
+
return CLI_TYPES.includes(provider);
|
|
13
|
+
}
|
|
14
|
+
export function providerKind(provider) {
|
|
15
|
+
return isCliType(provider) ? "cli" : "api";
|
|
16
|
+
}
|
|
17
|
+
const KNOWN_SESSION_DESCRIPTIONS = {
|
|
13
18
|
claude: "Claude Session",
|
|
14
19
|
codex: "Codex Session",
|
|
15
20
|
gemini: "Gemini Session",
|
|
@@ -17,6 +22,10 @@ const DEFAULT_SESSION_DESCRIPTIONS = {
|
|
|
17
22
|
mistral: "Mistral Session",
|
|
18
23
|
"grok-api": "Grok API Session",
|
|
19
24
|
};
|
|
25
|
+
export function defaultSessionDescription(provider) {
|
|
26
|
+
return KNOWN_SESSION_DESCRIPTIONS[provider] ?? `${provider} Session`;
|
|
27
|
+
}
|
|
28
|
+
const createEmptyActiveSessions = () => Object.fromEntries(PROVIDER_TYPES.map(provider => [provider, null]));
|
|
20
29
|
export class FileSessionManager {
|
|
21
30
|
storagePath;
|
|
22
31
|
storage = { sessions: {}, activeSession: createEmptyActiveSessions() };
|
|
@@ -107,7 +116,7 @@ export class FileSessionManager {
|
|
|
107
116
|
createSession(cli, description, sessionId) {
|
|
108
117
|
this.evictExpiredSessions();
|
|
109
118
|
const id = sessionId || randomUUID();
|
|
110
|
-
const sessionDescription = description ??
|
|
119
|
+
const sessionDescription = description ?? defaultSessionDescription(cli);
|
|
111
120
|
const session = {
|
|
112
121
|
id,
|
|
113
122
|
cli,
|
|
@@ -59,6 +59,17 @@ export const ACP_ENTRYPOINT_CONTRACTS = {
|
|
|
59
59
|
evidence: "agy 1.0.7 has no ACP flag or subcommand. Legacy Gemini CLI ACP evidence does not transfer. Watchlist item.",
|
|
60
60
|
docsRef: "docs/plans/first-class-acp-gateway-extension.dag.toml#provider_matrix.gemini",
|
|
61
61
|
},
|
|
62
|
+
devin: {
|
|
63
|
+
cli: "devin",
|
|
64
|
+
displayName: "Cognition Devin CLI",
|
|
65
|
+
status: "native",
|
|
66
|
+
executable: "devin",
|
|
67
|
+
entrypointArgs: ["acp"],
|
|
68
|
+
targetVersion: "devin 2026.5.26-8 (1a388fa9)",
|
|
69
|
+
probeArgs: [["--version"]],
|
|
70
|
+
evidence: 'Native ACP entrypoint `devin acp` (stdio JSON-RPC). Slice D1 manual initialize + session/new smoke passed (protocolVersion 1, agent "Affogato", session created). Third native runtime pilot; routing stays config-gated.',
|
|
71
|
+
docsRef: "docs/plans/devin-integration-scoping.md",
|
|
72
|
+
},
|
|
62
73
|
};
|
|
63
74
|
const PERMISSION_MODES = [
|
|
64
75
|
"default",
|
|
@@ -1412,8 +1423,7 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
1412
1423
|
},
|
|
1413
1424
|
"--agent": {
|
|
1414
1425
|
arity: "one",
|
|
1415
|
-
|
|
1416
|
-
description: "Agent/permission mode",
|
|
1426
|
+
description: "Agent/permission mode (builtin, install-gated, or custom agent name)",
|
|
1417
1427
|
},
|
|
1418
1428
|
"--enabled-tools": { arity: "one", description: "Enabled tool" },
|
|
1419
1429
|
"--resume": {
|
|
@@ -1449,6 +1459,7 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
1449
1459
|
description: "Additional writable workspace directory (Phase 4 slice ζ; repeat once per directory)",
|
|
1450
1460
|
},
|
|
1451
1461
|
},
|
|
1462
|
+
acknowledgedUpstreamFlags: ["--auto-approve"],
|
|
1452
1463
|
env: {
|
|
1453
1464
|
VIBE_ACTIVE_MODEL: {
|
|
1454
1465
|
arity: "one",
|
|
@@ -1561,6 +1572,103 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
1561
1572
|
},
|
|
1562
1573
|
],
|
|
1563
1574
|
},
|
|
1575
|
+
devin: {
|
|
1576
|
+
cli: "devin",
|
|
1577
|
+
executable: "devin",
|
|
1578
|
+
upstream: "Cognition Devin CLI",
|
|
1579
|
+
upstreamMetadata: {
|
|
1580
|
+
sourceUrls: ["https://cli.devin.ai/docs/reference/commands", "https://docs.devin.ai/cli"],
|
|
1581
|
+
packageName: "devin",
|
|
1582
|
+
installDocsUrl: "https://docs.devin.ai/cli",
|
|
1583
|
+
releaseChannel: "vendor",
|
|
1584
|
+
watchCategories: ["flags", "subcommands", "permission-modes", "acp-entrypoint"],
|
|
1585
|
+
},
|
|
1586
|
+
helpArgs: [["--help"]],
|
|
1587
|
+
subcommands: {},
|
|
1588
|
+
maxPositionals: 0,
|
|
1589
|
+
mcpTools: ["devin_request", "devin_request_async"],
|
|
1590
|
+
mcpParameters: [
|
|
1591
|
+
"prompt",
|
|
1592
|
+
"model",
|
|
1593
|
+
"permissionMode",
|
|
1594
|
+
"sessionId",
|
|
1595
|
+
"resumeLatest",
|
|
1596
|
+
"createNewSession",
|
|
1597
|
+
"promptFile",
|
|
1598
|
+
],
|
|
1599
|
+
flags: {
|
|
1600
|
+
"-p": {
|
|
1601
|
+
arity: "one",
|
|
1602
|
+
description: "Print response and exit (non-interactive); prompt value",
|
|
1603
|
+
},
|
|
1604
|
+
"--model": { arity: "one", description: "AI model for this session" },
|
|
1605
|
+
"--permission-mode": {
|
|
1606
|
+
arity: "one",
|
|
1607
|
+
values: ["normal", "auto", "dangerous", "yolo", "bypass"],
|
|
1608
|
+
description: "Permission mode (normal/auto = read-only auto-approve; dangerous/yolo/bypass = approve all)",
|
|
1609
|
+
},
|
|
1610
|
+
"--prompt-file": { arity: "one", description: "Load the initial prompt from a file" },
|
|
1611
|
+
"--resume": { arity: "one", description: "Resume a specific session by ID" },
|
|
1612
|
+
"--continue": { arity: "none", description: "Resume the most recent session in cwd" },
|
|
1613
|
+
},
|
|
1614
|
+
env: {},
|
|
1615
|
+
conformanceFixtures: [
|
|
1616
|
+
{
|
|
1617
|
+
id: "devin-minimal",
|
|
1618
|
+
description: "Minimal print-mode prompt request",
|
|
1619
|
+
args: ["-p", "hello"],
|
|
1620
|
+
expect: "pass",
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
id: "devin-unsupported-flag",
|
|
1624
|
+
description: "Unsupported flag is rejected before spawn",
|
|
1625
|
+
args: ["-p", "hello", "--not-a-devin-flag"],
|
|
1626
|
+
expect: "fail",
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
id: "devin-model",
|
|
1630
|
+
description: "--model is accepted",
|
|
1631
|
+
args: ["-p", "hello", "--model", "opus"],
|
|
1632
|
+
expect: "pass",
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
id: "devin-permission-mode",
|
|
1636
|
+
description: "Valid --permission-mode accepted (dangerous alias bypass)",
|
|
1637
|
+
args: ["-p", "hello", "--permission-mode", "bypass"],
|
|
1638
|
+
expect: "pass",
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
id: "devin-permission-mode-auto",
|
|
1642
|
+
description: "Valid --permission-mode alias 'auto' (= normal) accepted",
|
|
1643
|
+
args: ["-p", "hello", "--permission-mode", "auto"],
|
|
1644
|
+
expect: "pass",
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
id: "devin-permission-mode-yolo",
|
|
1648
|
+
description: "Valid --permission-mode alias 'yolo' (= dangerous) accepted",
|
|
1649
|
+
args: ["-p", "hello", "--permission-mode", "yolo"],
|
|
1650
|
+
expect: "pass",
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
id: "devin-permission-mode-invalid",
|
|
1654
|
+
description: "Invalid --permission-mode value rejected by contract",
|
|
1655
|
+
args: ["-p", "hello", "--permission-mode", "ludicrous"],
|
|
1656
|
+
expect: "fail",
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
id: "devin-prompt-file",
|
|
1660
|
+
description: "--prompt-file is accepted",
|
|
1661
|
+
args: ["-p", "hello", "--prompt-file", "/tmp/prompt.txt"],
|
|
1662
|
+
expect: "pass",
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
id: "devin-resume",
|
|
1666
|
+
description: "Resume by session id accepted",
|
|
1667
|
+
args: ["-p", "hello", "--resume", "abc12345"],
|
|
1668
|
+
expect: "pass",
|
|
1669
|
+
},
|
|
1670
|
+
],
|
|
1671
|
+
},
|
|
1564
1672
|
};
|
|
1565
1673
|
export function validateUpstreamCliArgs(cli, args) {
|
|
1566
1674
|
const contract = UPSTREAM_CLI_CONTRACTS[cli];
|
|
@@ -1936,6 +2044,8 @@ export function extractDiscoveredFlags(helpText) {
|
|
|
1936
2044
|
const name = `--${match[1].toLowerCase().replace(/_/g, "-")}`;
|
|
1937
2045
|
if (name === "--help")
|
|
1938
2046
|
continue;
|
|
2047
|
+
if (name.endsWith("-"))
|
|
2048
|
+
continue;
|
|
1939
2049
|
discovered.add(name);
|
|
1940
2050
|
}
|
|
1941
2051
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AsyncJobResult, AsyncJobSnapshot } from "./async-job-manager.js";
|
|
2
|
-
export type ValidationProvider =
|
|
1
|
+
import type { AsyncJobResult, AsyncJobSnapshot, JobProvider } from "./async-job-manager.js";
|
|
2
|
+
export type ValidationProvider = JobProvider;
|
|
3
3
|
export type NormalizedValidationStatus = "running" | "completed" | "failed" | "canceled" | "orphaned" | "skipped";
|
|
4
4
|
export interface RawJobReference {
|
|
5
5
|
jobId: string;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { AsyncJobManager } from "./async-job-manager.js";
|
|
2
2
|
import { type ProviderRuntimeStatus } from "./provider-status.js";
|
|
3
|
+
import type { ApiProviderRuntime } from "./config.js";
|
|
3
4
|
import { type NormalizedValidationResult, type ValidationProvider } from "./validation-normalizer.js";
|
|
4
5
|
import { type ValidationReport } from "./validation-report.js";
|
|
5
6
|
import { type ValidationIntent } from "./validation-prompts.js";
|
|
6
7
|
export interface ValidationOrchestratorDeps {
|
|
7
8
|
asyncJobManager: AsyncJobManager;
|
|
8
9
|
getProviderRuntimeStatus?: (provider: ValidationProvider) => ProviderRuntimeStatus;
|
|
10
|
+
apiProviders?: ApiProviderRuntime[];
|
|
9
11
|
}
|
|
10
12
|
export interface StartValidationInput {
|
|
11
13
|
intent: ValidationIntent;
|
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { getProviderRuntimeStatus } from "./provider-status.js";
|
|
3
|
+
import { createApiProvider } from "./api-provider.js";
|
|
4
|
+
import { prepareApiRequest } from "./api-request.js";
|
|
3
5
|
import { normalizeJobResult, normalizeSkippedProvider, normalizeStartedJob, } from "./validation-normalizer.js";
|
|
4
6
|
import { buildValidationReport } from "./validation-report.js";
|
|
5
7
|
import { buildJudgePrompt, buildValidationPrompt, } from "./validation-prompts.js";
|
|
8
|
+
function findApiReviewer(deps, provider) {
|
|
9
|
+
return deps.apiProviders?.find(p => p.name === provider) ?? null;
|
|
10
|
+
}
|
|
11
|
+
function resolveReviewerStatus(deps, provider) {
|
|
12
|
+
const api = findApiReviewer(deps, provider);
|
|
13
|
+
if (api) {
|
|
14
|
+
return { installed: true, version: null, loginStatus: "authenticated", displayName: api.name };
|
|
15
|
+
}
|
|
16
|
+
const runtimeStatus = deps.getProviderRuntimeStatus ?? getProviderRuntimeStatus;
|
|
17
|
+
return runtimeStatus(provider);
|
|
18
|
+
}
|
|
19
|
+
function dispatchProviderJob(deps, provider, prompt, correlationId) {
|
|
20
|
+
const api = findApiReviewer(deps, provider);
|
|
21
|
+
if (api) {
|
|
22
|
+
const apiProvider = createApiProvider(api.name, api.kind);
|
|
23
|
+
const apiRequest = prepareApiRequest(api, { prompt });
|
|
24
|
+
return deps.asyncJobManager.startHttpJob({ provider: apiProvider, apiRequest, correlationId })
|
|
25
|
+
.snapshot;
|
|
26
|
+
}
|
|
27
|
+
return deps.asyncJobManager.startJob(provider, buildProviderArgs(provider, prompt), correlationId);
|
|
28
|
+
}
|
|
6
29
|
export function startValidationRun(deps, input) {
|
|
7
30
|
const validationId = randomUUID();
|
|
8
31
|
const startedAt = new Date().toISOString();
|
|
@@ -67,8 +90,7 @@ export function startJudgeSynthesis(deps, input) {
|
|
|
67
90
|
note: "Judge synthesis requires at least one completed provider result; skipped, failed, canceled, or orphaned results are preserved in the report but are not judge evidence.",
|
|
68
91
|
};
|
|
69
92
|
}
|
|
70
|
-
const
|
|
71
|
-
const runtime = runtimeStatus(input.judgeProvider);
|
|
93
|
+
const runtime = resolveReviewerStatus(deps, input.judgeProvider);
|
|
72
94
|
if (!runtime.installed) {
|
|
73
95
|
return {
|
|
74
96
|
status: "skipped",
|
|
@@ -77,10 +99,10 @@ export function startJudgeSynthesis(deps, input) {
|
|
|
77
99
|
note: `${runtime.displayName} was selected as judge but is not installed.`,
|
|
78
100
|
};
|
|
79
101
|
}
|
|
80
|
-
const snapshot = deps
|
|
102
|
+
const snapshot = dispatchProviderJob(deps, input.judgeProvider, buildJudgePrompt({
|
|
81
103
|
question: input.question,
|
|
82
104
|
providerResults: completedResults,
|
|
83
|
-
})
|
|
105
|
+
}), `validation-judge-${randomUUID()}-${input.judgeProvider}`);
|
|
84
106
|
return {
|
|
85
107
|
status: "running",
|
|
86
108
|
judgeModel: input.judgeProvider,
|
|
@@ -102,15 +124,14 @@ export function collectValidationJobResult(deps, provider, jobId, model, maxChar
|
|
|
102
124
|
return normalizeJobResult(provider, model, result);
|
|
103
125
|
}
|
|
104
126
|
function startProviderJob(deps, provider, prompt, validationId) {
|
|
105
|
-
const
|
|
106
|
-
const runtime = runtimeStatus(provider);
|
|
127
|
+
const runtime = resolveReviewerStatus(deps, provider);
|
|
107
128
|
if (!runtime.installed) {
|
|
108
129
|
return normalizeSkippedProvider(provider, `${runtime.displayName} runtime is not installed.`);
|
|
109
130
|
}
|
|
110
131
|
const warning = runtime.loginStatus === "authenticated"
|
|
111
132
|
? undefined
|
|
112
133
|
: `${runtime.displayName} login status is ${runtime.loginStatus}; the job may fail until login is complete.`;
|
|
113
|
-
const snapshot = deps
|
|
134
|
+
const snapshot = dispatchProviderJob(deps, provider, prompt, `validation-${validationId}-${provider}`);
|
|
114
135
|
return normalizeStartedJob(provider, runtime.version, snapshot, warning);
|
|
115
136
|
}
|
|
116
137
|
function plannedJudgeSynthesis(input) {
|
|
@@ -1,7 +1,68 @@
|
|
|
1
|
+
import { z } from "zod/v3";
|
|
1
2
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import type { AsyncJobManager } from "./async-job-manager.js";
|
|
3
4
|
import { type ValidationOrchestratorDeps } from "./validation-orchestrator.js";
|
|
4
5
|
export interface ValidationToolDeps extends ValidationOrchestratorDeps {
|
|
5
6
|
asyncJobManager: AsyncJobManager;
|
|
6
7
|
}
|
|
8
|
+
export declare function buildValidationSchemas(deps: ValidationToolDeps): {
|
|
9
|
+
providerSchema: z.ZodEnum<[string, ...string[]]>;
|
|
10
|
+
providerListSchema: z.ZodDefault<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
|
|
11
|
+
normalizedProviderResultSchema: z.ZodObject<{
|
|
12
|
+
provider: z.ZodEnum<[string, ...string[]]>;
|
|
13
|
+
model: z.ZodNullable<z.ZodString>;
|
|
14
|
+
status: z.ZodEnum<["running", "completed", "failed", "canceled", "orphaned", "skipped"]>;
|
|
15
|
+
verdict: z.ZodNullable<z.ZodString>;
|
|
16
|
+
rationale: z.ZodNullable<z.ZodString>;
|
|
17
|
+
risks: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
18
|
+
rawJobReference: z.ZodNullable<z.ZodObject<{
|
|
19
|
+
jobId: z.ZodString;
|
|
20
|
+
correlationId: z.ZodString;
|
|
21
|
+
statusTool: z.ZodLiteral<"job_status">;
|
|
22
|
+
resultTool: z.ZodLiteral<"job_result">;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
correlationId: string;
|
|
25
|
+
jobId: string;
|
|
26
|
+
statusTool: "job_status";
|
|
27
|
+
resultTool: "job_result";
|
|
28
|
+
}, {
|
|
29
|
+
correlationId: string;
|
|
30
|
+
jobId: string;
|
|
31
|
+
statusTool: "job_status";
|
|
32
|
+
resultTool: "job_result";
|
|
33
|
+
}>>;
|
|
34
|
+
error: z.ZodNullable<z.ZodString>;
|
|
35
|
+
warning: z.ZodOptional<z.ZodString>;
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
error: string | null;
|
|
38
|
+
status: "running" | "completed" | "failed" | "canceled" | "orphaned" | "skipped";
|
|
39
|
+
model: string | null;
|
|
40
|
+
provider: string;
|
|
41
|
+
rawJobReference: {
|
|
42
|
+
correlationId: string;
|
|
43
|
+
jobId: string;
|
|
44
|
+
statusTool: "job_status";
|
|
45
|
+
resultTool: "job_result";
|
|
46
|
+
} | null;
|
|
47
|
+
verdict: string | null;
|
|
48
|
+
rationale: string | null;
|
|
49
|
+
risks: string[];
|
|
50
|
+
warning?: string | undefined;
|
|
51
|
+
}, {
|
|
52
|
+
error: string | null;
|
|
53
|
+
status: "running" | "completed" | "failed" | "canceled" | "orphaned" | "skipped";
|
|
54
|
+
model: string | null;
|
|
55
|
+
provider: string;
|
|
56
|
+
rawJobReference: {
|
|
57
|
+
correlationId: string;
|
|
58
|
+
jobId: string;
|
|
59
|
+
statusTool: "job_status";
|
|
60
|
+
resultTool: "job_result";
|
|
61
|
+
} | null;
|
|
62
|
+
verdict: string | null;
|
|
63
|
+
rationale: string | null;
|
|
64
|
+
risks?: string[] | undefined;
|
|
65
|
+
warning?: string | undefined;
|
|
66
|
+
}>;
|
|
67
|
+
};
|
|
7
68
|
export declare function registerValidationTools(server: McpServer, deps: ValidationToolDeps): void;
|
package/dist/validation-tools.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import { z } from "zod/v3";
|
|
2
|
+
import { CLI_TYPES } from "./session-manager.js";
|
|
2
3
|
import { getAvailableCliInfo } from "./model-registry.js";
|
|
4
|
+
import { apiProviderCatalogEntry } from "./api-request.js";
|
|
3
5
|
import { collectValidationJobResult, startJudgeSynthesis, startValidationRun, } from "./validation-orchestrator.js";
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
6
|
+
export function buildValidationSchemas(deps) {
|
|
7
|
+
const apiNames = (deps.apiProviders ?? []).map(p => p.name);
|
|
8
|
+
const allowed = [...CLI_TYPES, ...apiNames];
|
|
9
|
+
const providerSchema = z.enum(allowed);
|
|
10
|
+
const providerListSchema = z.array(providerSchema).min(1).default(["claude", "codex"]);
|
|
11
|
+
const normalizedProviderResultSchema = z.object({
|
|
12
|
+
provider: providerSchema,
|
|
13
|
+
model: z.string().nullable(),
|
|
14
|
+
status: z.enum(["running", "completed", "failed", "canceled", "orphaned", "skipped"]),
|
|
15
|
+
verdict: z.string().nullable(),
|
|
16
|
+
rationale: z.string().nullable(),
|
|
17
|
+
risks: z.array(z.string()).default([]),
|
|
18
|
+
rawJobReference: z
|
|
19
|
+
.object({
|
|
20
|
+
jobId: z.string(),
|
|
21
|
+
correlationId: z.string(),
|
|
22
|
+
statusTool: z.literal("job_status"),
|
|
23
|
+
resultTool: z.literal("job_result"),
|
|
24
|
+
})
|
|
25
|
+
.nullable(),
|
|
26
|
+
error: z.string().nullable(),
|
|
27
|
+
warning: z.string().optional(),
|
|
28
|
+
});
|
|
29
|
+
return { providerSchema, providerListSchema, normalizedProviderResultSchema };
|
|
30
|
+
}
|
|
24
31
|
function textResponse(body) {
|
|
25
32
|
const text = responseText(body);
|
|
26
33
|
return {
|
|
@@ -47,6 +54,7 @@ function findHumanReadableReport(value) {
|
|
|
47
54
|
return null;
|
|
48
55
|
}
|
|
49
56
|
export function registerValidationTools(server, deps) {
|
|
57
|
+
const { providerSchema, providerListSchema, normalizedProviderResultSchema } = buildValidationSchemas(deps);
|
|
50
58
|
server.tool("validate_with_models", "Ask two or more provider CLIs to independently validate a question. Starts validation jobs — poll with job_status, collect with job_result (not llm_job_*).", {
|
|
51
59
|
question: z.string().min(1).describe("Question or content to validate."),
|
|
52
60
|
models: providerListSchema.describe("Providers to ask. Defaults to Claude and Codex."),
|
|
@@ -208,7 +216,14 @@ export function registerValidationTools(server, deps) {
|
|
|
208
216
|
destructiveHint: false,
|
|
209
217
|
idempotentHint: true,
|
|
210
218
|
openWorldHint: false,
|
|
211
|
-
}, async () =>
|
|
219
|
+
}, async () => {
|
|
220
|
+
const apiProviders = (deps.apiProviders ?? []).map(apiProviderCatalogEntry);
|
|
221
|
+
return textResponse({
|
|
222
|
+
success: true,
|
|
223
|
+
models: getAvailableCliInfo(),
|
|
224
|
+
...(apiProviders.length > 0 ? { apiProviders } : {}),
|
|
225
|
+
});
|
|
226
|
+
});
|
|
212
227
|
server.tool("job_status", "Check a VALIDATION job's status (jobs started by validate_with_models/ask_model/etc.) — distinct from llm_job_status, which tracks provider request jobs.", {
|
|
213
228
|
jobId: z.string().min(1).describe("Validation job ID."),
|
|
214
229
|
}, {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Slice 0.5 (API-endpoint routing, locked decision B: arbitrary provider names).
|
|
2
|
+
--
|
|
3
|
+
-- Relax the closed provider enum on the session tables so that any
|
|
4
|
+
-- `[providers.<name>]` config key (a kind:"api" provider id) is a valid
|
|
5
|
+
-- `cli` value. Migration 003 widened the constraint only as far as the
|
|
6
|
+
-- hard-coded set ('claude','codex','gemini','grok','mistral','grok-api');
|
|
7
|
+
-- arbitrary API provider names had no DB-level home.
|
|
8
|
+
--
|
|
9
|
+
-- Provider-set validation now lives in the application layer (config loading
|
|
10
|
+
-- plus `SESSION_PROVIDER_ENUM` for the registered set). The database keeps a
|
|
11
|
+
-- single *format* guard so empty strings / whitespace / control characters are
|
|
12
|
+
-- still rejected — it no longer enumerates a fixed provider list. The pattern
|
|
13
|
+
-- accepts the existing five CLIs and `grok-api`, and any well-formed provider
|
|
14
|
+
-- identifier (e.g. `ollama`, `openai`, `vllm`, `llama3.3`).
|
|
15
|
+
|
|
16
|
+
ALTER TABLE sessions DROP CONSTRAINT IF EXISTS sessions_cli_check;
|
|
17
|
+
ALTER TABLE sessions
|
|
18
|
+
ADD CONSTRAINT sessions_cli_check
|
|
19
|
+
CHECK (cli ~ '^[A-Za-z][A-Za-z0-9._-]*$');
|
|
20
|
+
|
|
21
|
+
ALTER TABLE active_sessions DROP CONSTRAINT IF EXISTS active_sessions_cli_check;
|
|
22
|
+
ALTER TABLE active_sessions
|
|
23
|
+
ADD CONSTRAINT active_sessions_cli_check
|
|
24
|
+
CHECK (cli ~ '^[A-Za-z][A-Za-z0-9._-]*$');
|
|
25
|
+
|
|
26
|
+
INSERT INTO schema_migrations (version, name)
|
|
27
|
+
VALUES (5, '005_provider_type_open_api_names')
|
|
28
|
+
ON CONFLICT (version) DO NOTHING;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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",
|
|
13
|
+
"body-parser": "2.2.2",
|
|
13
14
|
"content-type": "1.0.5",
|
|
14
15
|
"smol-toml": "^1.6.1",
|
|
15
16
|
"type-is": "2.0.1",
|
|
@@ -587,9 +588,9 @@
|
|
|
587
588
|
}
|
|
588
589
|
},
|
|
589
590
|
"node_modules/hono": {
|
|
590
|
-
"version": "4.12.
|
|
591
|
-
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.
|
|
592
|
-
"integrity": "sha512-
|
|
591
|
+
"version": "4.12.25",
|
|
592
|
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
|
|
593
|
+
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==",
|
|
593
594
|
"license": "MIT",
|
|
594
595
|
"engines": {
|
|
595
596
|
"node": ">=16.9.0"
|