@opengeni/runtime 0.11.0 → 0.13.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/dist/{chunk-YUAFQEFY.js → chunk-65UDUFUO.js} +729 -118
- package/dist/chunk-65UDUFUO.js.map +1 -0
- package/dist/chunk-HAN5D27J.js +292 -0
- package/dist/chunk-HAN5D27J.js.map +1 -0
- package/dist/{index-D6yi-JcU.d.ts → index-DGUrY_yR.d.ts} +37 -7
- package/dist/index.d.ts +81 -10
- package/dist/index.js +696 -93
- package/dist/index.js.map +1 -1
- package/dist/mcp-network.d.ts +78 -0
- package/dist/mcp-network.js +62 -0
- package/dist/mcp-network.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +1 -1
- package/package.json +9 -4
- package/src/codex-tool-search.ts +20 -1
- package/src/index.ts +487 -83
- package/src/mcp-network.ts +330 -0
- package/src/sandbox/channel-a.ts +3 -28
- package/src/sandbox/display-stack.ts +860 -23
- package/src/sandbox/exec-banner.ts +67 -0
- package/src/sandbox/index.ts +7 -0
- package/src/sandbox/providers/modal.ts +29 -2
- 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,18 @@ 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
|
+
undiciFetch,
|
|
53
|
+
} from "./mcp-network";
|
|
40
54
|
import {
|
|
41
55
|
Agent,
|
|
42
56
|
AgentsError,
|
|
@@ -117,6 +131,7 @@ import {
|
|
|
117
131
|
CODEX_APPS_MCP_SERVER_ID,
|
|
118
132
|
CODEX_MODEL_ID_PREFIX,
|
|
119
133
|
CODEX_ORIGINATOR,
|
|
134
|
+
CODEX_RESPONSE_SDK_OUTER_TIMEOUT_MS,
|
|
120
135
|
boundModelToolOutputItems,
|
|
121
136
|
codexAppsSanitizingFetch,
|
|
122
137
|
codexRequestStorage,
|
|
@@ -181,7 +196,16 @@ export {
|
|
|
181
196
|
} from "./skill-library";
|
|
182
197
|
|
|
183
198
|
export type { RuntimeMetricsHooks } from "./metrics";
|
|
184
|
-
export
|
|
199
|
+
export {
|
|
200
|
+
createTurnToolCancellationController,
|
|
201
|
+
TurnSandboxCommandCancelledError,
|
|
202
|
+
} from "./sandbox/turn-tool-cancellation";
|
|
203
|
+
export type {
|
|
204
|
+
TurnSandboxCommandArgs,
|
|
205
|
+
TurnSandboxCommandSession,
|
|
206
|
+
TurnToolCancellationController,
|
|
207
|
+
TurnToolCancellationFence,
|
|
208
|
+
} from "./sandbox/turn-tool-cancellation";
|
|
185
209
|
|
|
186
210
|
// P4.3 computer-use surface (the agent's :0 driver). Re-exported from the barrel
|
|
187
211
|
// so callers (the worker, live proofs) reach SandboxComputer/ComputerUseCapability
|
|
@@ -269,8 +293,17 @@ export type {
|
|
|
269
293
|
CompactionItem,
|
|
270
294
|
PreparedCompactionPromptInput,
|
|
271
295
|
} from "./context-compaction";
|
|
272
|
-
export {
|
|
273
|
-
|
|
296
|
+
export {
|
|
297
|
+
MAX_MODEL_USAGE_TOKEN_COUNT,
|
|
298
|
+
modelCallUsageTelemetry,
|
|
299
|
+
modelUsageTokenCountOrNull,
|
|
300
|
+
normalizeModelCallUsage,
|
|
301
|
+
} from "./usage-telemetry";
|
|
302
|
+
export type {
|
|
303
|
+
ModelCallUsageInput,
|
|
304
|
+
ModelCallUsageNormalization,
|
|
305
|
+
ModelCallUsageTelemetry,
|
|
306
|
+
} from "./usage-telemetry";
|
|
274
307
|
|
|
275
308
|
ensureReadableStreamFrom();
|
|
276
309
|
|
|
@@ -289,6 +322,18 @@ export type ModelResponseUsage = {
|
|
|
289
322
|
totalTokens?: number;
|
|
290
323
|
inputTokensDetails?: Record<string, number> | Array<Record<string, number>>;
|
|
291
324
|
outputTokensDetails?: Record<string, number> | Array<Record<string, number>>;
|
|
325
|
+
requestUsageEntries?: Array<{
|
|
326
|
+
inputTokens?: number;
|
|
327
|
+
input_tokens?: number;
|
|
328
|
+
outputTokens?: number;
|
|
329
|
+
output_tokens?: number;
|
|
330
|
+
totalTokens?: number;
|
|
331
|
+
total_tokens?: number;
|
|
332
|
+
inputTokensDetails?: Record<string, number>;
|
|
333
|
+
input_tokens_details?: Record<string, number>;
|
|
334
|
+
outputTokensDetails?: Record<string, number>;
|
|
335
|
+
output_tokens_details?: Record<string, number>;
|
|
336
|
+
}>;
|
|
292
337
|
};
|
|
293
338
|
};
|
|
294
339
|
|
|
@@ -302,6 +347,8 @@ export type ResolveConnectionCredentialInput = {
|
|
|
302
347
|
serverId: string;
|
|
303
348
|
toolName?: string;
|
|
304
349
|
connectionRef: McpServerConnectionRef;
|
|
350
|
+
/** Exact MCP destination whose request would receive the resolved headers. */
|
|
351
|
+
destinationUrl: string;
|
|
305
352
|
forceRefresh?: boolean;
|
|
306
353
|
};
|
|
307
354
|
|
|
@@ -564,6 +611,10 @@ export function buildProviderClient(provider: ResolvedModelProvider, settings: S
|
|
|
564
611
|
// SDK retries on network/5xx/partial streams can replay provider work
|
|
565
612
|
// or external tool side effects without a durable checkpoint.
|
|
566
613
|
maxRetries: 0,
|
|
614
|
+
// The Codex transport owns finer headers/idle/whole deadlines and
|
|
615
|
+
// emits typed durable evidence. Keep the SDK's opaque envelope beyond
|
|
616
|
+
// that whole-response budget so `Request timed out.` cannot win first.
|
|
617
|
+
timeout: CODEX_RESPONSE_SDK_OUTER_TIMEOUT_MS,
|
|
567
618
|
fetch: codexSubscriptionFetch(instrumentedModelFetch(provider.id, globalThis.fetch)),
|
|
568
619
|
})
|
|
569
620
|
: // ResolvedModelProvider.apiKey is already the resolved key (configuredProviders
|
|
@@ -625,7 +676,7 @@ export function resolveTurnModel(
|
|
|
625
676
|
return {
|
|
626
677
|
provider: resolved.provider,
|
|
627
678
|
client,
|
|
628
|
-
model: buildModelInstance(resolved.provider, client, resolved.model.
|
|
679
|
+
model: buildModelInstance(resolved.provider, client, resolved.model.upstreamModelId),
|
|
629
680
|
configured: resolved.model,
|
|
630
681
|
};
|
|
631
682
|
}
|
|
@@ -1188,6 +1239,7 @@ export type GitCredentialBindingSeed = {
|
|
|
1188
1239
|
credentialBindingId: string;
|
|
1189
1240
|
provider: GitCredentialProvider;
|
|
1190
1241
|
token: string;
|
|
1242
|
+
transport?: GitCredentialTransport;
|
|
1191
1243
|
expiresAt?: string;
|
|
1192
1244
|
/** Total active bindings for this provider, used to suppress unsafe aliases. */
|
|
1193
1245
|
providerBindingCount?: number;
|
|
@@ -1907,14 +1959,17 @@ function maybeInstallCodexToolSearch(
|
|
|
1907
1959
|
}
|
|
1908
1960
|
|
|
1909
1961
|
/** True when the unprefixed tool `name` requires approval under `policy`. */
|
|
1910
|
-
function mcpToolRequiresApproval(
|
|
1911
|
-
|
|
1962
|
+
function mcpToolRequiresApproval(
|
|
1963
|
+
policy: boolean | ReadonlySet<string>,
|
|
1964
|
+
unprefixedName: string,
|
|
1965
|
+
): boolean {
|
|
1966
|
+
return policy === true || (policy !== false && policy.has(unprefixedName));
|
|
1912
1967
|
}
|
|
1913
1968
|
|
|
1914
1969
|
/** A per-server approval policy keyed by the server's `<id>__` tool prefix. */
|
|
1915
1970
|
type McpApprovalPolicy = {
|
|
1916
1971
|
prefix: string;
|
|
1917
|
-
requireApproval: boolean | string
|
|
1972
|
+
requireApproval: boolean | ReadonlySet<string>;
|
|
1918
1973
|
};
|
|
1919
1974
|
|
|
1920
1975
|
/** The subset of the agent surface the approval wrap needs — including `clone`. */
|
|
@@ -2005,7 +2060,8 @@ function applyMcpApprovalPolicy(agent: Agent<any, any>, settings: Settings): voi
|
|
|
2005
2060
|
)
|
|
2006
2061
|
.map((server) => ({
|
|
2007
2062
|
prefix: prefixedMcpToolName(server.id, ""),
|
|
2008
|
-
requireApproval:
|
|
2063
|
+
requireApproval:
|
|
2064
|
+
server.requireApproval === true ? true : new Set(server.requireApproval as string[]),
|
|
2009
2065
|
}))
|
|
2010
2066
|
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
2011
2067
|
if (policies.length === 0) {
|
|
@@ -2266,8 +2322,73 @@ export type PrepareToolsOptions = {
|
|
|
2266
2322
|
input: ResolveConnectionCredentialInput,
|
|
2267
2323
|
) => Promise<ResolveConnectionCredentialResult>;
|
|
2268
2324
|
onAuthNeeded?: (payload: ToolAuthNeededPayload) => Promise<void> | void;
|
|
2325
|
+
/** Injectable final MCP transport for tests and embedded hosts. */
|
|
2326
|
+
mcpFetchImpl?: FetchLike;
|
|
2269
2327
|
};
|
|
2270
2328
|
|
|
2329
|
+
type ConnectedMcpServerBatch = Awaited<ReturnType<typeof connectMcpServers>>;
|
|
2330
|
+
|
|
2331
|
+
export type ConnectedMcpServerBatches = {
|
|
2332
|
+
active: MCPServer[];
|
|
2333
|
+
failed: MCPServer[];
|
|
2334
|
+
errors: ReadonlyMap<MCPServer, Error>;
|
|
2335
|
+
close: () => Promise<void>;
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
/**
|
|
2339
|
+
* Connect SDK-managed MCP servers in stable, bounded batches. The SDK cleans a
|
|
2340
|
+
* failing strict batch; this wrapper additionally closes every earlier batch
|
|
2341
|
+
* before rethrowing, so a later-batch failure cannot leak live connections.
|
|
2342
|
+
*/
|
|
2343
|
+
export async function connectMcpServersInBatches(
|
|
2344
|
+
servers: MCPServer[],
|
|
2345
|
+
options: { strict: boolean },
|
|
2346
|
+
): Promise<ConnectedMcpServerBatches> {
|
|
2347
|
+
assertMcpServerSelectionWithinBounds(servers);
|
|
2348
|
+
const batches: ConnectedMcpServerBatch[] = [];
|
|
2349
|
+
try {
|
|
2350
|
+
for (let offset = 0; offset < servers.length; offset += MCP_MAX_CONCURRENT_SERVER_OPERATIONS) {
|
|
2351
|
+
batches.push(
|
|
2352
|
+
await connectMcpServers(
|
|
2353
|
+
servers.slice(offset, offset + MCP_MAX_CONCURRENT_SERVER_OPERATIONS),
|
|
2354
|
+
{
|
|
2355
|
+
connectInParallel: true,
|
|
2356
|
+
strict: options.strict,
|
|
2357
|
+
},
|
|
2358
|
+
),
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
await closeMcpServerBatches(batches).catch(() => undefined);
|
|
2363
|
+
throw error;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const errors = new Map<MCPServer, Error>();
|
|
2367
|
+
for (const batch of batches) {
|
|
2368
|
+
for (const [server, error] of batch.errors) errors.set(server, error);
|
|
2369
|
+
}
|
|
2370
|
+
return {
|
|
2371
|
+
active: batches.flatMap((batch) => batch.active),
|
|
2372
|
+
failed: batches.flatMap((batch) => batch.failed),
|
|
2373
|
+
errors,
|
|
2374
|
+
close: async () => {
|
|
2375
|
+
await closeMcpServerBatches(batches);
|
|
2376
|
+
},
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
async function closeMcpServerBatches(batches: ConnectedMcpServerBatch[]): Promise<void> {
|
|
2381
|
+
let firstError: unknown;
|
|
2382
|
+
for (const batch of [...batches].reverse()) {
|
|
2383
|
+
try {
|
|
2384
|
+
await batch.close();
|
|
2385
|
+
} catch (error) {
|
|
2386
|
+
firstError ??= error;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
if (firstError !== undefined) throw firstError;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2271
2392
|
export async function prepareAgentTools(
|
|
2272
2393
|
settings: Settings,
|
|
2273
2394
|
tools: ToolRef[],
|
|
@@ -2277,25 +2398,36 @@ export async function prepareAgentTools(
|
|
|
2277
2398
|
// codex_apps sanitizing fetch so every tools/list this turn accumulates the
|
|
2278
2399
|
// account's connector namespaces. Surfaced on PreparedAgentTools for the worker.
|
|
2279
2400
|
const codexConnectorNamespaces = new Set<string>();
|
|
2401
|
+
assertMcpServerSelectionWithinBounds(tools);
|
|
2280
2402
|
if (tools.length === 0) {
|
|
2281
2403
|
return { mcpServers: [], close: async () => {}, codexConnectorNamespaces };
|
|
2282
2404
|
}
|
|
2283
2405
|
const registry = new Map(settings.mcpServers.map((server) => [server.id, server]));
|
|
2284
|
-
const
|
|
2285
|
-
|
|
2406
|
+
const aggregateToolBudget = new McpAggregateToolListBudget();
|
|
2407
|
+
const mcpFetchImpl = options.mcpFetchImpl ?? undiciFetch;
|
|
2408
|
+
const servers = await boundedParallelMap(
|
|
2409
|
+
tools,
|
|
2410
|
+
MCP_MAX_CONCURRENT_SERVER_OPERATIONS,
|
|
2411
|
+
async (tool, index) => {
|
|
2286
2412
|
const config = registry.get(tool.id);
|
|
2287
2413
|
if (!config) {
|
|
2288
2414
|
throw new Error(`Unknown MCP server id: ${tool.id}`);
|
|
2289
2415
|
}
|
|
2290
2416
|
const url = firstPartyMcpServerUrlForRun(settings, config, options.workspaceId) ?? config.url;
|
|
2417
|
+
const firstParty = isFirstPartyMcpServer(settings, config);
|
|
2291
2418
|
const baseFetch = isCodexAppsMcpServer(config)
|
|
2292
|
-
? codexAppsSanitizingFetch(
|
|
2293
|
-
:
|
|
2419
|
+
? codexAppsSanitizingFetch(mcpFetchImpl, codexConnectorNamespaces)
|
|
2420
|
+
: mcpFetchImpl;
|
|
2421
|
+
const guardedFetch = guardedMcpFetch(
|
|
2422
|
+
firstParty ? { ...settings, integrationsAllowPrivateNetworkTargets: true } : settings,
|
|
2423
|
+
baseFetch,
|
|
2424
|
+
firstParty ? { requireHttpsOutsideLocalTest: false } : {},
|
|
2425
|
+
);
|
|
2294
2426
|
const fetchImpl = config.connectionRef
|
|
2295
|
-
? connectionBrokerFetch(
|
|
2296
|
-
:
|
|
2297
|
-
? firstPartyAuthFetch(
|
|
2298
|
-
:
|
|
2427
|
+
? connectionBrokerFetch(guardedFetch, config, options)
|
|
2428
|
+
: firstParty
|
|
2429
|
+
? firstPartyAuthFetch(guardedFetch, settings, options)
|
|
2430
|
+
: guardedFetch;
|
|
2299
2431
|
// A server is connected BEST-EFFORT (a connect OR tools-list failure drops
|
|
2300
2432
|
// it — its tools go unavailable for the turn — instead of failing the turn)
|
|
2301
2433
|
// in two cases:
|
|
@@ -2327,7 +2459,7 @@ export async function prepareAgentTools(
|
|
|
2327
2459
|
// MCP SDK's strict Tool schema rejects (fails the turn during tools/list);
|
|
2328
2460
|
// sanitize the response on the wire before validation. The namespace Set
|
|
2329
2461
|
// also captures each tool's original connector namespace (P4 Part B.1).
|
|
2330
|
-
|
|
2462
|
+
fetch: fetchImpl,
|
|
2331
2463
|
...(await mcpServerRequestInit(settings, config)),
|
|
2332
2464
|
...(config.timeoutMs
|
|
2333
2465
|
? {
|
|
@@ -2339,13 +2471,15 @@ export async function prepareAgentTools(
|
|
|
2339
2471
|
config.id,
|
|
2340
2472
|
config.allowedTools,
|
|
2341
2473
|
bestEffort,
|
|
2474
|
+
aggregateToolBudget,
|
|
2475
|
+
`${config.id}:${index}`,
|
|
2342
2476
|
);
|
|
2343
2477
|
return {
|
|
2344
2478
|
server,
|
|
2345
2479
|
bestEffort,
|
|
2346
2480
|
optional,
|
|
2347
2481
|
};
|
|
2348
|
-
}
|
|
2482
|
+
},
|
|
2349
2483
|
);
|
|
2350
2484
|
const requiredServers = servers.filter((entry) => !entry.bestEffort).map((entry) => entry.server);
|
|
2351
2485
|
const bestEffortServers = servers
|
|
@@ -2357,18 +2491,21 @@ export async function prepareAgentTools(
|
|
|
2357
2491
|
const optionalServerNames = new Set(
|
|
2358
2492
|
servers.filter((entry) => entry.optional).map((entry) => entry.server.name),
|
|
2359
2493
|
);
|
|
2360
|
-
const connectedRequired = await
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2494
|
+
const connectedRequired = await connectMcpServersInBatches(requiredServers, { strict: true });
|
|
2495
|
+
let connectedBestEffort: ConnectedMcpServerBatches | null = null;
|
|
2496
|
+
try {
|
|
2497
|
+
connectedBestEffort = bestEffortServers.length
|
|
2498
|
+
? await connectMcpServersInBatches(bestEffortServers, { strict: false })
|
|
2499
|
+
: null;
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
await connectedRequired.close().catch(() => undefined);
|
|
2502
|
+
throw error;
|
|
2503
|
+
}
|
|
2370
2504
|
if (connectedBestEffort) {
|
|
2371
2505
|
for (const failed of connectedBestEffort.failed) {
|
|
2506
|
+
if (failed instanceof PrefixedMcpServer) {
|
|
2507
|
+
failed.releaseAggregateBudget();
|
|
2508
|
+
}
|
|
2372
2509
|
if (!optionalServerNames.has(failed.name)) {
|
|
2373
2510
|
continue;
|
|
2374
2511
|
}
|
|
@@ -2382,10 +2519,20 @@ export async function prepareAgentTools(
|
|
|
2382
2519
|
return {
|
|
2383
2520
|
mcpServers: [...connectedRequired.active, ...(connectedBestEffort?.active ?? [])],
|
|
2384
2521
|
close: async () => {
|
|
2385
|
-
|
|
2522
|
+
let firstError: unknown;
|
|
2386
2523
|
if (connectedBestEffort) {
|
|
2387
|
-
|
|
2524
|
+
try {
|
|
2525
|
+
await connectedBestEffort.close();
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
firstError ??= error;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
try {
|
|
2531
|
+
await connectedRequired.close();
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
firstError ??= error;
|
|
2388
2534
|
}
|
|
2535
|
+
if (firstError !== undefined) throw firstError;
|
|
2389
2536
|
},
|
|
2390
2537
|
codexConnectorNamespaces,
|
|
2391
2538
|
};
|
|
@@ -2402,10 +2549,12 @@ function connectionBrokerFetch(
|
|
|
2402
2549
|
}
|
|
2403
2550
|
return async (input, init) => {
|
|
2404
2551
|
const request = await mcpRequestInfo(input, init);
|
|
2552
|
+
const destinationUrl = mcpRequestDestinationUrl(input);
|
|
2405
2553
|
const first = await resolveConnectionForRequest(
|
|
2406
2554
|
options,
|
|
2407
2555
|
config.id,
|
|
2408
2556
|
connectionRef,
|
|
2557
|
+
destinationUrl,
|
|
2409
2558
|
request.toolName,
|
|
2410
2559
|
false,
|
|
2411
2560
|
);
|
|
@@ -2417,10 +2566,12 @@ function connectionBrokerFetch(
|
|
|
2417
2566
|
withConnectionHeaders(input, init, first.headers),
|
|
2418
2567
|
);
|
|
2419
2568
|
if (response.status === 401) {
|
|
2569
|
+
await cancelMcpResponseBody(response);
|
|
2420
2570
|
const refreshed = await resolveConnectionForRequest(
|
|
2421
2571
|
options,
|
|
2422
2572
|
config.id,
|
|
2423
2573
|
connectionRef,
|
|
2574
|
+
destinationUrl,
|
|
2424
2575
|
request.toolName,
|
|
2425
2576
|
true,
|
|
2426
2577
|
);
|
|
@@ -2433,11 +2584,14 @@ function connectionBrokerFetch(
|
|
|
2433
2584
|
);
|
|
2434
2585
|
if (retry.status === 403) {
|
|
2435
2586
|
const auth = insufficientScopeAuth(retry.headers, connectionRef, refreshed.connectionId);
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2587
|
+
if (auth) {
|
|
2588
|
+
await cancelMcpResponseBody(retry);
|
|
2589
|
+
return await authNeededFetchResponse(options, config.id, request, auth, connectionRef);
|
|
2590
|
+
}
|
|
2591
|
+
return retry;
|
|
2439
2592
|
}
|
|
2440
2593
|
if (retry.status === 401) {
|
|
2594
|
+
await cancelMcpResponseBody(retry);
|
|
2441
2595
|
return await authNeededFetchResponse(
|
|
2442
2596
|
options,
|
|
2443
2597
|
config.id,
|
|
@@ -2461,9 +2615,11 @@ function connectionBrokerFetch(
|
|
|
2461
2615
|
}
|
|
2462
2616
|
if (response.status === 403) {
|
|
2463
2617
|
const auth = insufficientScopeAuth(response.headers, connectionRef, first.connectionId);
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2618
|
+
if (auth) {
|
|
2619
|
+
await cancelMcpResponseBody(response);
|
|
2620
|
+
return await authNeededFetchResponse(options, config.id, request, auth, connectionRef);
|
|
2621
|
+
}
|
|
2622
|
+
return response;
|
|
2467
2623
|
}
|
|
2468
2624
|
return response;
|
|
2469
2625
|
};
|
|
@@ -2473,6 +2629,7 @@ async function resolveConnectionForRequest(
|
|
|
2473
2629
|
options: PrepareToolsOptions,
|
|
2474
2630
|
serverId: string,
|
|
2475
2631
|
connectionRef: McpServerConnectionRef,
|
|
2632
|
+
destinationUrl: string,
|
|
2476
2633
|
toolName: string | undefined,
|
|
2477
2634
|
forceRefresh: boolean,
|
|
2478
2635
|
): Promise<ResolveConnectionCredentialResult> {
|
|
@@ -2494,6 +2651,7 @@ async function resolveConnectionForRequest(
|
|
|
2494
2651
|
workspaceId: options.workspaceId,
|
|
2495
2652
|
serverId,
|
|
2496
2653
|
connectionRef,
|
|
2654
|
+
destinationUrl,
|
|
2497
2655
|
forceRefresh,
|
|
2498
2656
|
...(toolName ? { toolName } : {}),
|
|
2499
2657
|
...(options.subjectId ? { subjectId: options.subjectId } : {}),
|
|
@@ -2610,6 +2768,10 @@ type McpRequestInfo = {
|
|
|
2610
2768
|
toolName?: string;
|
|
2611
2769
|
};
|
|
2612
2770
|
|
|
2771
|
+
function mcpRequestDestinationUrl(input: string | URL | Request): string {
|
|
2772
|
+
return new URL(input instanceof Request ? input.url : input.toString()).toString();
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2613
2775
|
async function mcpRequestInfo(
|
|
2614
2776
|
input: string | URL | Request,
|
|
2615
2777
|
init?: RequestInit,
|
|
@@ -2838,7 +3000,7 @@ async function signFirstPartyDelegatedBearer(
|
|
|
2838
3000
|
workspaceId: options.workspaceId,
|
|
2839
3001
|
subjectId: options.subjectId ?? "worker:first-party-mcp",
|
|
2840
3002
|
...(options.subjectLabel ? { subjectLabel: options.subjectLabel } : {}),
|
|
2841
|
-
permissions: options.firstPartyPermissions ??
|
|
3003
|
+
permissions: options.firstPartyPermissions ?? [...DEFAULT_FIRST_PARTY_MCP_PERMISSIONS],
|
|
2842
3004
|
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
2843
3005
|
...(options.turnId ? { turnId: options.turnId } : {}),
|
|
2844
3006
|
...(options.attemptId ? { attemptId: options.attemptId } : {}),
|
|
@@ -2922,22 +3084,6 @@ async function codexAppsMcpRequestInit(
|
|
|
2922
3084
|
// level scopes (billing/account/members/api_keys/workspace:admin) are
|
|
2923
3085
|
// intentionally excluded: they gate no first-party tool and are not agent
|
|
2924
3086
|
// 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
3087
|
// codex_apps is third-party-by-trust (the external ChatGPT connectors backend)
|
|
2942
3088
|
// but needs DYNAMIC auth, so it is its own category — deliberately NOT folded
|
|
2943
3089
|
// into the first-party allowlist, which would wrongly sign an OpenGeni delegated
|
|
@@ -3044,6 +3190,8 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3044
3190
|
registryId: string,
|
|
3045
3191
|
allowedTools?: string[],
|
|
3046
3192
|
bestEffort = false,
|
|
3193
|
+
private readonly aggregateToolBudget?: McpAggregateToolListBudget,
|
|
3194
|
+
private readonly aggregateSourceId = registryId,
|
|
3047
3195
|
) {
|
|
3048
3196
|
this.name = registryId;
|
|
3049
3197
|
this.prefix = prefixedMcpToolName(registryId, "");
|
|
@@ -3057,13 +3205,27 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3057
3205
|
}
|
|
3058
3206
|
|
|
3059
3207
|
close(): Promise<void> {
|
|
3208
|
+
this.releaseAggregateBudget();
|
|
3060
3209
|
return this.inner.close();
|
|
3061
3210
|
}
|
|
3062
3211
|
|
|
3212
|
+
releaseAggregateBudget(): void {
|
|
3213
|
+
this.aggregateToolBudget?.remove(this.aggregateSourceId);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3063
3216
|
async listTools(): Promise<RuntimeMcpTool[]> {
|
|
3064
|
-
let tools: RuntimeMcpTool[];
|
|
3065
3217
|
try {
|
|
3066
|
-
tools = await this.inner.listTools();
|
|
3218
|
+
const tools = assertMcpToolListWithinBounds(await this.inner.listTools()) as RuntimeMcpTool[];
|
|
3219
|
+
const exposed = tools
|
|
3220
|
+
.filter((tool) => this.isAllowed(tool.name))
|
|
3221
|
+
.map((tool) => ({
|
|
3222
|
+
...tool,
|
|
3223
|
+
name: prefixedMcpToolName(this.name, tool.name),
|
|
3224
|
+
}));
|
|
3225
|
+
const bounded = (this.aggregateToolBudget?.replace(this.aggregateSourceId, exposed) ??
|
|
3226
|
+
assertMcpToolListWithinBounds(exposed)) as RuntimeMcpTool[];
|
|
3227
|
+
this.listedToolSchemaTokens = estimateSerializedValueTokens(bounded);
|
|
3228
|
+
return bounded;
|
|
3067
3229
|
} catch (error) {
|
|
3068
3230
|
// A REQUIRED server's tools/list failure is fatal (fail-loud default): the
|
|
3069
3231
|
// caller explicitly requested it, so its absence must fail the turn.
|
|
@@ -3092,16 +3254,9 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3092
3254
|
{ serverId: this.name, ...safeMcpErrorFields(error) },
|
|
3093
3255
|
);
|
|
3094
3256
|
}
|
|
3257
|
+
this.releaseAggregateBudget();
|
|
3095
3258
|
return [];
|
|
3096
3259
|
}
|
|
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
3260
|
}
|
|
3106
3261
|
|
|
3107
3262
|
/** Latest exact tools/list projection used to build the model request. */
|
|
@@ -3119,7 +3274,9 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3119
3274
|
throw new Error(`MCP tool ${unprefixed} is not allowed for server ${this.name}`);
|
|
3120
3275
|
}
|
|
3121
3276
|
try {
|
|
3122
|
-
|
|
3277
|
+
const output = await this.inner.callTool(unprefixed, args, meta);
|
|
3278
|
+
assertMcpPayloadWithinBytes(output, MCP_MAX_TOOL_RESULT_BYTES, "MCP tool result");
|
|
3279
|
+
return output;
|
|
3123
3280
|
} catch (error) {
|
|
3124
3281
|
// The connection broker's auth-needed short-circuit arrives as a thrown
|
|
3125
3282
|
// JSON-RPC error (an inline isError result would be stripped by the SDK
|
|
@@ -4829,6 +4986,7 @@ function usageFromResponse(response: unknown): ModelResponseUsage["usage"] | nul
|
|
|
4829
4986
|
...numberProp(record, "totalTokens", "totalTokens", "total_tokens"),
|
|
4830
4987
|
...inputTokenDetailsProp(record),
|
|
4831
4988
|
...outputTokenDetailsProp(record),
|
|
4989
|
+
...requestUsageEntriesProp(record),
|
|
4832
4990
|
};
|
|
4833
4991
|
return Object.keys(usage).length > 0 ? usage : null;
|
|
4834
4992
|
}
|
|
@@ -4839,7 +4997,11 @@ function numberProp(
|
|
|
4839
4997
|
...keys: string[]
|
|
4840
4998
|
): Partial<ModelResponseUsage["usage"]> {
|
|
4841
4999
|
const value = keys.map((key) => raw[key]).find((candidate) => candidate !== undefined);
|
|
4842
|
-
|
|
5000
|
+
// Preserve numeric provider values verbatim here, including malformed ones.
|
|
5001
|
+
// The shared usage normalizer is the single bounded validation boundary and
|
|
5002
|
+
// needs to see NaN/infinite/fractional/oversized values so it can emit safe
|
|
5003
|
+
// field-path diagnostics rather than silently erasing the evidence.
|
|
5004
|
+
return typeof value === "number" ? { [outputKey]: value } : {};
|
|
4843
5005
|
}
|
|
4844
5006
|
|
|
4845
5007
|
function inputTokenDetailsProp(raw: Record<string, unknown>): Partial<ModelResponseUsage["usage"]> {
|
|
@@ -4848,7 +5010,7 @@ function inputTokenDetailsProp(raw: Record<string, unknown>): Partial<ModelRespo
|
|
|
4848
5010
|
raw.input_tokens_details ??
|
|
4849
5011
|
raw.promptTokensDetails ??
|
|
4850
5012
|
raw.prompt_tokens_details;
|
|
4851
|
-
if (
|
|
5013
|
+
if (details === undefined || details === null) {
|
|
4852
5014
|
return {};
|
|
4853
5015
|
}
|
|
4854
5016
|
return {
|
|
@@ -4861,7 +5023,7 @@ function outputTokenDetailsProp(
|
|
|
4861
5023
|
): Partial<ModelResponseUsage["usage"]> {
|
|
4862
5024
|
const details = raw.outputTokensDetails ?? raw.output_tokens_details;
|
|
4863
5025
|
const normalized = details ?? raw.completionTokensDetails ?? raw.completion_tokens_details;
|
|
4864
|
-
if (
|
|
5026
|
+
if (normalized === undefined || normalized === null) {
|
|
4865
5027
|
return {};
|
|
4866
5028
|
}
|
|
4867
5029
|
return {
|
|
@@ -4869,6 +5031,21 @@ function outputTokenDetailsProp(
|
|
|
4869
5031
|
};
|
|
4870
5032
|
}
|
|
4871
5033
|
|
|
5034
|
+
function requestUsageEntriesProp(
|
|
5035
|
+
raw: Record<string, unknown>,
|
|
5036
|
+
): Partial<ModelResponseUsage["usage"]> {
|
|
5037
|
+
const entries = raw.requestUsageEntries ?? raw.request_usage_entries;
|
|
5038
|
+
if (entries === undefined || entries === null) {
|
|
5039
|
+
return {};
|
|
5040
|
+
}
|
|
5041
|
+
return {
|
|
5042
|
+
// The normalizer validates every entry and all supported field aliases.
|
|
5043
|
+
// Preserve the SDK objects rather than rebuilding them and accidentally
|
|
5044
|
+
// dropping provider detail fields such as cache_write_tokens.
|
|
5045
|
+
requestUsageEntries: entries as NonNullable<ModelResponseUsage["usage"]["requestUsageEntries"]>,
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
|
|
4872
5049
|
export function serializeApprovals(interruptions: unknown[]): unknown[] {
|
|
4873
5050
|
return interruptions
|
|
4874
5051
|
.filter((item) => interruptionToolName(item) !== HUMAN_INPUT_TOOL_NAME)
|
|
@@ -5571,6 +5748,17 @@ type RuntimeGitBindingDescriptor = {
|
|
|
5571
5748
|
uri: string;
|
|
5572
5749
|
};
|
|
5573
5750
|
|
|
5751
|
+
type RuntimeGitHttpBrokerRouteDescriptor = {
|
|
5752
|
+
provider: GitCredentialProvider;
|
|
5753
|
+
credentialBindingId: string;
|
|
5754
|
+
bindingHash: string;
|
|
5755
|
+
repositoryUri: string;
|
|
5756
|
+
brokerUri: string;
|
|
5757
|
+
protocol: "https";
|
|
5758
|
+
host: string;
|
|
5759
|
+
path: string;
|
|
5760
|
+
};
|
|
5761
|
+
|
|
5574
5762
|
function runtimeGitBindingDescriptors(
|
|
5575
5763
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5576
5764
|
): RuntimeGitBindingDescriptor[] {
|
|
@@ -5614,6 +5802,115 @@ function runtimeGitBindingDescriptors(
|
|
|
5614
5802
|
});
|
|
5615
5803
|
}
|
|
5616
5804
|
|
|
5805
|
+
function gitCredentialBindingKey(
|
|
5806
|
+
provider: GitCredentialProvider,
|
|
5807
|
+
credentialBindingId: string,
|
|
5808
|
+
): string {
|
|
5809
|
+
return `${provider}\u0000${credentialBindingId}`;
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
function brokeredGitCredentialBindingKeys(
|
|
5813
|
+
bindings: GitCredentialBindingSeed[],
|
|
5814
|
+
): ReadonlySet<string> {
|
|
5815
|
+
return new Set(
|
|
5816
|
+
bindings
|
|
5817
|
+
.filter((binding) => binding.transport?.kind === "http_broker")
|
|
5818
|
+
.map((binding) => gitCredentialBindingKey(binding.provider, binding.credentialBindingId)),
|
|
5819
|
+
);
|
|
5820
|
+
}
|
|
5821
|
+
|
|
5822
|
+
function runtimeGitHttpBrokerRouteDescriptors(
|
|
5823
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5824
|
+
bindings: GitCredentialBindingSeed[],
|
|
5825
|
+
): RuntimeGitHttpBrokerRouteDescriptor[] {
|
|
5826
|
+
const resourceDescriptors = runtimeGitBindingDescriptors(resources);
|
|
5827
|
+
const byBindingAndUri = new Map<string, RuntimeGitBindingDescriptor>();
|
|
5828
|
+
for (const descriptor of resourceDescriptors) {
|
|
5829
|
+
byBindingAndUri.set(
|
|
5830
|
+
`${gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId)}\u0000${descriptor.uri}`,
|
|
5831
|
+
descriptor,
|
|
5832
|
+
);
|
|
5833
|
+
}
|
|
5834
|
+
|
|
5835
|
+
const routes: RuntimeGitHttpBrokerRouteDescriptor[] = [];
|
|
5836
|
+
const claimedRepositoryUris = new Set<string>();
|
|
5837
|
+
const claimedBrokerUris = new Set<string>();
|
|
5838
|
+
for (const binding of bindings) {
|
|
5839
|
+
if (!binding.transport) continue;
|
|
5840
|
+
if (
|
|
5841
|
+
typeof binding.transport !== "object" ||
|
|
5842
|
+
binding.transport.kind !== "http_broker" ||
|
|
5843
|
+
!Array.isArray(binding.transport.repositories)
|
|
5844
|
+
) {
|
|
5845
|
+
throw new Error(
|
|
5846
|
+
`Git credential binding ${binding.credentialBindingId} uses an unsupported transport`,
|
|
5847
|
+
);
|
|
5848
|
+
}
|
|
5849
|
+
const bindingKey = gitCredentialBindingKey(binding.provider, binding.credentialBindingId);
|
|
5850
|
+
const expected = resourceDescriptors.filter(
|
|
5851
|
+
(descriptor) =>
|
|
5852
|
+
gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId) === bindingKey,
|
|
5853
|
+
);
|
|
5854
|
+
if (expected.length === 0 || binding.transport.repositories.length !== expected.length) {
|
|
5855
|
+
throw new Error(
|
|
5856
|
+
`Git HTTP broker binding ${binding.credentialBindingId} does not cover its exact repository set`,
|
|
5857
|
+
);
|
|
5858
|
+
}
|
|
5859
|
+
for (const route of binding.transport.repositories) {
|
|
5860
|
+
if (
|
|
5861
|
+
!route ||
|
|
5862
|
+
typeof route !== "object" ||
|
|
5863
|
+
typeof route.repositoryUri !== "string" ||
|
|
5864
|
+
typeof route.brokerUri !== "string"
|
|
5865
|
+
) {
|
|
5866
|
+
throw new Error(
|
|
5867
|
+
`Git HTTP broker binding ${binding.credentialBindingId} contains an invalid repository route`,
|
|
5868
|
+
);
|
|
5869
|
+
}
|
|
5870
|
+
const descriptor = byBindingAndUri.get(`${bindingKey}\u0000${route.repositoryUri}`);
|
|
5871
|
+
if (!descriptor || claimedRepositoryUris.has(route.repositoryUri)) {
|
|
5872
|
+
throw new Error(
|
|
5873
|
+
`Git HTTP broker binding ${binding.credentialBindingId} contains an unexpected repository route`,
|
|
5874
|
+
);
|
|
5875
|
+
}
|
|
5876
|
+
let brokerUrl: URL;
|
|
5877
|
+
try {
|
|
5878
|
+
brokerUrl = new URL(route.brokerUri);
|
|
5879
|
+
} catch {
|
|
5880
|
+
throw new Error(
|
|
5881
|
+
`Git HTTP broker binding ${binding.credentialBindingId} contains an invalid broker URI`,
|
|
5882
|
+
);
|
|
5883
|
+
}
|
|
5884
|
+
if (
|
|
5885
|
+
brokerUrl.protocol !== "https:" ||
|
|
5886
|
+
brokerUrl.username ||
|
|
5887
|
+
brokerUrl.password ||
|
|
5888
|
+
brokerUrl.search ||
|
|
5889
|
+
brokerUrl.hash ||
|
|
5890
|
+
brokerUrl.href !== route.brokerUri ||
|
|
5891
|
+
claimedBrokerUris.has(brokerUrl.href)
|
|
5892
|
+
) {
|
|
5893
|
+
throw new Error(
|
|
5894
|
+
`Git HTTP broker binding ${binding.credentialBindingId} contains an unsafe broker URI`,
|
|
5895
|
+
);
|
|
5896
|
+
}
|
|
5897
|
+
claimedRepositoryUris.add(route.repositoryUri);
|
|
5898
|
+
claimedBrokerUris.add(brokerUrl.href);
|
|
5899
|
+
routes.push({
|
|
5900
|
+
provider: binding.provider,
|
|
5901
|
+
credentialBindingId: binding.credentialBindingId,
|
|
5902
|
+
bindingHash: gitCredentialBindingHash(binding.credentialBindingId),
|
|
5903
|
+
repositoryUri: route.repositoryUri,
|
|
5904
|
+
brokerUri: route.brokerUri,
|
|
5905
|
+
protocol: "https",
|
|
5906
|
+
host: brokerUrl.host.toLowerCase(),
|
|
5907
|
+
path: brokerUrl.pathname.replace(/^\/+|\/+$/g, ""),
|
|
5908
|
+
});
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
return routes;
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5617
5914
|
function gitUsernameForProvider(provider: GitCredentialProvider): string {
|
|
5618
5915
|
if (provider === "github") return "x-access-token";
|
|
5619
5916
|
if (provider === "gitlab") return "oauth2";
|
|
@@ -5622,22 +5919,48 @@ function gitUsernameForProvider(provider: GitCredentialProvider): string {
|
|
|
5622
5919
|
|
|
5623
5920
|
function gitCredentialHelperBindingCaseLines(
|
|
5624
5921
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5922
|
+
bindings: GitCredentialBindingSeed[],
|
|
5923
|
+
): string[] {
|
|
5924
|
+
const brokeredBindings = brokeredGitCredentialBindingKeys(bindings);
|
|
5925
|
+
return runtimeGitBindingDescriptors(resources)
|
|
5926
|
+
.filter(
|
|
5927
|
+
(descriptor) =>
|
|
5928
|
+
!brokeredBindings.has(
|
|
5929
|
+
gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId),
|
|
5930
|
+
),
|
|
5931
|
+
)
|
|
5932
|
+
.flatMap((descriptor) => {
|
|
5933
|
+
const paths = new Set([
|
|
5934
|
+
descriptor.path,
|
|
5935
|
+
descriptor.path.replace(/\.git$/, ""),
|
|
5936
|
+
`${descriptor.path.replace(/\.git$/, "")}.git`,
|
|
5937
|
+
]);
|
|
5938
|
+
return [...paths].map(
|
|
5939
|
+
(path) =>
|
|
5940
|
+
` ${shellQuote(`${descriptor.protocol}|${descriptor.host}|${path}`)}) username=${shellQuote(gitUsernameForProvider(descriptor.provider))}; token_file="$credential_dir/${descriptor.bindingHash}-token" ;;`,
|
|
5941
|
+
);
|
|
5942
|
+
});
|
|
5943
|
+
}
|
|
5944
|
+
|
|
5945
|
+
function gitCredentialHelperBrokerCaseLines(
|
|
5946
|
+
routes: RuntimeGitHttpBrokerRouteDescriptor[],
|
|
5625
5947
|
): string[] {
|
|
5626
|
-
return
|
|
5948
|
+
return routes.flatMap((route) => {
|
|
5627
5949
|
const paths = new Set([
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
`${
|
|
5950
|
+
route.path,
|
|
5951
|
+
route.path.replace(/\.git$/, ""),
|
|
5952
|
+
`${route.path.replace(/\.git$/, "")}.git`,
|
|
5631
5953
|
]);
|
|
5632
5954
|
return [...paths].map(
|
|
5633
5955
|
(path) =>
|
|
5634
|
-
` ${shellQuote(`${
|
|
5956
|
+
` ${shellQuote(`${route.protocol}|${route.host}|${path}`)}) username=opengeni; token_file="$credential_dir/${route.bindingHash}-token" ;;`,
|
|
5635
5957
|
);
|
|
5636
5958
|
});
|
|
5637
5959
|
}
|
|
5638
5960
|
|
|
5639
5961
|
function gitAskpassHostProviderCaseLines(
|
|
5640
5962
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5963
|
+
brokerRoutes: RuntimeGitHttpBrokerRouteDescriptor[],
|
|
5641
5964
|
): string[] {
|
|
5642
5965
|
const hosts = new Map<string, { provider: GitCredentialProvider; bindings: Set<string> }>();
|
|
5643
5966
|
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
@@ -5648,13 +5971,19 @@ function gitAskpassHostProviderCaseLines(
|
|
|
5648
5971
|
entry.bindings.add(`${descriptor.provider}\u0000${descriptor.credentialBindingId}`);
|
|
5649
5972
|
hosts.set(descriptor.host, entry);
|
|
5650
5973
|
}
|
|
5651
|
-
|
|
5652
|
-
.
|
|
5653
|
-
.
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5974
|
+
const brokerHosts = [...new Set(brokerRoutes.map((route) => route.host))]
|
|
5975
|
+
.sort((a, b) => a.localeCompare(b))
|
|
5976
|
+
.map((hostname) => ` ${shellQuote(hostname)}) printf '\\n'; return 0 ;;`);
|
|
5977
|
+
return [
|
|
5978
|
+
...brokerHosts,
|
|
5979
|
+
...[...hosts.entries()]
|
|
5980
|
+
.filter(([, entry]) => entry.bindings.size === 1)
|
|
5981
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
5982
|
+
.map(
|
|
5983
|
+
([hostname, entry]) =>
|
|
5984
|
+
` ${shellQuote(hostname)}) printf '%s\\n' ${entry.provider}; return 0 ;;`,
|
|
5985
|
+
),
|
|
5986
|
+
];
|
|
5658
5987
|
}
|
|
5659
5988
|
|
|
5660
5989
|
function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed[] = []): string[] {
|
|
@@ -5664,7 +5993,7 @@ function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed
|
|
|
5664
5993
|
const count = Math.max(1, binding.providerBindingCount ?? 1);
|
|
5665
5994
|
const provider = binding.provider;
|
|
5666
5995
|
const lines = [`write_git_binding_token ${shellQuote(hash)} "\${${seedEnv}:-}"`];
|
|
5667
|
-
if (count === 1) {
|
|
5996
|
+
if (count === 1 && binding.transport?.kind !== "http_broker") {
|
|
5668
5997
|
lines.push(`write_git_provider_token ${shellQuote(provider)} "\${${seedEnv}:-}"`);
|
|
5669
5998
|
} else {
|
|
5670
5999
|
lines.push(`remove_git_provider_token ${shellQuote(provider)}`);
|
|
@@ -5726,14 +6055,45 @@ function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed
|
|
|
5726
6055
|
];
|
|
5727
6056
|
}
|
|
5728
6057
|
|
|
6058
|
+
function gitHttpBrokerConfigCommandLines(routes: RuntimeGitHttpBrokerRouteDescriptor[]): string[] {
|
|
6059
|
+
return [
|
|
6060
|
+
'git_http_broker_config="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/http-broker.gitconfig"',
|
|
6061
|
+
'mkdir -p "$(dirname "$git_http_broker_config")"',
|
|
6062
|
+
'broker_umask="$(umask)"',
|
|
6063
|
+
"umask 077",
|
|
6064
|
+
': > "$git_http_broker_config.tmp.$$"',
|
|
6065
|
+
...routes.map(
|
|
6066
|
+
(route) =>
|
|
6067
|
+
`git config --file "$git_http_broker_config.tmp.$$" --add ${shellQuote(`url.${route.brokerUri}.insteadOf`)} ${shellQuote(route.repositoryUri)}`,
|
|
6068
|
+
),
|
|
6069
|
+
'mv -f "$git_http_broker_config.tmp.$$" "$git_http_broker_config"',
|
|
6070
|
+
'umask "$broker_umask"',
|
|
6071
|
+
'git config --global --unset-all include.path "$git_http_broker_config" >/dev/null 2>&1 || true',
|
|
6072
|
+
'git config --global --add include.path "$git_http_broker_config"',
|
|
6073
|
+
];
|
|
6074
|
+
}
|
|
6075
|
+
|
|
5729
6076
|
function gitCredentialHelperCommandLines(
|
|
5730
6077
|
resources: Extract<ResourceRef, { kind: "repository" }>[] = [],
|
|
5731
6078
|
bindings: GitCredentialBindingSeed[] = [],
|
|
5732
6079
|
): string[] {
|
|
5733
|
-
const
|
|
5734
|
-
const
|
|
6080
|
+
const brokerRoutes = runtimeGitHttpBrokerRouteDescriptors(resources, bindings);
|
|
6081
|
+
const hostProviderCases = gitAskpassHostProviderCaseLines(resources, brokerRoutes);
|
|
6082
|
+
const bindingCases = [
|
|
6083
|
+
...gitCredentialHelperBindingCaseLines(resources, bindings),
|
|
6084
|
+
...gitCredentialHelperBrokerCaseLines(brokerRoutes),
|
|
6085
|
+
];
|
|
5735
6086
|
const descriptors = runtimeGitBindingDescriptors(resources);
|
|
5736
|
-
const
|
|
6087
|
+
const brokeredBindings = brokeredGitCredentialBindingKeys(bindings);
|
|
6088
|
+
const wrapperDescriptors =
|
|
6089
|
+
bindings.length > 0
|
|
6090
|
+
? descriptors.filter(
|
|
6091
|
+
(descriptor) =>
|
|
6092
|
+
!brokeredBindings.has(
|
|
6093
|
+
gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId),
|
|
6094
|
+
),
|
|
6095
|
+
)
|
|
6096
|
+
: [];
|
|
5737
6097
|
const bindingProviders = new Map<GitCredentialProvider, Set<string>>();
|
|
5738
6098
|
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
5739
6099
|
const ids = bindingProviders.get(descriptor.provider) ?? new Set<string>();
|
|
@@ -5766,6 +6126,31 @@ function gitCredentialHelperCommandLines(
|
|
|
5766
6126
|
.filter(([, ids]) => ids.size > 1)
|
|
5767
6127
|
.map(([provider]) => provider)
|
|
5768
6128
|
: [];
|
|
6129
|
+
const brokeredOriginCases = brokerRoutes.flatMap((route) => {
|
|
6130
|
+
const base = route.repositoryUri.replace(/\.git$/, "");
|
|
6131
|
+
return [...new Set([route.repositoryUri, base, `${base}.git`])].map(
|
|
6132
|
+
(uri) => ` ${shellQuote(`${route.provider}|${uri}`)}) return 0 ;;`,
|
|
6133
|
+
);
|
|
6134
|
+
});
|
|
6135
|
+
const brokeredBindingHashCases = bindings
|
|
6136
|
+
.filter((binding) => binding.transport?.kind === "http_broker")
|
|
6137
|
+
.map(
|
|
6138
|
+
(binding) =>
|
|
6139
|
+
` ${shellQuote(`${binding.provider}|${gitCredentialBindingHash(binding.credentialBindingId)}`)}) return 0 ;;`,
|
|
6140
|
+
);
|
|
6141
|
+
const providerBindingKinds = new Map<
|
|
6142
|
+
GitCredentialProvider,
|
|
6143
|
+
{ direct: number; brokered: number }
|
|
6144
|
+
>();
|
|
6145
|
+
for (const binding of bindings) {
|
|
6146
|
+
const counts = providerBindingKinds.get(binding.provider) ?? { direct: 0, brokered: 0 };
|
|
6147
|
+
if (binding.transport?.kind === "http_broker") counts.brokered += 1;
|
|
6148
|
+
else counts.direct += 1;
|
|
6149
|
+
providerBindingKinds.set(binding.provider, counts);
|
|
6150
|
+
}
|
|
6151
|
+
const brokerOnlyProviders = [...providerBindingKinds.entries()]
|
|
6152
|
+
.filter(([, counts]) => counts.brokered > 0 && counts.direct === 0)
|
|
6153
|
+
.map(([provider]) => provider);
|
|
5769
6154
|
return [
|
|
5770
6155
|
...gitCredentialTokenWriterCommandLines(bindings),
|
|
5771
6156
|
// Provision git/provider-CLI helpers at SETUP (runtime) before any clone
|
|
@@ -5857,6 +6242,7 @@ function gitCredentialHelperCommandLines(
|
|
|
5857
6242
|
"git config --global --add credential.helper ''",
|
|
5858
6243
|
'git config --global --add credential.helper "$git_credential_helper"',
|
|
5859
6244
|
"git config --global credential.useHttpPath true",
|
|
6245
|
+
...gitHttpBrokerConfigCommandLines(brokerRoutes),
|
|
5860
6246
|
'wrapper_dir="${OPENGENI_GIT_CLI_WRAPPER_DIR:-$HOME/.opengeni/bin}"',
|
|
5861
6247
|
'mkdir -p "$wrapper_dir"',
|
|
5862
6248
|
"for opengeni_git_cli_tool in gh glab az; do",
|
|
@@ -5884,12 +6270,24 @@ function gitCredentialHelperCommandLines(
|
|
|
5884
6270
|
" *) return 1 ;;",
|
|
5885
6271
|
" esac",
|
|
5886
6272
|
"}",
|
|
6273
|
+
"binding_hash_is_brokered() {",
|
|
6274
|
+
' case "$provider|$1" in',
|
|
6275
|
+
...brokeredBindingHashCases,
|
|
6276
|
+
" *) return 1 ;;",
|
|
6277
|
+
" esac",
|
|
6278
|
+
"}",
|
|
5887
6279
|
"binding_hash_for_origin() {",
|
|
5888
6280
|
' case "$provider|$1" in',
|
|
5889
6281
|
...originWrapperHashes,
|
|
5890
6282
|
" *) return 1 ;;",
|
|
5891
6283
|
" esac",
|
|
5892
6284
|
"}",
|
|
6285
|
+
"origin_is_brokered() {",
|
|
6286
|
+
' case "$provider|$1" in',
|
|
6287
|
+
...brokeredOriginCases,
|
|
6288
|
+
" *) return 1 ;;",
|
|
6289
|
+
" esac",
|
|
6290
|
+
"}",
|
|
5893
6291
|
"sole_binding_hash() {",
|
|
5894
6292
|
' case "$provider" in',
|
|
5895
6293
|
...soleWrapperHashes,
|
|
@@ -5897,19 +6295,25 @@ function gitCredentialHelperCommandLines(
|
|
|
5897
6295
|
" esac",
|
|
5898
6296
|
"}",
|
|
5899
6297
|
`multi_binding_providers=${shellQuote(multiWrapperProviders.join(" "))}`,
|
|
6298
|
+
`broker_only_providers=${shellQuote(brokerOnlyProviders.join(" "))}`,
|
|
5900
6299
|
'if [ -n "$provider" ]; then',
|
|
5901
6300
|
" binding_hash=",
|
|
5902
6301
|
' if [ -n "${OPENGENI_GIT_BINDING:-}" ]; then',
|
|
5903
6302
|
' binding_hash="$(hash_binding_id "$OPENGENI_GIT_BINDING")"',
|
|
6303
|
+
' 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
6304
|
' 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
6305
|
" elif command -v git >/dev/null 2>&1; then",
|
|
5906
6306
|
' origin="$(git config --get remote.origin.url 2>/dev/null || true)"',
|
|
6307
|
+
' [ -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
6308
|
' [ -z "$origin" ] || binding_hash="$(binding_hash_for_origin "$origin" 2>/dev/null || true)"',
|
|
5908
6309
|
" fi",
|
|
5909
6310
|
' [ -n "$binding_hash" ] || binding_hash="$(sole_binding_hash 2>/dev/null || true)"',
|
|
5910
6311
|
' if [ -n "$binding_hash" ]; then',
|
|
5911
6312
|
' token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$binding_hash-token"',
|
|
5912
6313
|
" else",
|
|
6314
|
+
' case " $broker_only_providers " in',
|
|
6315
|
+
' *" $provider "*) printf \'%s\\n\' "$tool provider API authentication is host-brokered for this session; use the configured provider MCP tools" >&2; exit 2 ;;',
|
|
6316
|
+
" esac",
|
|
5913
6317
|
' case " $multi_binding_providers " in',
|
|
5914
6318
|
' *" $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
6319
|
" esac",
|