@opengeni/runtime 0.11.0 → 0.13.3
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/dist/{chunk-YUAFQEFY.js → chunk-JGUQSAG5.js} +2605 -279
- package/dist/chunk-JGUQSAG5.js.map +1 -0
- package/dist/chunk-SSB7VOLE.js +301 -0
- package/dist/chunk-SSB7VOLE.js.map +1 -0
- package/dist/{index-D6yi-JcU.d.ts → index-DU-KXVlx.d.ts} +364 -31
- package/dist/index.d.ts +103 -10
- package/dist/index.js +756 -761
- package/dist/index.js.map +1 -1
- package/dist/mcp-network.d.ts +80 -0
- package/dist/mcp-network.js +66 -0
- package/dist/mcp-network.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +39 -1
- package/package.json +9 -4
- package/src/codex-tool-search.ts +20 -1
- package/src/index.ts +502 -85
- package/src/mcp-network.ts +343 -0
- package/src/sandbox/channel-a.ts +92 -71
- package/src/sandbox/display-stack.ts +860 -23
- package/src/sandbox/exec-banner.ts +82 -0
- package/src/sandbox/index.ts +367 -145
- package/src/sandbox/provider-errors.ts +175 -0
- package/src/sandbox/providers/modal.ts +29 -2
- package/src/sandbox/routing/backend-resolver.ts +15 -0
- package/src/sandbox/routing/routing-session.ts +730 -36
- package/src/sandbox/select.ts +8 -0
- package/src/sandbox/turn-tool-cancellation.ts +366 -92
- package/src/sandbox/workspace-archive.ts +289 -0
- package/src/sandbox-computer.ts +3 -1
- package/src/usage-telemetry.ts +495 -51
- package/dist/chunk-YUAFQEFY.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
approvalIdentifier,
|
|
16
16
|
CAPABILITY_DESCRIPTORS,
|
|
17
|
+
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
17
18
|
assertUniqueResourceMountPaths,
|
|
18
19
|
gitCredentialBindingIdForRepository,
|
|
19
20
|
gitCredentialProviderForRepository,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
signDelegatedAccessToken,
|
|
28
29
|
RequestHumanInputToolInput,
|
|
29
30
|
type GitCredentialProvider,
|
|
31
|
+
type GitCredentialTransport,
|
|
30
32
|
type HumanInputResponse,
|
|
31
33
|
type McpServerConnectionRef,
|
|
32
34
|
type Permission,
|
|
@@ -37,6 +39,19 @@ import {
|
|
|
37
39
|
type ToolAuthNeededPayload,
|
|
38
40
|
type ToolRef,
|
|
39
41
|
} from "@opengeni/contracts";
|
|
42
|
+
import {
|
|
43
|
+
MCP_MAX_CONCURRENT_SERVER_OPERATIONS,
|
|
44
|
+
MCP_MAX_TOOL_RESULT_BYTES,
|
|
45
|
+
McpAggregateToolListBudget,
|
|
46
|
+
assertMcpPayloadWithinBytes,
|
|
47
|
+
assertMcpServerSelectionWithinBounds,
|
|
48
|
+
assertMcpToolListWithinBounds,
|
|
49
|
+
boundedParallelMap,
|
|
50
|
+
cancelMcpResponseBody,
|
|
51
|
+
guardedMcpFetch,
|
|
52
|
+
mcpOuterConnectTimeoutMs,
|
|
53
|
+
undiciFetch,
|
|
54
|
+
} from "./mcp-network";
|
|
40
55
|
import {
|
|
41
56
|
Agent,
|
|
42
57
|
AgentsError,
|
|
@@ -117,6 +132,7 @@ import {
|
|
|
117
132
|
CODEX_APPS_MCP_SERVER_ID,
|
|
118
133
|
CODEX_MODEL_ID_PREFIX,
|
|
119
134
|
CODEX_ORIGINATOR,
|
|
135
|
+
CODEX_RESPONSE_SDK_OUTER_TIMEOUT_MS,
|
|
120
136
|
boundModelToolOutputItems,
|
|
121
137
|
codexAppsSanitizingFetch,
|
|
122
138
|
codexRequestStorage,
|
|
@@ -181,7 +197,16 @@ export {
|
|
|
181
197
|
} from "./skill-library";
|
|
182
198
|
|
|
183
199
|
export type { RuntimeMetricsHooks } from "./metrics";
|
|
184
|
-
export
|
|
200
|
+
export {
|
|
201
|
+
createTurnToolCancellationController,
|
|
202
|
+
TurnSandboxCommandCancelledError,
|
|
203
|
+
} from "./sandbox/turn-tool-cancellation";
|
|
204
|
+
export type {
|
|
205
|
+
TurnSandboxCommandArgs,
|
|
206
|
+
TurnSandboxCommandSession,
|
|
207
|
+
TurnToolCancellationController,
|
|
208
|
+
TurnToolCancellationFence,
|
|
209
|
+
} from "./sandbox/turn-tool-cancellation";
|
|
185
210
|
|
|
186
211
|
// P4.3 computer-use surface (the agent's :0 driver). Re-exported from the barrel
|
|
187
212
|
// so callers (the worker, live proofs) reach SandboxComputer/ComputerUseCapability
|
|
@@ -269,8 +294,17 @@ export type {
|
|
|
269
294
|
CompactionItem,
|
|
270
295
|
PreparedCompactionPromptInput,
|
|
271
296
|
} from "./context-compaction";
|
|
272
|
-
export {
|
|
273
|
-
|
|
297
|
+
export {
|
|
298
|
+
MAX_MODEL_USAGE_TOKEN_COUNT,
|
|
299
|
+
modelCallUsageTelemetry,
|
|
300
|
+
modelUsageTokenCountOrNull,
|
|
301
|
+
normalizeModelCallUsage,
|
|
302
|
+
} from "./usage-telemetry";
|
|
303
|
+
export type {
|
|
304
|
+
ModelCallUsageInput,
|
|
305
|
+
ModelCallUsageNormalization,
|
|
306
|
+
ModelCallUsageTelemetry,
|
|
307
|
+
} from "./usage-telemetry";
|
|
274
308
|
|
|
275
309
|
ensureReadableStreamFrom();
|
|
276
310
|
|
|
@@ -289,6 +323,18 @@ export type ModelResponseUsage = {
|
|
|
289
323
|
totalTokens?: number;
|
|
290
324
|
inputTokensDetails?: Record<string, number> | Array<Record<string, number>>;
|
|
291
325
|
outputTokensDetails?: Record<string, number> | Array<Record<string, number>>;
|
|
326
|
+
requestUsageEntries?: Array<{
|
|
327
|
+
inputTokens?: number;
|
|
328
|
+
input_tokens?: number;
|
|
329
|
+
outputTokens?: number;
|
|
330
|
+
output_tokens?: number;
|
|
331
|
+
totalTokens?: number;
|
|
332
|
+
total_tokens?: number;
|
|
333
|
+
inputTokensDetails?: Record<string, number>;
|
|
334
|
+
input_tokens_details?: Record<string, number>;
|
|
335
|
+
outputTokensDetails?: Record<string, number>;
|
|
336
|
+
output_tokens_details?: Record<string, number>;
|
|
337
|
+
}>;
|
|
292
338
|
};
|
|
293
339
|
};
|
|
294
340
|
|
|
@@ -302,6 +348,8 @@ export type ResolveConnectionCredentialInput = {
|
|
|
302
348
|
serverId: string;
|
|
303
349
|
toolName?: string;
|
|
304
350
|
connectionRef: McpServerConnectionRef;
|
|
351
|
+
/** Exact MCP destination whose request would receive the resolved headers. */
|
|
352
|
+
destinationUrl: string;
|
|
305
353
|
forceRefresh?: boolean;
|
|
306
354
|
};
|
|
307
355
|
|
|
@@ -564,6 +612,10 @@ export function buildProviderClient(provider: ResolvedModelProvider, settings: S
|
|
|
564
612
|
// SDK retries on network/5xx/partial streams can replay provider work
|
|
565
613
|
// or external tool side effects without a durable checkpoint.
|
|
566
614
|
maxRetries: 0,
|
|
615
|
+
// The Codex transport owns finer headers/idle/whole deadlines and
|
|
616
|
+
// emits typed durable evidence. Keep the SDK's opaque envelope beyond
|
|
617
|
+
// that whole-response budget so `Request timed out.` cannot win first.
|
|
618
|
+
timeout: CODEX_RESPONSE_SDK_OUTER_TIMEOUT_MS,
|
|
567
619
|
fetch: codexSubscriptionFetch(instrumentedModelFetch(provider.id, globalThis.fetch)),
|
|
568
620
|
})
|
|
569
621
|
: // ResolvedModelProvider.apiKey is already the resolved key (configuredProviders
|
|
@@ -625,7 +677,7 @@ export function resolveTurnModel(
|
|
|
625
677
|
return {
|
|
626
678
|
provider: resolved.provider,
|
|
627
679
|
client,
|
|
628
|
-
model: buildModelInstance(resolved.provider, client, resolved.model.
|
|
680
|
+
model: buildModelInstance(resolved.provider, client, resolved.model.upstreamModelId),
|
|
629
681
|
configured: resolved.model,
|
|
630
682
|
};
|
|
631
683
|
}
|
|
@@ -1188,6 +1240,7 @@ export type GitCredentialBindingSeed = {
|
|
|
1188
1240
|
credentialBindingId: string;
|
|
1189
1241
|
provider: GitCredentialProvider;
|
|
1190
1242
|
token: string;
|
|
1243
|
+
transport?: GitCredentialTransport;
|
|
1191
1244
|
expiresAt?: string;
|
|
1192
1245
|
/** Total active bindings for this provider, used to suppress unsafe aliases. */
|
|
1193
1246
|
providerBindingCount?: number;
|
|
@@ -1907,14 +1960,17 @@ function maybeInstallCodexToolSearch(
|
|
|
1907
1960
|
}
|
|
1908
1961
|
|
|
1909
1962
|
/** True when the unprefixed tool `name` requires approval under `policy`. */
|
|
1910
|
-
function mcpToolRequiresApproval(
|
|
1911
|
-
|
|
1963
|
+
function mcpToolRequiresApproval(
|
|
1964
|
+
policy: boolean | ReadonlySet<string>,
|
|
1965
|
+
unprefixedName: string,
|
|
1966
|
+
): boolean {
|
|
1967
|
+
return policy === true || (policy !== false && policy.has(unprefixedName));
|
|
1912
1968
|
}
|
|
1913
1969
|
|
|
1914
1970
|
/** A per-server approval policy keyed by the server's `<id>__` tool prefix. */
|
|
1915
1971
|
type McpApprovalPolicy = {
|
|
1916
1972
|
prefix: string;
|
|
1917
|
-
requireApproval: boolean | string
|
|
1973
|
+
requireApproval: boolean | ReadonlySet<string>;
|
|
1918
1974
|
};
|
|
1919
1975
|
|
|
1920
1976
|
/** The subset of the agent surface the approval wrap needs — including `clone`. */
|
|
@@ -2005,7 +2061,8 @@ function applyMcpApprovalPolicy(agent: Agent<any, any>, settings: Settings): voi
|
|
|
2005
2061
|
)
|
|
2006
2062
|
.map((server) => ({
|
|
2007
2063
|
prefix: prefixedMcpToolName(server.id, ""),
|
|
2008
|
-
requireApproval:
|
|
2064
|
+
requireApproval:
|
|
2065
|
+
server.requireApproval === true ? true : new Set(server.requireApproval as string[]),
|
|
2009
2066
|
}))
|
|
2010
2067
|
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
2011
2068
|
if (policies.length === 0) {
|
|
@@ -2266,8 +2323,76 @@ export type PrepareToolsOptions = {
|
|
|
2266
2323
|
input: ResolveConnectionCredentialInput,
|
|
2267
2324
|
) => Promise<ResolveConnectionCredentialResult>;
|
|
2268
2325
|
onAuthNeeded?: (payload: ToolAuthNeededPayload) => Promise<void> | void;
|
|
2326
|
+
/** Injectable final MCP transport for tests and embedded hosts. */
|
|
2327
|
+
mcpFetchImpl?: FetchLike;
|
|
2269
2328
|
};
|
|
2270
2329
|
|
|
2330
|
+
type ConnectedMcpServerBatch = Awaited<ReturnType<typeof connectMcpServers>>;
|
|
2331
|
+
|
|
2332
|
+
export type ConnectedMcpServerBatches = {
|
|
2333
|
+
active: MCPServer[];
|
|
2334
|
+
failed: MCPServer[];
|
|
2335
|
+
errors: ReadonlyMap<MCPServer, Error>;
|
|
2336
|
+
close: () => Promise<void>;
|
|
2337
|
+
};
|
|
2338
|
+
|
|
2339
|
+
/**
|
|
2340
|
+
* Connect SDK-managed MCP servers in stable, bounded batches. The SDK cleans a
|
|
2341
|
+
* failing strict batch; this wrapper additionally closes every earlier batch
|
|
2342
|
+
* before rethrowing, so a later-batch failure cannot leak live connections.
|
|
2343
|
+
*/
|
|
2344
|
+
export async function connectMcpServersInBatches(
|
|
2345
|
+
servers: MCPServer[],
|
|
2346
|
+
options: { strict: boolean; connectTimeoutMs?: number },
|
|
2347
|
+
): Promise<ConnectedMcpServerBatches> {
|
|
2348
|
+
assertMcpServerSelectionWithinBounds(servers);
|
|
2349
|
+
const batches: ConnectedMcpServerBatch[] = [];
|
|
2350
|
+
try {
|
|
2351
|
+
for (let offset = 0; offset < servers.length; offset += MCP_MAX_CONCURRENT_SERVER_OPERATIONS) {
|
|
2352
|
+
batches.push(
|
|
2353
|
+
await connectMcpServers(
|
|
2354
|
+
servers.slice(offset, offset + MCP_MAX_CONCURRENT_SERVER_OPERATIONS),
|
|
2355
|
+
{
|
|
2356
|
+
...(options.connectTimeoutMs === undefined
|
|
2357
|
+
? {}
|
|
2358
|
+
: { connectTimeoutMs: options.connectTimeoutMs }),
|
|
2359
|
+
connectInParallel: true,
|
|
2360
|
+
strict: options.strict,
|
|
2361
|
+
},
|
|
2362
|
+
),
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
await closeMcpServerBatches(batches).catch(() => undefined);
|
|
2367
|
+
throw error;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
const errors = new Map<MCPServer, Error>();
|
|
2371
|
+
for (const batch of batches) {
|
|
2372
|
+
for (const [server, error] of batch.errors) errors.set(server, error);
|
|
2373
|
+
}
|
|
2374
|
+
return {
|
|
2375
|
+
active: batches.flatMap((batch) => batch.active),
|
|
2376
|
+
failed: batches.flatMap((batch) => batch.failed),
|
|
2377
|
+
errors,
|
|
2378
|
+
close: async () => {
|
|
2379
|
+
await closeMcpServerBatches(batches);
|
|
2380
|
+
},
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
async function closeMcpServerBatches(batches: ConnectedMcpServerBatch[]): Promise<void> {
|
|
2385
|
+
let firstError: unknown;
|
|
2386
|
+
for (const batch of [...batches].reverse()) {
|
|
2387
|
+
try {
|
|
2388
|
+
await batch.close();
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
firstError ??= error;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (firstError !== undefined) throw firstError;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2271
2396
|
export async function prepareAgentTools(
|
|
2272
2397
|
settings: Settings,
|
|
2273
2398
|
tools: ToolRef[],
|
|
@@ -2277,25 +2402,36 @@ export async function prepareAgentTools(
|
|
|
2277
2402
|
// codex_apps sanitizing fetch so every tools/list this turn accumulates the
|
|
2278
2403
|
// account's connector namespaces. Surfaced on PreparedAgentTools for the worker.
|
|
2279
2404
|
const codexConnectorNamespaces = new Set<string>();
|
|
2405
|
+
assertMcpServerSelectionWithinBounds(tools);
|
|
2280
2406
|
if (tools.length === 0) {
|
|
2281
2407
|
return { mcpServers: [], close: async () => {}, codexConnectorNamespaces };
|
|
2282
2408
|
}
|
|
2283
2409
|
const registry = new Map(settings.mcpServers.map((server) => [server.id, server]));
|
|
2284
|
-
const
|
|
2285
|
-
|
|
2410
|
+
const aggregateToolBudget = new McpAggregateToolListBudget();
|
|
2411
|
+
const mcpFetchImpl = options.mcpFetchImpl ?? undiciFetch;
|
|
2412
|
+
const servers = await boundedParallelMap(
|
|
2413
|
+
tools,
|
|
2414
|
+
MCP_MAX_CONCURRENT_SERVER_OPERATIONS,
|
|
2415
|
+
async (tool, index) => {
|
|
2286
2416
|
const config = registry.get(tool.id);
|
|
2287
2417
|
if (!config) {
|
|
2288
2418
|
throw new Error(`Unknown MCP server id: ${tool.id}`);
|
|
2289
2419
|
}
|
|
2290
2420
|
const url = firstPartyMcpServerUrlForRun(settings, config, options.workspaceId) ?? config.url;
|
|
2421
|
+
const firstParty = isFirstPartyMcpServer(settings, config);
|
|
2291
2422
|
const baseFetch = isCodexAppsMcpServer(config)
|
|
2292
|
-
? codexAppsSanitizingFetch(
|
|
2293
|
-
:
|
|
2423
|
+
? codexAppsSanitizingFetch(mcpFetchImpl, codexConnectorNamespaces)
|
|
2424
|
+
: mcpFetchImpl;
|
|
2425
|
+
const guardedFetch = guardedMcpFetch(
|
|
2426
|
+
firstParty ? { ...settings, integrationsAllowPrivateNetworkTargets: true } : settings,
|
|
2427
|
+
baseFetch,
|
|
2428
|
+
firstParty ? { requireHttpsOutsideLocalTest: false } : {},
|
|
2429
|
+
);
|
|
2294
2430
|
const fetchImpl = config.connectionRef
|
|
2295
|
-
? connectionBrokerFetch(
|
|
2296
|
-
:
|
|
2297
|
-
? firstPartyAuthFetch(
|
|
2298
|
-
:
|
|
2431
|
+
? connectionBrokerFetch(guardedFetch, config, options)
|
|
2432
|
+
: firstParty
|
|
2433
|
+
? firstPartyAuthFetch(guardedFetch, settings, options)
|
|
2434
|
+
: guardedFetch;
|
|
2299
2435
|
// A server is connected BEST-EFFORT (a connect OR tools-list failure drops
|
|
2300
2436
|
// it — its tools go unavailable for the turn — instead of failing the turn)
|
|
2301
2437
|
// in two cases:
|
|
@@ -2327,7 +2463,7 @@ export async function prepareAgentTools(
|
|
|
2327
2463
|
// MCP SDK's strict Tool schema rejects (fails the turn during tools/list);
|
|
2328
2464
|
// sanitize the response on the wire before validation. The namespace Set
|
|
2329
2465
|
// also captures each tool's original connector namespace (P4 Part B.1).
|
|
2330
|
-
|
|
2466
|
+
fetch: fetchImpl,
|
|
2331
2467
|
...(await mcpServerRequestInit(settings, config)),
|
|
2332
2468
|
...(config.timeoutMs
|
|
2333
2469
|
? {
|
|
@@ -2339,36 +2475,50 @@ export async function prepareAgentTools(
|
|
|
2339
2475
|
config.id,
|
|
2340
2476
|
config.allowedTools,
|
|
2341
2477
|
bestEffort,
|
|
2478
|
+
aggregateToolBudget,
|
|
2479
|
+
`${config.id}:${index}`,
|
|
2342
2480
|
);
|
|
2343
2481
|
return {
|
|
2344
2482
|
server,
|
|
2345
2483
|
bestEffort,
|
|
2346
2484
|
optional,
|
|
2485
|
+
timeoutMs: config.timeoutMs,
|
|
2347
2486
|
};
|
|
2348
|
-
}
|
|
2487
|
+
},
|
|
2349
2488
|
);
|
|
2350
|
-
const
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2489
|
+
const requiredEntries = servers.filter((entry) => !entry.bestEffort);
|
|
2490
|
+
const bestEffortEntries = servers.filter((entry) => entry.bestEffort);
|
|
2491
|
+
const requiredServers = requiredEntries.map((entry) => entry.server);
|
|
2492
|
+
const bestEffortServers = bestEffortEntries.map((entry) => entry.server);
|
|
2354
2493
|
// Names of the OPTIONAL servers (not codex_apps) so a drop is surfaced as a
|
|
2355
2494
|
// warning; codex_apps keeps its historically-quiet drop (a not-logged-in
|
|
2356
2495
|
// ChatGPT plan is a normal, non-noteworthy state).
|
|
2357
2496
|
const optionalServerNames = new Set(
|
|
2358
2497
|
servers.filter((entry) => entry.optional).map((entry) => entry.server.name),
|
|
2359
2498
|
);
|
|
2360
|
-
const connectedRequired = await
|
|
2361
|
-
connectInParallel: true,
|
|
2499
|
+
const connectedRequired = await connectMcpServersInBatches(requiredServers, {
|
|
2362
2500
|
strict: true,
|
|
2501
|
+
connectTimeoutMs: mcpOuterConnectTimeoutMs(requiredEntries.map((entry) => entry.timeoutMs)),
|
|
2363
2502
|
});
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2503
|
+
let connectedBestEffort: ConnectedMcpServerBatches | null = null;
|
|
2504
|
+
try {
|
|
2505
|
+
connectedBestEffort = bestEffortServers.length
|
|
2506
|
+
? await connectMcpServersInBatches(bestEffortServers, {
|
|
2507
|
+
strict: false,
|
|
2508
|
+
connectTimeoutMs: mcpOuterConnectTimeoutMs(
|
|
2509
|
+
bestEffortEntries.map((entry) => entry.timeoutMs),
|
|
2510
|
+
),
|
|
2511
|
+
})
|
|
2512
|
+
: null;
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
await connectedRequired.close().catch(() => undefined);
|
|
2515
|
+
throw error;
|
|
2516
|
+
}
|
|
2370
2517
|
if (connectedBestEffort) {
|
|
2371
2518
|
for (const failed of connectedBestEffort.failed) {
|
|
2519
|
+
if (failed instanceof PrefixedMcpServer) {
|
|
2520
|
+
failed.releaseAggregateBudget();
|
|
2521
|
+
}
|
|
2372
2522
|
if (!optionalServerNames.has(failed.name)) {
|
|
2373
2523
|
continue;
|
|
2374
2524
|
}
|
|
@@ -2382,10 +2532,20 @@ export async function prepareAgentTools(
|
|
|
2382
2532
|
return {
|
|
2383
2533
|
mcpServers: [...connectedRequired.active, ...(connectedBestEffort?.active ?? [])],
|
|
2384
2534
|
close: async () => {
|
|
2385
|
-
|
|
2535
|
+
let firstError: unknown;
|
|
2386
2536
|
if (connectedBestEffort) {
|
|
2387
|
-
|
|
2537
|
+
try {
|
|
2538
|
+
await connectedBestEffort.close();
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
firstError ??= error;
|
|
2541
|
+
}
|
|
2388
2542
|
}
|
|
2543
|
+
try {
|
|
2544
|
+
await connectedRequired.close();
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
firstError ??= error;
|
|
2547
|
+
}
|
|
2548
|
+
if (firstError !== undefined) throw firstError;
|
|
2389
2549
|
},
|
|
2390
2550
|
codexConnectorNamespaces,
|
|
2391
2551
|
};
|
|
@@ -2402,10 +2562,12 @@ function connectionBrokerFetch(
|
|
|
2402
2562
|
}
|
|
2403
2563
|
return async (input, init) => {
|
|
2404
2564
|
const request = await mcpRequestInfo(input, init);
|
|
2565
|
+
const destinationUrl = mcpRequestDestinationUrl(input);
|
|
2405
2566
|
const first = await resolveConnectionForRequest(
|
|
2406
2567
|
options,
|
|
2407
2568
|
config.id,
|
|
2408
2569
|
connectionRef,
|
|
2570
|
+
destinationUrl,
|
|
2409
2571
|
request.toolName,
|
|
2410
2572
|
false,
|
|
2411
2573
|
);
|
|
@@ -2417,10 +2579,12 @@ function connectionBrokerFetch(
|
|
|
2417
2579
|
withConnectionHeaders(input, init, first.headers),
|
|
2418
2580
|
);
|
|
2419
2581
|
if (response.status === 401) {
|
|
2582
|
+
await cancelMcpResponseBody(response);
|
|
2420
2583
|
const refreshed = await resolveConnectionForRequest(
|
|
2421
2584
|
options,
|
|
2422
2585
|
config.id,
|
|
2423
2586
|
connectionRef,
|
|
2587
|
+
destinationUrl,
|
|
2424
2588
|
request.toolName,
|
|
2425
2589
|
true,
|
|
2426
2590
|
);
|
|
@@ -2433,11 +2597,14 @@ function connectionBrokerFetch(
|
|
|
2433
2597
|
);
|
|
2434
2598
|
if (retry.status === 403) {
|
|
2435
2599
|
const auth = insufficientScopeAuth(retry.headers, connectionRef, refreshed.connectionId);
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2600
|
+
if (auth) {
|
|
2601
|
+
await cancelMcpResponseBody(retry);
|
|
2602
|
+
return await authNeededFetchResponse(options, config.id, request, auth, connectionRef);
|
|
2603
|
+
}
|
|
2604
|
+
return retry;
|
|
2439
2605
|
}
|
|
2440
2606
|
if (retry.status === 401) {
|
|
2607
|
+
await cancelMcpResponseBody(retry);
|
|
2441
2608
|
return await authNeededFetchResponse(
|
|
2442
2609
|
options,
|
|
2443
2610
|
config.id,
|
|
@@ -2461,9 +2628,11 @@ function connectionBrokerFetch(
|
|
|
2461
2628
|
}
|
|
2462
2629
|
if (response.status === 403) {
|
|
2463
2630
|
const auth = insufficientScopeAuth(response.headers, connectionRef, first.connectionId);
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2631
|
+
if (auth) {
|
|
2632
|
+
await cancelMcpResponseBody(response);
|
|
2633
|
+
return await authNeededFetchResponse(options, config.id, request, auth, connectionRef);
|
|
2634
|
+
}
|
|
2635
|
+
return response;
|
|
2467
2636
|
}
|
|
2468
2637
|
return response;
|
|
2469
2638
|
};
|
|
@@ -2473,6 +2642,7 @@ async function resolveConnectionForRequest(
|
|
|
2473
2642
|
options: PrepareToolsOptions,
|
|
2474
2643
|
serverId: string,
|
|
2475
2644
|
connectionRef: McpServerConnectionRef,
|
|
2645
|
+
destinationUrl: string,
|
|
2476
2646
|
toolName: string | undefined,
|
|
2477
2647
|
forceRefresh: boolean,
|
|
2478
2648
|
): Promise<ResolveConnectionCredentialResult> {
|
|
@@ -2494,6 +2664,7 @@ async function resolveConnectionForRequest(
|
|
|
2494
2664
|
workspaceId: options.workspaceId,
|
|
2495
2665
|
serverId,
|
|
2496
2666
|
connectionRef,
|
|
2667
|
+
destinationUrl,
|
|
2497
2668
|
forceRefresh,
|
|
2498
2669
|
...(toolName ? { toolName } : {}),
|
|
2499
2670
|
...(options.subjectId ? { subjectId: options.subjectId } : {}),
|
|
@@ -2610,6 +2781,10 @@ type McpRequestInfo = {
|
|
|
2610
2781
|
toolName?: string;
|
|
2611
2782
|
};
|
|
2612
2783
|
|
|
2784
|
+
function mcpRequestDestinationUrl(input: string | URL | Request): string {
|
|
2785
|
+
return new URL(input instanceof Request ? input.url : input.toString()).toString();
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2613
2788
|
async function mcpRequestInfo(
|
|
2614
2789
|
input: string | URL | Request,
|
|
2615
2790
|
init?: RequestInit,
|
|
@@ -2838,7 +3013,7 @@ async function signFirstPartyDelegatedBearer(
|
|
|
2838
3013
|
workspaceId: options.workspaceId,
|
|
2839
3014
|
subjectId: options.subjectId ?? "worker:first-party-mcp",
|
|
2840
3015
|
...(options.subjectLabel ? { subjectLabel: options.subjectLabel } : {}),
|
|
2841
|
-
permissions: options.firstPartyPermissions ??
|
|
3016
|
+
permissions: options.firstPartyPermissions ?? [...DEFAULT_FIRST_PARTY_MCP_PERMISSIONS],
|
|
2842
3017
|
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
2843
3018
|
...(options.turnId ? { turnId: options.turnId } : {}),
|
|
2844
3019
|
...(options.attemptId ? { attemptId: options.attemptId } : {}),
|
|
@@ -2922,22 +3097,6 @@ async function codexAppsMcpRequestInit(
|
|
|
2922
3097
|
// level scopes (billing/account/members/api_keys/workspace:admin) are
|
|
2923
3098
|
// intentionally excluded: they gate no first-party tool and are not agent
|
|
2924
3099
|
// capabilities. (A finer-grained capability model comes later.)
|
|
2925
|
-
const firstPartyMcpPermissions: Permission[] = [
|
|
2926
|
-
"workspace:read",
|
|
2927
|
-
"files:read",
|
|
2928
|
-
"documents:search",
|
|
2929
|
-
"scheduled_tasks:manage",
|
|
2930
|
-
"scheduled_tasks:run",
|
|
2931
|
-
"goals:manage",
|
|
2932
|
-
"sessions:read",
|
|
2933
|
-
"sessions:create",
|
|
2934
|
-
"sessions:control",
|
|
2935
|
-
"variable-sets:use",
|
|
2936
|
-
"variable-sets:manage",
|
|
2937
|
-
"rigs:use",
|
|
2938
|
-
"github:use",
|
|
2939
|
-
];
|
|
2940
|
-
|
|
2941
3100
|
// codex_apps is third-party-by-trust (the external ChatGPT connectors backend)
|
|
2942
3101
|
// but needs DYNAMIC auth, so it is its own category — deliberately NOT folded
|
|
2943
3102
|
// into the first-party allowlist, which would wrongly sign an OpenGeni delegated
|
|
@@ -3044,6 +3203,8 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3044
3203
|
registryId: string,
|
|
3045
3204
|
allowedTools?: string[],
|
|
3046
3205
|
bestEffort = false,
|
|
3206
|
+
private readonly aggregateToolBudget?: McpAggregateToolListBudget,
|
|
3207
|
+
private readonly aggregateSourceId = registryId,
|
|
3047
3208
|
) {
|
|
3048
3209
|
this.name = registryId;
|
|
3049
3210
|
this.prefix = prefixedMcpToolName(registryId, "");
|
|
@@ -3057,13 +3218,27 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3057
3218
|
}
|
|
3058
3219
|
|
|
3059
3220
|
close(): Promise<void> {
|
|
3221
|
+
this.releaseAggregateBudget();
|
|
3060
3222
|
return this.inner.close();
|
|
3061
3223
|
}
|
|
3062
3224
|
|
|
3225
|
+
releaseAggregateBudget(): void {
|
|
3226
|
+
this.aggregateToolBudget?.remove(this.aggregateSourceId);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3063
3229
|
async listTools(): Promise<RuntimeMcpTool[]> {
|
|
3064
|
-
let tools: RuntimeMcpTool[];
|
|
3065
3230
|
try {
|
|
3066
|
-
tools = await this.inner.listTools();
|
|
3231
|
+
const tools = assertMcpToolListWithinBounds(await this.inner.listTools()) as RuntimeMcpTool[];
|
|
3232
|
+
const exposed = tools
|
|
3233
|
+
.filter((tool) => this.isAllowed(tool.name))
|
|
3234
|
+
.map((tool) => ({
|
|
3235
|
+
...tool,
|
|
3236
|
+
name: prefixedMcpToolName(this.name, tool.name),
|
|
3237
|
+
}));
|
|
3238
|
+
const bounded = (this.aggregateToolBudget?.replace(this.aggregateSourceId, exposed) ??
|
|
3239
|
+
assertMcpToolListWithinBounds(exposed)) as RuntimeMcpTool[];
|
|
3240
|
+
this.listedToolSchemaTokens = estimateSerializedValueTokens(bounded);
|
|
3241
|
+
return bounded;
|
|
3067
3242
|
} catch (error) {
|
|
3068
3243
|
// A REQUIRED server's tools/list failure is fatal (fail-loud default): the
|
|
3069
3244
|
// caller explicitly requested it, so its absence must fail the turn.
|
|
@@ -3092,16 +3267,9 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3092
3267
|
{ serverId: this.name, ...safeMcpErrorFields(error) },
|
|
3093
3268
|
);
|
|
3094
3269
|
}
|
|
3270
|
+
this.releaseAggregateBudget();
|
|
3095
3271
|
return [];
|
|
3096
3272
|
}
|
|
3097
|
-
const exposed = tools
|
|
3098
|
-
.filter((tool) => this.isAllowed(tool.name))
|
|
3099
|
-
.map((tool) => ({
|
|
3100
|
-
...tool,
|
|
3101
|
-
name: prefixedMcpToolName(this.name, tool.name),
|
|
3102
|
-
}));
|
|
3103
|
-
this.listedToolSchemaTokens = estimateSerializedValueTokens(exposed);
|
|
3104
|
-
return exposed;
|
|
3105
3273
|
}
|
|
3106
3274
|
|
|
3107
3275
|
/** Latest exact tools/list projection used to build the model request. */
|
|
@@ -3119,7 +3287,9 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3119
3287
|
throw new Error(`MCP tool ${unprefixed} is not allowed for server ${this.name}`);
|
|
3120
3288
|
}
|
|
3121
3289
|
try {
|
|
3122
|
-
|
|
3290
|
+
const output = await this.inner.callTool(unprefixed, args, meta);
|
|
3291
|
+
assertMcpPayloadWithinBytes(output, MCP_MAX_TOOL_RESULT_BYTES, "MCP tool result");
|
|
3292
|
+
return output;
|
|
3123
3293
|
} catch (error) {
|
|
3124
3294
|
// The connection broker's auth-needed short-circuit arrives as a thrown
|
|
3125
3295
|
// JSON-RPC error (an inline isError result would be stripped by the SDK
|
|
@@ -4829,6 +4999,7 @@ function usageFromResponse(response: unknown): ModelResponseUsage["usage"] | nul
|
|
|
4829
4999
|
...numberProp(record, "totalTokens", "totalTokens", "total_tokens"),
|
|
4830
5000
|
...inputTokenDetailsProp(record),
|
|
4831
5001
|
...outputTokenDetailsProp(record),
|
|
5002
|
+
...requestUsageEntriesProp(record),
|
|
4832
5003
|
};
|
|
4833
5004
|
return Object.keys(usage).length > 0 ? usage : null;
|
|
4834
5005
|
}
|
|
@@ -4839,7 +5010,11 @@ function numberProp(
|
|
|
4839
5010
|
...keys: string[]
|
|
4840
5011
|
): Partial<ModelResponseUsage["usage"]> {
|
|
4841
5012
|
const value = keys.map((key) => raw[key]).find((candidate) => candidate !== undefined);
|
|
4842
|
-
|
|
5013
|
+
// Preserve numeric provider values verbatim here, including malformed ones.
|
|
5014
|
+
// The shared usage normalizer is the single bounded validation boundary and
|
|
5015
|
+
// needs to see NaN/infinite/fractional/oversized values so it can emit safe
|
|
5016
|
+
// field-path diagnostics rather than silently erasing the evidence.
|
|
5017
|
+
return typeof value === "number" ? { [outputKey]: value } : {};
|
|
4843
5018
|
}
|
|
4844
5019
|
|
|
4845
5020
|
function inputTokenDetailsProp(raw: Record<string, unknown>): Partial<ModelResponseUsage["usage"]> {
|
|
@@ -4848,7 +5023,7 @@ function inputTokenDetailsProp(raw: Record<string, unknown>): Partial<ModelRespo
|
|
|
4848
5023
|
raw.input_tokens_details ??
|
|
4849
5024
|
raw.promptTokensDetails ??
|
|
4850
5025
|
raw.prompt_tokens_details;
|
|
4851
|
-
if (
|
|
5026
|
+
if (details === undefined || details === null) {
|
|
4852
5027
|
return {};
|
|
4853
5028
|
}
|
|
4854
5029
|
return {
|
|
@@ -4861,7 +5036,7 @@ function outputTokenDetailsProp(
|
|
|
4861
5036
|
): Partial<ModelResponseUsage["usage"]> {
|
|
4862
5037
|
const details = raw.outputTokensDetails ?? raw.output_tokens_details;
|
|
4863
5038
|
const normalized = details ?? raw.completionTokensDetails ?? raw.completion_tokens_details;
|
|
4864
|
-
if (
|
|
5039
|
+
if (normalized === undefined || normalized === null) {
|
|
4865
5040
|
return {};
|
|
4866
5041
|
}
|
|
4867
5042
|
return {
|
|
@@ -4869,6 +5044,21 @@ function outputTokenDetailsProp(
|
|
|
4869
5044
|
};
|
|
4870
5045
|
}
|
|
4871
5046
|
|
|
5047
|
+
function requestUsageEntriesProp(
|
|
5048
|
+
raw: Record<string, unknown>,
|
|
5049
|
+
): Partial<ModelResponseUsage["usage"]> {
|
|
5050
|
+
const entries = raw.requestUsageEntries ?? raw.request_usage_entries;
|
|
5051
|
+
if (entries === undefined || entries === null) {
|
|
5052
|
+
return {};
|
|
5053
|
+
}
|
|
5054
|
+
return {
|
|
5055
|
+
// The normalizer validates every entry and all supported field aliases.
|
|
5056
|
+
// Preserve the SDK objects rather than rebuilding them and accidentally
|
|
5057
|
+
// dropping provider detail fields such as cache_write_tokens.
|
|
5058
|
+
requestUsageEntries: entries as NonNullable<ModelResponseUsage["usage"]["requestUsageEntries"]>,
|
|
5059
|
+
};
|
|
5060
|
+
}
|
|
5061
|
+
|
|
4872
5062
|
export function serializeApprovals(interruptions: unknown[]): unknown[] {
|
|
4873
5063
|
return interruptions
|
|
4874
5064
|
.filter((item) => interruptionToolName(item) !== HUMAN_INPUT_TOOL_NAME)
|
|
@@ -5571,6 +5761,17 @@ type RuntimeGitBindingDescriptor = {
|
|
|
5571
5761
|
uri: string;
|
|
5572
5762
|
};
|
|
5573
5763
|
|
|
5764
|
+
type RuntimeGitHttpBrokerRouteDescriptor = {
|
|
5765
|
+
provider: GitCredentialProvider;
|
|
5766
|
+
credentialBindingId: string;
|
|
5767
|
+
bindingHash: string;
|
|
5768
|
+
repositoryUri: string;
|
|
5769
|
+
brokerUri: string;
|
|
5770
|
+
protocol: "https";
|
|
5771
|
+
host: string;
|
|
5772
|
+
path: string;
|
|
5773
|
+
};
|
|
5774
|
+
|
|
5574
5775
|
function runtimeGitBindingDescriptors(
|
|
5575
5776
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5576
5777
|
): RuntimeGitBindingDescriptor[] {
|
|
@@ -5614,6 +5815,115 @@ function runtimeGitBindingDescriptors(
|
|
|
5614
5815
|
});
|
|
5615
5816
|
}
|
|
5616
5817
|
|
|
5818
|
+
function gitCredentialBindingKey(
|
|
5819
|
+
provider: GitCredentialProvider,
|
|
5820
|
+
credentialBindingId: string,
|
|
5821
|
+
): string {
|
|
5822
|
+
return `${provider}\u0000${credentialBindingId}`;
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5825
|
+
function brokeredGitCredentialBindingKeys(
|
|
5826
|
+
bindings: GitCredentialBindingSeed[],
|
|
5827
|
+
): ReadonlySet<string> {
|
|
5828
|
+
return new Set(
|
|
5829
|
+
bindings
|
|
5830
|
+
.filter((binding) => binding.transport?.kind === "http_broker")
|
|
5831
|
+
.map((binding) => gitCredentialBindingKey(binding.provider, binding.credentialBindingId)),
|
|
5832
|
+
);
|
|
5833
|
+
}
|
|
5834
|
+
|
|
5835
|
+
function runtimeGitHttpBrokerRouteDescriptors(
|
|
5836
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5837
|
+
bindings: GitCredentialBindingSeed[],
|
|
5838
|
+
): RuntimeGitHttpBrokerRouteDescriptor[] {
|
|
5839
|
+
const resourceDescriptors = runtimeGitBindingDescriptors(resources);
|
|
5840
|
+
const byBindingAndUri = new Map<string, RuntimeGitBindingDescriptor>();
|
|
5841
|
+
for (const descriptor of resourceDescriptors) {
|
|
5842
|
+
byBindingAndUri.set(
|
|
5843
|
+
`${gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId)}\u0000${descriptor.uri}`,
|
|
5844
|
+
descriptor,
|
|
5845
|
+
);
|
|
5846
|
+
}
|
|
5847
|
+
|
|
5848
|
+
const routes: RuntimeGitHttpBrokerRouteDescriptor[] = [];
|
|
5849
|
+
const claimedRepositoryUris = new Set<string>();
|
|
5850
|
+
const claimedBrokerUris = new Set<string>();
|
|
5851
|
+
for (const binding of bindings) {
|
|
5852
|
+
if (!binding.transport) continue;
|
|
5853
|
+
if (
|
|
5854
|
+
typeof binding.transport !== "object" ||
|
|
5855
|
+
binding.transport.kind !== "http_broker" ||
|
|
5856
|
+
!Array.isArray(binding.transport.repositories)
|
|
5857
|
+
) {
|
|
5858
|
+
throw new Error(
|
|
5859
|
+
`Git credential binding ${binding.credentialBindingId} uses an unsupported transport`,
|
|
5860
|
+
);
|
|
5861
|
+
}
|
|
5862
|
+
const bindingKey = gitCredentialBindingKey(binding.provider, binding.credentialBindingId);
|
|
5863
|
+
const expected = resourceDescriptors.filter(
|
|
5864
|
+
(descriptor) =>
|
|
5865
|
+
gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId) === bindingKey,
|
|
5866
|
+
);
|
|
5867
|
+
if (expected.length === 0 || binding.transport.repositories.length !== expected.length) {
|
|
5868
|
+
throw new Error(
|
|
5869
|
+
`Git HTTP broker binding ${binding.credentialBindingId} does not cover its exact repository set`,
|
|
5870
|
+
);
|
|
5871
|
+
}
|
|
5872
|
+
for (const route of binding.transport.repositories) {
|
|
5873
|
+
if (
|
|
5874
|
+
!route ||
|
|
5875
|
+
typeof route !== "object" ||
|
|
5876
|
+
typeof route.repositoryUri !== "string" ||
|
|
5877
|
+
typeof route.brokerUri !== "string"
|
|
5878
|
+
) {
|
|
5879
|
+
throw new Error(
|
|
5880
|
+
`Git HTTP broker binding ${binding.credentialBindingId} contains an invalid repository route`,
|
|
5881
|
+
);
|
|
5882
|
+
}
|
|
5883
|
+
const descriptor = byBindingAndUri.get(`${bindingKey}\u0000${route.repositoryUri}`);
|
|
5884
|
+
if (!descriptor || claimedRepositoryUris.has(route.repositoryUri)) {
|
|
5885
|
+
throw new Error(
|
|
5886
|
+
`Git HTTP broker binding ${binding.credentialBindingId} contains an unexpected repository route`,
|
|
5887
|
+
);
|
|
5888
|
+
}
|
|
5889
|
+
let brokerUrl: URL;
|
|
5890
|
+
try {
|
|
5891
|
+
brokerUrl = new URL(route.brokerUri);
|
|
5892
|
+
} catch {
|
|
5893
|
+
throw new Error(
|
|
5894
|
+
`Git HTTP broker binding ${binding.credentialBindingId} contains an invalid broker URI`,
|
|
5895
|
+
);
|
|
5896
|
+
}
|
|
5897
|
+
if (
|
|
5898
|
+
brokerUrl.protocol !== "https:" ||
|
|
5899
|
+
brokerUrl.username ||
|
|
5900
|
+
brokerUrl.password ||
|
|
5901
|
+
brokerUrl.search ||
|
|
5902
|
+
brokerUrl.hash ||
|
|
5903
|
+
brokerUrl.href !== route.brokerUri ||
|
|
5904
|
+
claimedBrokerUris.has(brokerUrl.href)
|
|
5905
|
+
) {
|
|
5906
|
+
throw new Error(
|
|
5907
|
+
`Git HTTP broker binding ${binding.credentialBindingId} contains an unsafe broker URI`,
|
|
5908
|
+
);
|
|
5909
|
+
}
|
|
5910
|
+
claimedRepositoryUris.add(route.repositoryUri);
|
|
5911
|
+
claimedBrokerUris.add(brokerUrl.href);
|
|
5912
|
+
routes.push({
|
|
5913
|
+
provider: binding.provider,
|
|
5914
|
+
credentialBindingId: binding.credentialBindingId,
|
|
5915
|
+
bindingHash: gitCredentialBindingHash(binding.credentialBindingId),
|
|
5916
|
+
repositoryUri: route.repositoryUri,
|
|
5917
|
+
brokerUri: route.brokerUri,
|
|
5918
|
+
protocol: "https",
|
|
5919
|
+
host: brokerUrl.host.toLowerCase(),
|
|
5920
|
+
path: brokerUrl.pathname.replace(/^\/+|\/+$/g, ""),
|
|
5921
|
+
});
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
return routes;
|
|
5925
|
+
}
|
|
5926
|
+
|
|
5617
5927
|
function gitUsernameForProvider(provider: GitCredentialProvider): string {
|
|
5618
5928
|
if (provider === "github") return "x-access-token";
|
|
5619
5929
|
if (provider === "gitlab") return "oauth2";
|
|
@@ -5622,22 +5932,48 @@ function gitUsernameForProvider(provider: GitCredentialProvider): string {
|
|
|
5622
5932
|
|
|
5623
5933
|
function gitCredentialHelperBindingCaseLines(
|
|
5624
5934
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5935
|
+
bindings: GitCredentialBindingSeed[],
|
|
5936
|
+
): string[] {
|
|
5937
|
+
const brokeredBindings = brokeredGitCredentialBindingKeys(bindings);
|
|
5938
|
+
return runtimeGitBindingDescriptors(resources)
|
|
5939
|
+
.filter(
|
|
5940
|
+
(descriptor) =>
|
|
5941
|
+
!brokeredBindings.has(
|
|
5942
|
+
gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId),
|
|
5943
|
+
),
|
|
5944
|
+
)
|
|
5945
|
+
.flatMap((descriptor) => {
|
|
5946
|
+
const paths = new Set([
|
|
5947
|
+
descriptor.path,
|
|
5948
|
+
descriptor.path.replace(/\.git$/, ""),
|
|
5949
|
+
`${descriptor.path.replace(/\.git$/, "")}.git`,
|
|
5950
|
+
]);
|
|
5951
|
+
return [...paths].map(
|
|
5952
|
+
(path) =>
|
|
5953
|
+
` ${shellQuote(`${descriptor.protocol}|${descriptor.host}|${path}`)}) username=${shellQuote(gitUsernameForProvider(descriptor.provider))}; token_file="$credential_dir/${descriptor.bindingHash}-token" ;;`,
|
|
5954
|
+
);
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
function gitCredentialHelperBrokerCaseLines(
|
|
5959
|
+
routes: RuntimeGitHttpBrokerRouteDescriptor[],
|
|
5625
5960
|
): string[] {
|
|
5626
|
-
return
|
|
5961
|
+
return routes.flatMap((route) => {
|
|
5627
5962
|
const paths = new Set([
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
`${
|
|
5963
|
+
route.path,
|
|
5964
|
+
route.path.replace(/\.git$/, ""),
|
|
5965
|
+
`${route.path.replace(/\.git$/, "")}.git`,
|
|
5631
5966
|
]);
|
|
5632
5967
|
return [...paths].map(
|
|
5633
5968
|
(path) =>
|
|
5634
|
-
` ${shellQuote(`${
|
|
5969
|
+
` ${shellQuote(`${route.protocol}|${route.host}|${path}`)}) username=opengeni; token_file="$credential_dir/${route.bindingHash}-token" ;;`,
|
|
5635
5970
|
);
|
|
5636
5971
|
});
|
|
5637
5972
|
}
|
|
5638
5973
|
|
|
5639
5974
|
function gitAskpassHostProviderCaseLines(
|
|
5640
5975
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5976
|
+
brokerRoutes: RuntimeGitHttpBrokerRouteDescriptor[],
|
|
5641
5977
|
): string[] {
|
|
5642
5978
|
const hosts = new Map<string, { provider: GitCredentialProvider; bindings: Set<string> }>();
|
|
5643
5979
|
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
@@ -5648,13 +5984,19 @@ function gitAskpassHostProviderCaseLines(
|
|
|
5648
5984
|
entry.bindings.add(`${descriptor.provider}\u0000${descriptor.credentialBindingId}`);
|
|
5649
5985
|
hosts.set(descriptor.host, entry);
|
|
5650
5986
|
}
|
|
5651
|
-
|
|
5652
|
-
.
|
|
5653
|
-
.
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5987
|
+
const brokerHosts = [...new Set(brokerRoutes.map((route) => route.host))]
|
|
5988
|
+
.sort((a, b) => a.localeCompare(b))
|
|
5989
|
+
.map((hostname) => ` ${shellQuote(hostname)}) printf '\\n'; return 0 ;;`);
|
|
5990
|
+
return [
|
|
5991
|
+
...brokerHosts,
|
|
5992
|
+
...[...hosts.entries()]
|
|
5993
|
+
.filter(([, entry]) => entry.bindings.size === 1)
|
|
5994
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
5995
|
+
.map(
|
|
5996
|
+
([hostname, entry]) =>
|
|
5997
|
+
` ${shellQuote(hostname)}) printf '%s\\n' ${entry.provider}; return 0 ;;`,
|
|
5998
|
+
),
|
|
5999
|
+
];
|
|
5658
6000
|
}
|
|
5659
6001
|
|
|
5660
6002
|
function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed[] = []): string[] {
|
|
@@ -5664,7 +6006,7 @@ function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed
|
|
|
5664
6006
|
const count = Math.max(1, binding.providerBindingCount ?? 1);
|
|
5665
6007
|
const provider = binding.provider;
|
|
5666
6008
|
const lines = [`write_git_binding_token ${shellQuote(hash)} "\${${seedEnv}:-}"`];
|
|
5667
|
-
if (count === 1) {
|
|
6009
|
+
if (count === 1 && binding.transport?.kind !== "http_broker") {
|
|
5668
6010
|
lines.push(`write_git_provider_token ${shellQuote(provider)} "\${${seedEnv}:-}"`);
|
|
5669
6011
|
} else {
|
|
5670
6012
|
lines.push(`remove_git_provider_token ${shellQuote(provider)}`);
|
|
@@ -5726,14 +6068,45 @@ function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed
|
|
|
5726
6068
|
];
|
|
5727
6069
|
}
|
|
5728
6070
|
|
|
6071
|
+
function gitHttpBrokerConfigCommandLines(routes: RuntimeGitHttpBrokerRouteDescriptor[]): string[] {
|
|
6072
|
+
return [
|
|
6073
|
+
'git_http_broker_config="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/http-broker.gitconfig"',
|
|
6074
|
+
'mkdir -p "$(dirname "$git_http_broker_config")"',
|
|
6075
|
+
'broker_umask="$(umask)"',
|
|
6076
|
+
"umask 077",
|
|
6077
|
+
': > "$git_http_broker_config.tmp.$$"',
|
|
6078
|
+
...routes.map(
|
|
6079
|
+
(route) =>
|
|
6080
|
+
`git config --file "$git_http_broker_config.tmp.$$" --add ${shellQuote(`url.${route.brokerUri}.insteadOf`)} ${shellQuote(route.repositoryUri)}`,
|
|
6081
|
+
),
|
|
6082
|
+
'mv -f "$git_http_broker_config.tmp.$$" "$git_http_broker_config"',
|
|
6083
|
+
'umask "$broker_umask"',
|
|
6084
|
+
'git config --global --unset-all include.path "$git_http_broker_config" >/dev/null 2>&1 || true',
|
|
6085
|
+
'git config --global --add include.path "$git_http_broker_config"',
|
|
6086
|
+
];
|
|
6087
|
+
}
|
|
6088
|
+
|
|
5729
6089
|
function gitCredentialHelperCommandLines(
|
|
5730
6090
|
resources: Extract<ResourceRef, { kind: "repository" }>[] = [],
|
|
5731
6091
|
bindings: GitCredentialBindingSeed[] = [],
|
|
5732
6092
|
): string[] {
|
|
5733
|
-
const
|
|
5734
|
-
const
|
|
6093
|
+
const brokerRoutes = runtimeGitHttpBrokerRouteDescriptors(resources, bindings);
|
|
6094
|
+
const hostProviderCases = gitAskpassHostProviderCaseLines(resources, brokerRoutes);
|
|
6095
|
+
const bindingCases = [
|
|
6096
|
+
...gitCredentialHelperBindingCaseLines(resources, bindings),
|
|
6097
|
+
...gitCredentialHelperBrokerCaseLines(brokerRoutes),
|
|
6098
|
+
];
|
|
5735
6099
|
const descriptors = runtimeGitBindingDescriptors(resources);
|
|
5736
|
-
const
|
|
6100
|
+
const brokeredBindings = brokeredGitCredentialBindingKeys(bindings);
|
|
6101
|
+
const wrapperDescriptors =
|
|
6102
|
+
bindings.length > 0
|
|
6103
|
+
? descriptors.filter(
|
|
6104
|
+
(descriptor) =>
|
|
6105
|
+
!brokeredBindings.has(
|
|
6106
|
+
gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId),
|
|
6107
|
+
),
|
|
6108
|
+
)
|
|
6109
|
+
: [];
|
|
5737
6110
|
const bindingProviders = new Map<GitCredentialProvider, Set<string>>();
|
|
5738
6111
|
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
5739
6112
|
const ids = bindingProviders.get(descriptor.provider) ?? new Set<string>();
|
|
@@ -5766,6 +6139,31 @@ function gitCredentialHelperCommandLines(
|
|
|
5766
6139
|
.filter(([, ids]) => ids.size > 1)
|
|
5767
6140
|
.map(([provider]) => provider)
|
|
5768
6141
|
: [];
|
|
6142
|
+
const brokeredOriginCases = brokerRoutes.flatMap((route) => {
|
|
6143
|
+
const base = route.repositoryUri.replace(/\.git$/, "");
|
|
6144
|
+
return [...new Set([route.repositoryUri, base, `${base}.git`])].map(
|
|
6145
|
+
(uri) => ` ${shellQuote(`${route.provider}|${uri}`)}) return 0 ;;`,
|
|
6146
|
+
);
|
|
6147
|
+
});
|
|
6148
|
+
const brokeredBindingHashCases = bindings
|
|
6149
|
+
.filter((binding) => binding.transport?.kind === "http_broker")
|
|
6150
|
+
.map(
|
|
6151
|
+
(binding) =>
|
|
6152
|
+
` ${shellQuote(`${binding.provider}|${gitCredentialBindingHash(binding.credentialBindingId)}`)}) return 0 ;;`,
|
|
6153
|
+
);
|
|
6154
|
+
const providerBindingKinds = new Map<
|
|
6155
|
+
GitCredentialProvider,
|
|
6156
|
+
{ direct: number; brokered: number }
|
|
6157
|
+
>();
|
|
6158
|
+
for (const binding of bindings) {
|
|
6159
|
+
const counts = providerBindingKinds.get(binding.provider) ?? { direct: 0, brokered: 0 };
|
|
6160
|
+
if (binding.transport?.kind === "http_broker") counts.brokered += 1;
|
|
6161
|
+
else counts.direct += 1;
|
|
6162
|
+
providerBindingKinds.set(binding.provider, counts);
|
|
6163
|
+
}
|
|
6164
|
+
const brokerOnlyProviders = [...providerBindingKinds.entries()]
|
|
6165
|
+
.filter(([, counts]) => counts.brokered > 0 && counts.direct === 0)
|
|
6166
|
+
.map(([provider]) => provider);
|
|
5769
6167
|
return [
|
|
5770
6168
|
...gitCredentialTokenWriterCommandLines(bindings),
|
|
5771
6169
|
// Provision git/provider-CLI helpers at SETUP (runtime) before any clone
|
|
@@ -5857,6 +6255,7 @@ function gitCredentialHelperCommandLines(
|
|
|
5857
6255
|
"git config --global --add credential.helper ''",
|
|
5858
6256
|
'git config --global --add credential.helper "$git_credential_helper"',
|
|
5859
6257
|
"git config --global credential.useHttpPath true",
|
|
6258
|
+
...gitHttpBrokerConfigCommandLines(brokerRoutes),
|
|
5860
6259
|
'wrapper_dir="${OPENGENI_GIT_CLI_WRAPPER_DIR:-$HOME/.opengeni/bin}"',
|
|
5861
6260
|
'mkdir -p "$wrapper_dir"',
|
|
5862
6261
|
"for opengeni_git_cli_tool in gh glab az; do",
|
|
@@ -5884,12 +6283,24 @@ function gitCredentialHelperCommandLines(
|
|
|
5884
6283
|
" *) return 1 ;;",
|
|
5885
6284
|
" esac",
|
|
5886
6285
|
"}",
|
|
6286
|
+
"binding_hash_is_brokered() {",
|
|
6287
|
+
' case "$provider|$1" in',
|
|
6288
|
+
...brokeredBindingHashCases,
|
|
6289
|
+
" *) return 1 ;;",
|
|
6290
|
+
" esac",
|
|
6291
|
+
"}",
|
|
5887
6292
|
"binding_hash_for_origin() {",
|
|
5888
6293
|
' case "$provider|$1" in',
|
|
5889
6294
|
...originWrapperHashes,
|
|
5890
6295
|
" *) return 1 ;;",
|
|
5891
6296
|
" esac",
|
|
5892
6297
|
"}",
|
|
6298
|
+
"origin_is_brokered() {",
|
|
6299
|
+
' case "$provider|$1" in',
|
|
6300
|
+
...brokeredOriginCases,
|
|
6301
|
+
" *) return 1 ;;",
|
|
6302
|
+
" esac",
|
|
6303
|
+
"}",
|
|
5893
6304
|
"sole_binding_hash() {",
|
|
5894
6305
|
' case "$provider" in',
|
|
5895
6306
|
...soleWrapperHashes,
|
|
@@ -5897,19 +6308,25 @@ function gitCredentialHelperCommandLines(
|
|
|
5897
6308
|
" esac",
|
|
5898
6309
|
"}",
|
|
5899
6310
|
`multi_binding_providers=${shellQuote(multiWrapperProviders.join(" "))}`,
|
|
6311
|
+
`broker_only_providers=${shellQuote(brokerOnlyProviders.join(" "))}`,
|
|
5900
6312
|
'if [ -n "$provider" ]; then',
|
|
5901
6313
|
" binding_hash=",
|
|
5902
6314
|
' if [ -n "${OPENGENI_GIT_BINDING:-}" ]; then',
|
|
5903
6315
|
' binding_hash="$(hash_binding_id "$OPENGENI_GIT_BINDING")"',
|
|
6316
|
+
' binding_hash_is_brokered "$binding_hash" && { printf \'%s\\n\' "$tool provider API authentication is host-brokered for OPENGENI_GIT_BINDING; use the configured provider MCP tools" >&2; exit 2; }',
|
|
5904
6317
|
' binding_hash_allowed "$binding_hash" || { printf \'%s\\n\' "OPENGENI_GIT_BINDING does not select a $provider credential attached to this session" >&2; exit 2; }',
|
|
5905
6318
|
" elif command -v git >/dev/null 2>&1; then",
|
|
5906
6319
|
' origin="$(git config --get remote.origin.url 2>/dev/null || true)"',
|
|
6320
|
+
' [ -z "$origin" ] || ! origin_is_brokered "$origin" || { printf \'%s\\n\' "$tool provider API authentication is host-brokered for this repository; use the configured provider MCP tools" >&2; exit 2; }',
|
|
5907
6321
|
' [ -z "$origin" ] || binding_hash="$(binding_hash_for_origin "$origin" 2>/dev/null || true)"',
|
|
5908
6322
|
" fi",
|
|
5909
6323
|
' [ -n "$binding_hash" ] || binding_hash="$(sole_binding_hash 2>/dev/null || true)"',
|
|
5910
6324
|
' if [ -n "$binding_hash" ]; then',
|
|
5911
6325
|
' token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$binding_hash-token"',
|
|
5912
6326
|
" else",
|
|
6327
|
+
' case " $broker_only_providers " in',
|
|
6328
|
+
' *" $provider "*) printf \'%s\\n\' "$tool provider API authentication is host-brokered for this session; use the configured provider MCP tools" >&2; exit 2 ;;',
|
|
6329
|
+
" esac",
|
|
5913
6330
|
' case " $multi_binding_providers " in',
|
|
5914
6331
|
' *" $provider "*) printf \'%s\\n\' "Unable to select one of multiple $provider credentials; run inside an attached repository or set OPENGENI_GIT_BINDING" >&2; exit 2 ;;',
|
|
5915
6332
|
" esac",
|