@opengeni/runtime 0.13.3 → 0.13.10
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-SSB7VOLE.js → chunk-QYXOVIUK.js} +15 -4
- package/dist/chunk-QYXOVIUK.js.map +1 -0
- package/dist/{chunk-JGUQSAG5.js → chunk-RHB7ETON.js} +3078 -3030
- package/dist/chunk-RHB7ETON.js.map +1 -0
- package/dist/{index-DU-KXVlx.d.ts → index-Ciehex_q.d.ts} +13 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +255 -43
- package/dist/index.js.map +1 -1
- package/dist/mcp-network.d.ts +1 -0
- package/dist/mcp-network.js +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +1 -1
- package/package.json +3 -3
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/troubleshooting.md +1 -1
- package/src/codex-tool-search.ts +161 -36
- package/src/index.ts +209 -25
- package/src/mcp-network.ts +15 -3
- package/src/sandbox/index.ts +1 -0
- package/src/sandbox/routing/routing-session.ts +49 -4
- package/src/sandbox/run-credentials.ts +2 -2
- package/src/sandbox/turn-tool-cancellation.ts +29 -0
- package/src/sandbox/workspace-archive.ts +4 -1
- package/src/sandbox-computer.ts +5 -2
- package/dist/chunk-JGUQSAG5.js.map +0 -1
- package/dist/chunk-SSB7VOLE.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
normalizeResourceMountPath,
|
|
24
24
|
prefixedMcpToolName as sharedPrefixedMcpToolName,
|
|
25
25
|
resourceMountPath,
|
|
26
|
+
redactSensitiveText,
|
|
26
27
|
sessionEventMediaPreview,
|
|
27
28
|
sessionEventMediaPreviewFromDataUrl,
|
|
28
29
|
signDelegatedAccessToken,
|
|
@@ -186,6 +187,14 @@ import {
|
|
|
186
187
|
} from "./sandbox/turn-tool-cancellation";
|
|
187
188
|
import { computerUse, type ComputerToolMode } from "./sandbox-computer";
|
|
188
189
|
import type { RuntimeMetricsHooks } from "./metrics";
|
|
190
|
+
|
|
191
|
+
// The Agents SDK's debug namespaces can otherwise serialize complete model
|
|
192
|
+
// inputs/outputs and tool arguments/results. These getters read process.env on
|
|
193
|
+
// every log call, so keep the process-global guard permanently enabled rather
|
|
194
|
+
// than toggling it around concurrent turns.
|
|
195
|
+
process.env.OPENAI_AGENTS_DONT_LOG_MODEL_DATA = "1";
|
|
196
|
+
process.env.OPENAI_AGENTS_DONT_LOG_TOOL_DATA = "1";
|
|
197
|
+
|
|
189
198
|
export {
|
|
190
199
|
getSkillLibraryEntry,
|
|
191
200
|
isSkillLibraryEntryId,
|
|
@@ -1621,6 +1630,35 @@ export function appendToolspaceInstructions(composed: string, toolspaceAvailable
|
|
|
1621
1630
|
return toolspaceAvailable ? `${composed} ${TOOLSPACE_PROGRAMMATIC_DIRECTIVE}` : composed;
|
|
1622
1631
|
}
|
|
1623
1632
|
|
|
1633
|
+
const GIT_BINDING_DISCOVERY_DIRECTIVE =
|
|
1634
|
+
"This managed sandbox has multiple credential bindings for at least one Git provider. Native Git selects credentials from the repository remote. For gh, glab, or az, run inside the intended attached repository so its origin selects the binding; when running elsewhere, inspect $HOME/.opengeni/git-bindings.json and set OPENGENI_GIT_BINDING to the listed credentialBindingId. The inventory contains identifiers and repository routes, never credential values. A binding marked git_http_broker is Git-only, so use the configured provider MCP tools for provider API operations on it.";
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Make multi-account provider-CLI selection discoverable to the model.
|
|
1638
|
+
*
|
|
1639
|
+
* Binding ids are opaque routing identifiers, not credentials. The detailed,
|
|
1640
|
+
* secret-free repository mapping is materialized in the sandbox by the Git
|
|
1641
|
+
* setup hook so prompts stay compact.
|
|
1642
|
+
*/
|
|
1643
|
+
export function appendGitCredentialBindingInstructions(
|
|
1644
|
+
composed: string,
|
|
1645
|
+
bindings: GitCredentialBindingSeed[] | undefined,
|
|
1646
|
+
activeSandboxBackend?: Settings["sandboxBackend"],
|
|
1647
|
+
): string {
|
|
1648
|
+
if (activeSandboxBackend === "selfhosted" || !bindings?.length) {
|
|
1649
|
+
return composed;
|
|
1650
|
+
}
|
|
1651
|
+
const bindingsByProvider = new Map<GitCredentialProvider, Set<string>>();
|
|
1652
|
+
for (const binding of bindings) {
|
|
1653
|
+
const ids = bindingsByProvider.get(binding.provider) ?? new Set<string>();
|
|
1654
|
+
ids.add(binding.credentialBindingId);
|
|
1655
|
+
bindingsByProvider.set(binding.provider, ids);
|
|
1656
|
+
}
|
|
1657
|
+
return [...bindingsByProvider.values()].some((ids) => ids.size > 1)
|
|
1658
|
+
? `${composed} ${GIT_BINDING_DISCOVERY_DIRECTIVE}`
|
|
1659
|
+
: composed;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1624
1662
|
/**
|
|
1625
1663
|
* Appends the one-shot genesis title directive (genesis turn only), joined by
|
|
1626
1664
|
* " " and always LAST so a white-label persona template or a per-session
|
|
@@ -1687,7 +1725,7 @@ export function mcpToolErrorOutput(error: unknown): {
|
|
|
1687
1725
|
isError: true;
|
|
1688
1726
|
content: [{ type: "text"; text: string }];
|
|
1689
1727
|
} {
|
|
1690
|
-
const details = error instanceof Error ? error.message : String(error);
|
|
1728
|
+
const details = redactSensitiveText(error instanceof Error ? error.message : String(error));
|
|
1691
1729
|
return {
|
|
1692
1730
|
isError: true,
|
|
1693
1731
|
content: [
|
|
@@ -1776,12 +1814,14 @@ export function buildOpenGeniAgent(
|
|
|
1776
1814
|
// when a toolspace token was minted for this turn (feature enabled + a
|
|
1777
1815
|
// per-turn seed — the mint gate, which includes selfhosted turns since
|
|
1778
1816
|
// they now receive the token too) — appendToolspaceInstructions,
|
|
1779
|
-
// 3. +
|
|
1817
|
+
// 3. + managed-sandbox Git binding discovery, ONLY when one provider has
|
|
1818
|
+
// multiple credential bindings,
|
|
1819
|
+
// 4. + workspace memory working set, ONLY when the workspace setting is on
|
|
1780
1820
|
// and the worker resolved a nonblank block — appendWorkspaceMemory,
|
|
1781
|
-
//
|
|
1821
|
+
// 5. + the per-session persona instructions (session-specific, so it
|
|
1782
1822
|
// refines both the workspace persona and the substrate note),
|
|
1783
|
-
//
|
|
1784
|
-
//
|
|
1823
|
+
// 6. + host context for this exact turn, when supplied,
|
|
1824
|
+
// 7. + durable session-setting state (title present + child notification
|
|
1785
1825
|
// mode), when supplied by the worker,
|
|
1786
1826
|
// The genesis title directive is deliberately NOT part of this persistent
|
|
1787
1827
|
// string. runAgentStream injects it into the first model call only.
|
|
@@ -1789,13 +1829,17 @@ export function buildOpenGeniAgent(
|
|
|
1789
1829
|
appendTurnInstructions(
|
|
1790
1830
|
appendSessionInstructions(
|
|
1791
1831
|
appendWorkspaceMemory(
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1832
|
+
appendGitCredentialBindingInstructions(
|
|
1833
|
+
appendToolspaceInstructions(
|
|
1834
|
+
composeAgentInstructions(
|
|
1835
|
+
options.instructionsTemplate ?? settings.agentInstructionsTemplate,
|
|
1836
|
+
options.workspaceEnvironment,
|
|
1837
|
+
options.rig,
|
|
1838
|
+
),
|
|
1839
|
+
settings.toolspaceEnabled && Boolean(options.toolspaceTokenSeed),
|
|
1797
1840
|
),
|
|
1798
|
-
|
|
1841
|
+
options.gitCredentialBindings,
|
|
1842
|
+
options.activeSandboxBackend,
|
|
1799
1843
|
),
|
|
1800
1844
|
options.workspaceMemory,
|
|
1801
1845
|
),
|
|
@@ -1938,13 +1982,13 @@ export function buildOpenGeniAgent(
|
|
|
1938
1982
|
}
|
|
1939
1983
|
|
|
1940
1984
|
/**
|
|
1941
|
-
* Enable Codex-CLI-style progressive
|
|
1985
|
+
* Enable Codex-CLI-style progressive MCP disclosure on a Codex turn when the
|
|
1942
1986
|
* flag is on. Gated on `structuredToolTransport === false` — the same signal that
|
|
1943
|
-
* identifies a
|
|
1944
|
-
* tools) — so no non-
|
|
1987
|
+
* identifies a Codex-subscription turn (the ChatGPT backend that rejects hosted
|
|
1988
|
+
* tools) — so no non-Codex turn is ever touched. On qualifying turns it wraps
|
|
1945
1989
|
* `getAllTools` (clone-survivingly — see {@link installCodexToolSearch}) to defer
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1990
|
+
* selected non-mandatory MCP schemas and add the client tool_search tool. The
|
|
1991
|
+
* description combines live connector namespaces with selected server identities.
|
|
1948
1992
|
*/
|
|
1949
1993
|
function maybeInstallCodexToolSearch(
|
|
1950
1994
|
agent: Agent<any, any>,
|
|
@@ -1952,9 +1996,23 @@ function maybeInstallCodexToolSearch(
|
|
|
1952
1996
|
options: BuildAgentOptions,
|
|
1953
1997
|
): void {
|
|
1954
1998
|
if (settings.codexToolSearchEnabled && options.structuredToolTransport === false) {
|
|
1999
|
+
const mcpServers = options.mcpServers ?? [];
|
|
2000
|
+
// `defer_loading:true` removes these MCP schemas from provider context until
|
|
2001
|
+
// tool_search discloses a bounded match. Keep the compaction estimator on
|
|
2002
|
+
// that same wire truth; otherwise a large deferred catalog can falsely trip
|
|
2003
|
+
// compaction before the first real model request. OpenGeni remains eager.
|
|
2004
|
+
for (const server of mcpServers) {
|
|
2005
|
+
if (server.name === "opengeni") continue;
|
|
2006
|
+
(
|
|
2007
|
+
server as MCPServer & {
|
|
2008
|
+
deferModelToolSchemaAccounting?: () => void;
|
|
2009
|
+
}
|
|
2010
|
+
).deferModelToolSchemaAccounting?.();
|
|
2011
|
+
}
|
|
1955
2012
|
installCodexToolSearch(
|
|
1956
2013
|
agent as unknown as Parameters<typeof installCodexToolSearch>[0],
|
|
1957
2014
|
options.codexConnectorNamespaces ?? new Set<string>(),
|
|
2015
|
+
new Set(mcpServers.map((server) => server.name)),
|
|
1958
2016
|
);
|
|
1959
2017
|
}
|
|
1960
2018
|
}
|
|
@@ -2408,7 +2466,13 @@ export async function prepareAgentTools(
|
|
|
2408
2466
|
}
|
|
2409
2467
|
const registry = new Map(settings.mcpServers.map((server) => [server.id, server]));
|
|
2410
2468
|
const aggregateToolBudget = new McpAggregateToolListBudget();
|
|
2411
|
-
|
|
2469
|
+
// npm Undici's dispatcher transport can hang indefinitely under Bun even
|
|
2470
|
+
// after an AbortSignal fires. Bun's native fetch is the supported runtime
|
|
2471
|
+
// transport there; destination policy validation still runs before every
|
|
2472
|
+
// credential-bearing MCP request.
|
|
2473
|
+
const useBunNativeFetch = options.mcpFetchImpl === undefined && !!process.versions.bun;
|
|
2474
|
+
const mcpFetchImpl =
|
|
2475
|
+
options.mcpFetchImpl ?? (useBunNativeFetch ? globalThis.fetch.bind(globalThis) : undiciFetch);
|
|
2412
2476
|
const servers = await boundedParallelMap(
|
|
2413
2477
|
tools,
|
|
2414
2478
|
MCP_MAX_CONCURRENT_SERVER_OPERATIONS,
|
|
@@ -2425,7 +2489,10 @@ export async function prepareAgentTools(
|
|
|
2425
2489
|
const guardedFetch = guardedMcpFetch(
|
|
2426
2490
|
firstParty ? { ...settings, integrationsAllowPrivateNetworkTargets: true } : settings,
|
|
2427
2491
|
baseFetch,
|
|
2428
|
-
|
|
2492
|
+
{
|
|
2493
|
+
...(firstParty ? { requireHttpsOutsideLocalTest: false } : {}),
|
|
2494
|
+
...(useBunNativeFetch ? { pinResolvedDestination: false } : {}),
|
|
2495
|
+
},
|
|
2429
2496
|
);
|
|
2430
2497
|
const fetchImpl = config.connectionRef
|
|
2431
2498
|
? connectionBrokerFetch(guardedFetch, config, options)
|
|
@@ -2459,6 +2526,10 @@ export async function prepareAgentTools(
|
|
|
2459
2526
|
url,
|
|
2460
2527
|
name: config.name ?? config.id,
|
|
2461
2528
|
cacheToolsList: config.cacheToolsList,
|
|
2529
|
+
// The upstream transport logger receives raw thrown errors, whose
|
|
2530
|
+
// messages may contain response bodies, URLs, headers, or echoed
|
|
2531
|
+
// credentials. Keep its diagnostic surface structural only.
|
|
2532
|
+
logger: safeMcpTransportLogger(config.id),
|
|
2462
2533
|
// codex_apps returns connector tools with empty `outputSchema: {}` that the
|
|
2463
2534
|
// MCP SDK's strict Tool schema rejects (fails the turn during tools/list);
|
|
2464
2535
|
// sanitize the response on the wire before validation. The namespace Set
|
|
@@ -2525,7 +2596,7 @@ export async function prepareAgentTools(
|
|
|
2525
2596
|
const error = connectedBestEffort.errors.get(failed);
|
|
2526
2597
|
console.warn(
|
|
2527
2598
|
`[mcp] optional server "${failed.name}" failed to connect/list tools; skipping it for this turn`,
|
|
2528
|
-
error
|
|
2599
|
+
safeMcpErrorFields(error),
|
|
2529
2600
|
);
|
|
2530
2601
|
}
|
|
2531
2602
|
}
|
|
@@ -2939,6 +3010,46 @@ function safeMcpErrorFields(error: unknown): {
|
|
|
2939
3010
|
return { errorClass, status };
|
|
2940
3011
|
}
|
|
2941
3012
|
|
|
3013
|
+
function safeMcpTransportError(error: unknown): Error & { status?: number; code?: number } {
|
|
3014
|
+
const fields = safeMcpErrorFields(error);
|
|
3015
|
+
const safeError = new Error(
|
|
3016
|
+
`MCP transport operation failed (${mcpErrorReason(fields)})`,
|
|
3017
|
+
) as Error & {
|
|
3018
|
+
status?: number;
|
|
3019
|
+
code?: number;
|
|
3020
|
+
};
|
|
3021
|
+
safeError.name = "McpTransportError";
|
|
3022
|
+
if (fields.status !== undefined) {
|
|
3023
|
+
safeError.status = fields.status;
|
|
3024
|
+
safeError.code = fields.status;
|
|
3025
|
+
}
|
|
3026
|
+
return safeError;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
function safeMcpTransportLogger(serverId: string) {
|
|
3030
|
+
const logFailure = (_message: string, ...args: unknown[]) => {
|
|
3031
|
+
let error: unknown;
|
|
3032
|
+
for (let index = args.length - 1; index >= 0; index -= 1) {
|
|
3033
|
+
if (args[index] instanceof Error) {
|
|
3034
|
+
error = args[index];
|
|
3035
|
+
break;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
console.warn("[mcp] transport operation failed", {
|
|
3039
|
+
serverId,
|
|
3040
|
+
...safeMcpErrorFields(error),
|
|
3041
|
+
});
|
|
3042
|
+
};
|
|
3043
|
+
return {
|
|
3044
|
+
namespace: "opengeni:mcp-transport",
|
|
3045
|
+
debug: () => undefined,
|
|
3046
|
+
error: logFailure,
|
|
3047
|
+
warn: logFailure,
|
|
3048
|
+
dontLogModelData: true,
|
|
3049
|
+
dontLogToolData: true,
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
|
|
2942
3053
|
// Compose the safe model/log reason string ("StreamableHTTPError 401", or just
|
|
2943
3054
|
// the class when no numeric status is available). Never carries the raw body.
|
|
2944
3055
|
function mcpErrorReason(fields: { errorClass: string; status?: number }): string {
|
|
@@ -3197,6 +3308,7 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3197
3308
|
private readonly bestEffort: boolean;
|
|
3198
3309
|
private loggedListToolsFailure = false;
|
|
3199
3310
|
private listedToolSchemaTokens = 0;
|
|
3311
|
+
private modelToolSchemaAccountingDeferred = false;
|
|
3200
3312
|
|
|
3201
3313
|
constructor(
|
|
3202
3314
|
private readonly inner: MCPServer,
|
|
@@ -3214,7 +3326,11 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3214
3326
|
}
|
|
3215
3327
|
|
|
3216
3328
|
connect(): Promise<void> {
|
|
3217
|
-
return this.inner.connect()
|
|
3329
|
+
return this.inner.connect().catch((error: unknown) => {
|
|
3330
|
+
// connectMcpServers has its own global logger and logs the thrown Error.
|
|
3331
|
+
// Never let a raw transport response body cross that logging boundary.
|
|
3332
|
+
throw safeMcpTransportError(error);
|
|
3333
|
+
});
|
|
3218
3334
|
}
|
|
3219
3335
|
|
|
3220
3336
|
close(): Promise<void> {
|
|
@@ -3274,7 +3390,16 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3274
3390
|
|
|
3275
3391
|
/** Latest exact tools/list projection used to build the model request. */
|
|
3276
3392
|
modelToolSchemaTokens(): number {
|
|
3277
|
-
return this.listedToolSchemaTokens;
|
|
3393
|
+
return this.modelToolSchemaAccountingDeferred ? 0 : this.listedToolSchemaTokens;
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
/**
|
|
3397
|
+
* Keep model-input accounting aligned with Codex `defer_loading`: the full
|
|
3398
|
+
* schema is not provider context until a tool_search_output discloses it.
|
|
3399
|
+
* Instances are turn-local, so this cannot leak into a non-Codex turn.
|
|
3400
|
+
*/
|
|
3401
|
+
deferModelToolSchemaAccounting(): void {
|
|
3402
|
+
this.modelToolSchemaAccountingDeferred = true;
|
|
3278
3403
|
}
|
|
3279
3404
|
|
|
3280
3405
|
async callTool(
|
|
@@ -5337,6 +5462,7 @@ function sandboxFileDownloadCommand(download: SandboxFileDownload, targetPath: s
|
|
|
5337
5462
|
const targetDir = posixPath.dirname(targetPath);
|
|
5338
5463
|
const tmpPath = `${targetPath}.opengeni-download-$$`;
|
|
5339
5464
|
return [
|
|
5465
|
+
"set +x",
|
|
5340
5466
|
"set -eu",
|
|
5341
5467
|
`mkdir -p -- ${shellQuote(targetDir)}`,
|
|
5342
5468
|
`if [ ! -f ${shellQuote(targetPath)} ]; then`,
|
|
@@ -5759,6 +5885,7 @@ type RuntimeGitBindingDescriptor = {
|
|
|
5759
5885
|
host: string;
|
|
5760
5886
|
path: string;
|
|
5761
5887
|
uri: string;
|
|
5888
|
+
mountPath: string;
|
|
5762
5889
|
};
|
|
5763
5890
|
|
|
5764
5891
|
type RuntimeGitHttpBrokerRouteDescriptor = {
|
|
@@ -5811,10 +5938,61 @@ function runtimeGitBindingDescriptors(
|
|
|
5811
5938
|
host: url.host.toLowerCase(),
|
|
5812
5939
|
path,
|
|
5813
5940
|
uri: resource.uri,
|
|
5941
|
+
mountPath: `/workspace/${resourceMountPath(resource)}`,
|
|
5814
5942
|
};
|
|
5815
5943
|
});
|
|
5816
5944
|
}
|
|
5817
5945
|
|
|
5946
|
+
function gitCredentialBindingInventoryCommandLines(
|
|
5947
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5948
|
+
bindings: GitCredentialBindingSeed[],
|
|
5949
|
+
): string[] {
|
|
5950
|
+
type GitBindingInventoryTransport = "direct_token" | "git_http_broker";
|
|
5951
|
+
type GitBindingInventoryEntry = {
|
|
5952
|
+
credentialBindingId: string;
|
|
5953
|
+
provider: GitCredentialProvider;
|
|
5954
|
+
transport: GitBindingInventoryTransport;
|
|
5955
|
+
repositories: Array<{ uri: string; mountPath: string }>;
|
|
5956
|
+
};
|
|
5957
|
+
const bindingTransports = new Map<string, GitBindingInventoryTransport>(
|
|
5958
|
+
bindings.map((binding) => [
|
|
5959
|
+
gitCredentialBindingKey(binding.provider, binding.credentialBindingId),
|
|
5960
|
+
binding.transport?.kind === "http_broker" ? "git_http_broker" : "direct_token",
|
|
5961
|
+
]),
|
|
5962
|
+
);
|
|
5963
|
+
const entries = new Map<string, GitBindingInventoryEntry>();
|
|
5964
|
+
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
5965
|
+
const key = gitCredentialBindingKey(descriptor.provider, descriptor.credentialBindingId);
|
|
5966
|
+
const transport = bindingTransports.get(key);
|
|
5967
|
+
if (!transport) continue;
|
|
5968
|
+
const entry: GitBindingInventoryEntry = entries.get(key) ?? {
|
|
5969
|
+
credentialBindingId: descriptor.credentialBindingId,
|
|
5970
|
+
provider: descriptor.provider,
|
|
5971
|
+
transport,
|
|
5972
|
+
repositories: [],
|
|
5973
|
+
};
|
|
5974
|
+
entry.repositories.push({ uri: descriptor.uri, mountPath: descriptor.mountPath });
|
|
5975
|
+
entries.set(key, entry);
|
|
5976
|
+
}
|
|
5977
|
+
const inventory = `${JSON.stringify(
|
|
5978
|
+
{
|
|
5979
|
+
version: 1,
|
|
5980
|
+
bindings: [...entries.values()],
|
|
5981
|
+
},
|
|
5982
|
+
null,
|
|
5983
|
+
2,
|
|
5984
|
+
)}\n`;
|
|
5985
|
+
return [
|
|
5986
|
+
'git_binding_inventory="${OPENGENI_GIT_BINDINGS_FILE:-$HOME/.opengeni/git-bindings.json}"',
|
|
5987
|
+
'mkdir -p "$(dirname "$git_binding_inventory")"',
|
|
5988
|
+
'inventory_umask="$(umask)"',
|
|
5989
|
+
"umask 077",
|
|
5990
|
+
`printf '%s' ${shellQuote(inventory)} > "$git_binding_inventory.tmp.$$"`,
|
|
5991
|
+
'mv -f "$git_binding_inventory.tmp.$$" "$git_binding_inventory"',
|
|
5992
|
+
'umask "$inventory_umask"',
|
|
5993
|
+
];
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5818
5996
|
function gitCredentialBindingKey(
|
|
5819
5997
|
provider: GitCredentialProvider,
|
|
5820
5998
|
credentialBindingId: string,
|
|
@@ -6166,6 +6344,7 @@ function gitCredentialHelperCommandLines(
|
|
|
6166
6344
|
.map(([provider]) => provider);
|
|
6167
6345
|
return [
|
|
6168
6346
|
...gitCredentialTokenWriterCommandLines(bindings),
|
|
6347
|
+
...gitCredentialBindingInventoryCommandLines(resources, bindings),
|
|
6169
6348
|
// Provision git/provider-CLI helpers at SETUP (runtime) before any clone
|
|
6170
6349
|
// runs. Renewal updates only token files and deliberately leaves these
|
|
6171
6350
|
// repository-specific host mappings intact.
|
|
@@ -6328,7 +6507,7 @@ function gitCredentialHelperCommandLines(
|
|
|
6328
6507
|
' *" $provider "*) printf \'%s\\n\' "$tool provider API authentication is host-brokered for this session; use the configured provider MCP tools" >&2; exit 2 ;;',
|
|
6329
6508
|
" esac",
|
|
6330
6509
|
' case " $multi_binding_providers " in',
|
|
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 ;;',
|
|
6510
|
+
' *" $provider "*) printf \'%s\\n\' "Unable to select one of multiple $provider credentials; run inside an attached repository, or inspect ${OPENGENI_GIT_BINDINGS_FILE:-$HOME/.opengeni/git-bindings.json} and set OPENGENI_GIT_BINDING" >&2; exit 2 ;;',
|
|
6332
6511
|
" esac",
|
|
6333
6512
|
' case "$provider" in',
|
|
6334
6513
|
' github) token_file="${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" ;;',
|
|
@@ -6382,6 +6561,7 @@ export function gitProviderTokenRefreshCommand(seeds: GitTokenSeeds): string {
|
|
|
6382
6561
|
return "";
|
|
6383
6562
|
}
|
|
6384
6563
|
return [
|
|
6564
|
+
"set +x",
|
|
6385
6565
|
seedPrefix,
|
|
6386
6566
|
"set -eu",
|
|
6387
6567
|
'export HOME="${HOME:-/workspace}"',
|
|
@@ -6395,6 +6575,7 @@ export function gitCredentialBindingTokenRefreshCommand(
|
|
|
6395
6575
|
const seedPrefix = gitCredentialBindingSeedExportPrefix(bindings);
|
|
6396
6576
|
if (!seedPrefix) return "";
|
|
6397
6577
|
return [
|
|
6578
|
+
"set +x",
|
|
6398
6579
|
seedPrefix,
|
|
6399
6580
|
"set -eu",
|
|
6400
6581
|
'export HOME="${HOME:-/workspace}"',
|
|
@@ -6450,6 +6631,7 @@ export function repositoryCloneCommand(
|
|
|
6450
6631
|
): string {
|
|
6451
6632
|
assertUniqueResourceMountPaths(resources);
|
|
6452
6633
|
const commands = [
|
|
6634
|
+
"set +x",
|
|
6453
6635
|
"set -eu",
|
|
6454
6636
|
'export HOME="${HOME:-/workspace}"',
|
|
6455
6637
|
'export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"',
|
|
@@ -6559,6 +6741,7 @@ export function toolspaceTokenSeedCommand(
|
|
|
6559
6741
|
options: { tokenFile?: string; legacyTokenFile?: string } = {},
|
|
6560
6742
|
): string {
|
|
6561
6743
|
return [
|
|
6744
|
+
"set +x",
|
|
6562
6745
|
"set -eu",
|
|
6563
6746
|
'export HOME="${HOME:-/workspace}"',
|
|
6564
6747
|
'if [ -n "${OPENGENI_TOOLSPACE_TOKEN_SEED:-}" ]; then',
|
|
@@ -6588,7 +6771,7 @@ export async function runToolspaceTokenSeedHook(
|
|
|
6588
6771
|
if (!context.toolspaceTokenSeed) {
|
|
6589
6772
|
return;
|
|
6590
6773
|
}
|
|
6591
|
-
const command = `
|
|
6774
|
+
const command = `set +x\nexport OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(context.toolspaceTokenSeed)}\n${toolspaceTokenSeedCommand(
|
|
6592
6775
|
{
|
|
6593
6776
|
...(context.toolspaceTokenFile ? { tokenFile: context.toolspaceTokenFile } : {}),
|
|
6594
6777
|
...(context.toolspaceTokenFile && context.environment.OPENGENI_TOOLSPACE_TOKEN_FILE
|
|
@@ -6620,7 +6803,7 @@ export async function refreshToolspaceTokenFile(
|
|
|
6620
6803
|
legacyTokenFile?: string;
|
|
6621
6804
|
} = {},
|
|
6622
6805
|
): Promise<void> {
|
|
6623
|
-
const command = `
|
|
6806
|
+
const command = `set +x\nexport OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(token)}\n${toolspaceTokenSeedCommand(
|
|
6624
6807
|
{
|
|
6625
6808
|
...(options.tokenFile ? { tokenFile: options.tokenFile } : {}),
|
|
6626
6809
|
...(options.legacyTokenFile ? { legacyTokenFile: options.legacyTokenFile } : {}),
|
|
@@ -6833,7 +7016,7 @@ export async function runRepositoryCloneHook(
|
|
|
6833
7016
|
.filter(Boolean)
|
|
6834
7017
|
.join("\n");
|
|
6835
7018
|
const cloneCommand = repositoryCloneCommand(resources, gitCredentialBindings);
|
|
6836
|
-
const command = seedPrefix ?
|
|
7019
|
+
const command = seedPrefix ? `set +x\n${seedPrefix}\n${cloneCommand}` : cloneCommand;
|
|
6837
7020
|
const result = await runSandboxLifecycleCommand(
|
|
6838
7021
|
session,
|
|
6839
7022
|
{
|
|
@@ -6864,6 +7047,7 @@ export async function runRepositoryCloneHook(
|
|
|
6864
7047
|
|
|
6865
7048
|
export function azureCliLoginCommand(): string {
|
|
6866
7049
|
return [
|
|
7050
|
+
"set +x",
|
|
6867
7051
|
'export HOME="${HOME:-/workspace}"',
|
|
6868
7052
|
'mkdir -p "$HOME/.azure"',
|
|
6869
7053
|
'CLIENT_ID="${AZURE_CLIENT_ID:-${ARM_CLIENT_ID:-}}"',
|
package/src/mcp-network.ts
CHANGED
|
@@ -212,16 +212,28 @@ export function guardedMcpFetch<TInput extends string | URL | Request>(
|
|
|
212
212
|
options: {
|
|
213
213
|
maxResponseBytes?: number;
|
|
214
214
|
dnsLookup?: DnsLookup;
|
|
215
|
+
pinResolvedDestination?: boolean;
|
|
215
216
|
requireHttpsOutsideLocalTest?: boolean;
|
|
216
217
|
} = {},
|
|
217
218
|
): (input: TInput, init?: RequestInit) => Promise<Response> {
|
|
218
219
|
return async (input: TInput, init?: RequestInit) => {
|
|
219
|
-
const
|
|
220
|
-
fetchImpl: fetchImpl as FetchLike,
|
|
220
|
+
const destinationOptions = {
|
|
221
221
|
...(options.dnsLookup ? { dnsLookup: options.dnsLookup } : {}),
|
|
222
222
|
label: "MCP endpoint",
|
|
223
223
|
requireHttpsOutsideLocalTest: options.requireHttpsOutsideLocalTest ?? true,
|
|
224
|
-
}
|
|
224
|
+
};
|
|
225
|
+
let response: Response;
|
|
226
|
+
if (options.pinResolvedDestination === false) {
|
|
227
|
+
await resolvePinnedDestination(input instanceof Request ? input.url : input, settings, {
|
|
228
|
+
...destinationOptions,
|
|
229
|
+
});
|
|
230
|
+
response = await fetchImpl(input, { ...init, redirect: "manual" });
|
|
231
|
+
} else {
|
|
232
|
+
response = await pinnedFetch(input, init, settings, {
|
|
233
|
+
fetchImpl: fetchImpl as FetchLike,
|
|
234
|
+
...destinationOptions,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
225
237
|
return boundMcpResponseBody(response, options.maxResponseBytes ?? MCP_MAX_RESPONSE_BYTES);
|
|
226
238
|
};
|
|
227
239
|
}
|
package/src/sandbox/index.ts
CHANGED
|
@@ -379,6 +379,7 @@ export {
|
|
|
379
379
|
type DefaultBackendLossResult,
|
|
380
380
|
type RoutableBackendSession,
|
|
381
381
|
type ResolvedActiveBackend,
|
|
382
|
+
type RoutingMutationSettlementResult,
|
|
382
383
|
type RoutingRetainedProcess,
|
|
383
384
|
type RoutingRetainedProcessAdoption,
|
|
384
385
|
type RoutingRetainedProcessTerminalProof,
|
|
@@ -115,6 +115,15 @@ export type RoutingRetainedProcess = {
|
|
|
115
115
|
providerSessionId: number;
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
/** A yielded process was durably promoted, but the mutable authority checked
|
|
119
|
+
* after that commit was stale. The provider output must still be rejected;
|
|
120
|
+
* this result carries only the safe process identity needed to drain the exact
|
|
121
|
+
* original backend without replaying the mutation. */
|
|
122
|
+
export type RoutingMutationSettlementResult = {
|
|
123
|
+
status: "retained_process_durable_output_rejected";
|
|
124
|
+
retainedProcess: RoutingRetainedProcess;
|
|
125
|
+
};
|
|
126
|
+
|
|
118
127
|
/** Exact copied route identity required to reconstruct an already-durable
|
|
119
128
|
* retained process after the request/runtime object that opened it is gone.
|
|
120
129
|
* The live session is deliberately not caller-supplied: adoption may bind only
|
|
@@ -184,7 +193,7 @@ export interface RoutingSandboxSessionDeps {
|
|
|
184
193
|
/** Stable candidate generated before durable promotion is attempted. It is
|
|
185
194
|
* supplied only when the provider returned a positive yielded-session id. */
|
|
186
195
|
retainedProcess?: RoutingRetainedProcess;
|
|
187
|
-
}) => Promise<void>;
|
|
196
|
+
}) => Promise<void | RoutingMutationSettlementResult>;
|
|
188
197
|
/** Admit one model/user-visible stdin mutation under the already-durable
|
|
189
198
|
* retained process authority. Control polling and helper execs never call it. */
|
|
190
199
|
beforeProcessMutation?: (input: {
|
|
@@ -265,13 +274,15 @@ export class RoutingBackendRecoveryRequiredError extends Error {
|
|
|
265
274
|
export class RoutingMutationOutcomeUnknownError extends Error {
|
|
266
275
|
readonly name = "RoutingMutationOutcomeUnknownError";
|
|
267
276
|
readonly retryable = false;
|
|
277
|
+
readonly retainedProcess: RoutingRetainedProcess | null;
|
|
268
278
|
|
|
269
279
|
constructor(
|
|
270
280
|
public readonly op: string,
|
|
271
281
|
message: string,
|
|
272
|
-
options?: { cause?: unknown },
|
|
282
|
+
options?: { cause?: unknown; retainedProcess?: RoutingRetainedProcess },
|
|
273
283
|
) {
|
|
274
284
|
super(message, options);
|
|
285
|
+
this.retainedProcess = options?.retainedProcess ?? null;
|
|
275
286
|
}
|
|
276
287
|
}
|
|
277
288
|
|
|
@@ -665,7 +676,8 @@ export class RoutingSandboxSession implements RoutableBackendSession {
|
|
|
665
676
|
);
|
|
666
677
|
}
|
|
667
678
|
try {
|
|
668
|
-
await this.deps.afterMutation(pending);
|
|
679
|
+
const result = await this.deps.afterMutation(pending);
|
|
680
|
+
if (result) this.confirmDurableRejectedPromotion(record, result);
|
|
669
681
|
record.pendingParentPromotion = null;
|
|
670
682
|
record.durable = true;
|
|
671
683
|
} catch (error) {
|
|
@@ -677,6 +689,24 @@ export class RoutingSandboxSession implements RoutableBackendSession {
|
|
|
677
689
|
}
|
|
678
690
|
}
|
|
679
691
|
|
|
692
|
+
private confirmDurableRejectedPromotion(
|
|
693
|
+
record: RetainedProcessRecord,
|
|
694
|
+
result: RoutingMutationSettlementResult,
|
|
695
|
+
): void {
|
|
696
|
+
if (
|
|
697
|
+
result.status !== "retained_process_durable_output_rejected" ||
|
|
698
|
+
result.retainedProcess.id !== record.process.id ||
|
|
699
|
+
result.retainedProcess.providerSessionId !== record.process.providerSessionId
|
|
700
|
+
) {
|
|
701
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
702
|
+
"retainProcess",
|
|
703
|
+
`Durable promotion result did not match retained provider session ${record.process.providerSessionId}`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
record.pendingParentPromotion = null;
|
|
707
|
+
record.durable = true;
|
|
708
|
+
}
|
|
709
|
+
|
|
680
710
|
private async settleRetainedProcess(
|
|
681
711
|
record: RetainedProcessRecord,
|
|
682
712
|
proof: RoutingRetainedProcessTerminalProof,
|
|
@@ -936,9 +966,24 @@ export class RoutingSandboxSession implements RoutableBackendSession {
|
|
|
936
966
|
...(retainedProcess ? { retainedProcess } : {}),
|
|
937
967
|
};
|
|
938
968
|
try {
|
|
939
|
-
await this.deps.afterMutation(settlement);
|
|
969
|
+
const settlementResult = await this.deps.afterMutation(settlement);
|
|
970
|
+
if (retainedRecord && settlementResult) {
|
|
971
|
+
this.confirmDurableRejectedPromotion(retainedRecord, settlementResult);
|
|
972
|
+
this.invalidate(backend);
|
|
973
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
974
|
+
op,
|
|
975
|
+
`Mutating sandbox operation "${op}" yielded provider session ${retainedRecord.process.providerSessionId}; durable promotion succeeded but stale authority rejected its output, and the operation was not replayed`,
|
|
976
|
+
{ retainedProcess: retainedRecord.process },
|
|
977
|
+
);
|
|
978
|
+
}
|
|
940
979
|
if (retainedRecord) retainedRecord.durable = true;
|
|
941
980
|
} catch (error) {
|
|
981
|
+
if (
|
|
982
|
+
error instanceof RoutingMutationOutcomeUnknownError &&
|
|
983
|
+
error.retainedProcess !== null
|
|
984
|
+
) {
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
942
987
|
if (retainedRecord) retainedRecord.pendingParentPromotion = settlement;
|
|
943
988
|
this.invalidate(backend);
|
|
944
989
|
throw new RoutingMutationOutcomeUnknownError(
|
|
@@ -496,9 +496,9 @@ async function runCredentialCommand(
|
|
|
496
496
|
cmd: string,
|
|
497
497
|
commandRunner?: RunCredentialCommandRunner,
|
|
498
498
|
): Promise<void> {
|
|
499
|
-
const marked = `set -eu\n${cmd}\nprintf '\\n${COMMAND_OK_MARKER}\\n'`;
|
|
499
|
+
const marked = `set +x\nset -eu\n${cmd}\nprintf '\\n${COMMAND_OK_MARKER}\\n'`;
|
|
500
500
|
const args: ExecCommandArgs = {
|
|
501
|
-
cmd: `
|
|
501
|
+
cmd: `set +x\nenv -u BASH_ENV bash --noprofile --norc -c ${shellQuote(marked)}`,
|
|
502
502
|
yieldTimeMs: 120_000,
|
|
503
503
|
maxOutputTokens: 4_000,
|
|
504
504
|
};
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
parseExecBannerExitCode,
|
|
5
5
|
parseExecBannerSessionId,
|
|
6
6
|
} from "./exec-banner";
|
|
7
|
+
import { RoutingMutationOutcomeUnknownError } from "./routing/routing-session";
|
|
7
8
|
|
|
8
9
|
const TURN_EXEC_YIELD_MS = 250;
|
|
9
10
|
const TURN_WRITE_YIELD_MS = 250;
|
|
@@ -759,6 +760,34 @@ class TurnToolCancellationControllerImpl implements TurnToolCancellationControll
|
|
|
759
760
|
correlationId,
|
|
760
761
|
async () => await tool.invoke(runContext, cancellableInput, details),
|
|
761
762
|
);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
const retainedProcess =
|
|
765
|
+
!useRemoteOpCancellation && error instanceof RoutingMutationOutcomeUnknownError
|
|
766
|
+
? error.retainedProcess
|
|
767
|
+
: null;
|
|
768
|
+
const processSession =
|
|
769
|
+
retainedProcess === null
|
|
770
|
+
? null
|
|
771
|
+
: retainedProcessSession(cancellationSession, retainedProcess.providerSessionId);
|
|
772
|
+
if (retainedProcess && processSession) {
|
|
773
|
+
// The provider output remains rejected, but DB promotion is
|
|
774
|
+
// already durable. Preserve only the safe process locator and
|
|
775
|
+
// exact routing session so turn finalization can drain it; never
|
|
776
|
+
// invoke a helper through the mutable current route.
|
|
777
|
+
this.shellSessions.set(retainedProcess.providerSessionId, {
|
|
778
|
+
sessionId: retainedProcess.providerSessionId,
|
|
779
|
+
markerPath,
|
|
780
|
+
token,
|
|
781
|
+
runContext,
|
|
782
|
+
execInvoke: tool.invoke,
|
|
783
|
+
writeInvoke: this.rawWriteInvoke,
|
|
784
|
+
processSession,
|
|
785
|
+
identity: null,
|
|
786
|
+
identityValidated: false,
|
|
787
|
+
cancellation: null,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
throw error;
|
|
762
791
|
} finally {
|
|
763
792
|
remoteExec?.settle();
|
|
764
793
|
}
|
|
@@ -151,7 +151,10 @@ export function readVerifiedWorkspaceArchive(
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
function stdoutFromExecResult(value: unknown): string {
|
|
154
|
-
if (typeof value === "string")
|
|
154
|
+
if (typeof value === "string") {
|
|
155
|
+
const delimiter = /\r?\nOutput:\r?\n/u.exec(value);
|
|
156
|
+
return delimiter ? value.slice(delimiter.index + delimiter[0].length) : value;
|
|
157
|
+
}
|
|
155
158
|
if (!value || typeof value !== "object") return "";
|
|
156
159
|
const output = value as { stdout?: unknown; output?: unknown };
|
|
157
160
|
return typeof output.stdout === "string"
|