@opengeni/runtime 0.13.4 → 0.13.11
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-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 +248 -40
- package/dist/index.js.map +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 +198 -23
- 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/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
|
}
|
|
@@ -2468,6 +2526,10 @@ export async function prepareAgentTools(
|
|
|
2468
2526
|
url,
|
|
2469
2527
|
name: config.name ?? config.id,
|
|
2470
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),
|
|
2471
2533
|
// codex_apps returns connector tools with empty `outputSchema: {}` that the
|
|
2472
2534
|
// MCP SDK's strict Tool schema rejects (fails the turn during tools/list);
|
|
2473
2535
|
// sanitize the response on the wire before validation. The namespace Set
|
|
@@ -2534,7 +2596,7 @@ export async function prepareAgentTools(
|
|
|
2534
2596
|
const error = connectedBestEffort.errors.get(failed);
|
|
2535
2597
|
console.warn(
|
|
2536
2598
|
`[mcp] optional server "${failed.name}" failed to connect/list tools; skipping it for this turn`,
|
|
2537
|
-
error
|
|
2599
|
+
safeMcpErrorFields(error),
|
|
2538
2600
|
);
|
|
2539
2601
|
}
|
|
2540
2602
|
}
|
|
@@ -2948,6 +3010,46 @@ function safeMcpErrorFields(error: unknown): {
|
|
|
2948
3010
|
return { errorClass, status };
|
|
2949
3011
|
}
|
|
2950
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
|
+
|
|
2951
3053
|
// Compose the safe model/log reason string ("StreamableHTTPError 401", or just
|
|
2952
3054
|
// the class when no numeric status is available). Never carries the raw body.
|
|
2953
3055
|
function mcpErrorReason(fields: { errorClass: string; status?: number }): string {
|
|
@@ -3206,6 +3308,7 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3206
3308
|
private readonly bestEffort: boolean;
|
|
3207
3309
|
private loggedListToolsFailure = false;
|
|
3208
3310
|
private listedToolSchemaTokens = 0;
|
|
3311
|
+
private modelToolSchemaAccountingDeferred = false;
|
|
3209
3312
|
|
|
3210
3313
|
constructor(
|
|
3211
3314
|
private readonly inner: MCPServer,
|
|
@@ -3223,7 +3326,11 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3223
3326
|
}
|
|
3224
3327
|
|
|
3225
3328
|
connect(): Promise<void> {
|
|
3226
|
-
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
|
+
});
|
|
3227
3334
|
}
|
|
3228
3335
|
|
|
3229
3336
|
close(): Promise<void> {
|
|
@@ -3283,7 +3390,16 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
3283
3390
|
|
|
3284
3391
|
/** Latest exact tools/list projection used to build the model request. */
|
|
3285
3392
|
modelToolSchemaTokens(): number {
|
|
3286
|
-
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;
|
|
3287
3403
|
}
|
|
3288
3404
|
|
|
3289
3405
|
async callTool(
|
|
@@ -5346,6 +5462,7 @@ function sandboxFileDownloadCommand(download: SandboxFileDownload, targetPath: s
|
|
|
5346
5462
|
const targetDir = posixPath.dirname(targetPath);
|
|
5347
5463
|
const tmpPath = `${targetPath}.opengeni-download-$$`;
|
|
5348
5464
|
return [
|
|
5465
|
+
"set +x",
|
|
5349
5466
|
"set -eu",
|
|
5350
5467
|
`mkdir -p -- ${shellQuote(targetDir)}`,
|
|
5351
5468
|
`if [ ! -f ${shellQuote(targetPath)} ]; then`,
|
|
@@ -5768,6 +5885,7 @@ type RuntimeGitBindingDescriptor = {
|
|
|
5768
5885
|
host: string;
|
|
5769
5886
|
path: string;
|
|
5770
5887
|
uri: string;
|
|
5888
|
+
mountPath: string;
|
|
5771
5889
|
};
|
|
5772
5890
|
|
|
5773
5891
|
type RuntimeGitHttpBrokerRouteDescriptor = {
|
|
@@ -5820,10 +5938,61 @@ function runtimeGitBindingDescriptors(
|
|
|
5820
5938
|
host: url.host.toLowerCase(),
|
|
5821
5939
|
path,
|
|
5822
5940
|
uri: resource.uri,
|
|
5941
|
+
mountPath: `/workspace/${resourceMountPath(resource)}`,
|
|
5823
5942
|
};
|
|
5824
5943
|
});
|
|
5825
5944
|
}
|
|
5826
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
|
+
|
|
5827
5996
|
function gitCredentialBindingKey(
|
|
5828
5997
|
provider: GitCredentialProvider,
|
|
5829
5998
|
credentialBindingId: string,
|
|
@@ -6175,6 +6344,7 @@ function gitCredentialHelperCommandLines(
|
|
|
6175
6344
|
.map(([provider]) => provider);
|
|
6176
6345
|
return [
|
|
6177
6346
|
...gitCredentialTokenWriterCommandLines(bindings),
|
|
6347
|
+
...gitCredentialBindingInventoryCommandLines(resources, bindings),
|
|
6178
6348
|
// Provision git/provider-CLI helpers at SETUP (runtime) before any clone
|
|
6179
6349
|
// runs. Renewal updates only token files and deliberately leaves these
|
|
6180
6350
|
// repository-specific host mappings intact.
|
|
@@ -6337,7 +6507,7 @@ function gitCredentialHelperCommandLines(
|
|
|
6337
6507
|
' *" $provider "*) printf \'%s\\n\' "$tool provider API authentication is host-brokered for this session; use the configured provider MCP tools" >&2; exit 2 ;;',
|
|
6338
6508
|
" esac",
|
|
6339
6509
|
' case " $multi_binding_providers " in',
|
|
6340
|
-
' *" $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 ;;',
|
|
6341
6511
|
" esac",
|
|
6342
6512
|
' case "$provider" in',
|
|
6343
6513
|
' github) token_file="${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" ;;',
|
|
@@ -6391,6 +6561,7 @@ export function gitProviderTokenRefreshCommand(seeds: GitTokenSeeds): string {
|
|
|
6391
6561
|
return "";
|
|
6392
6562
|
}
|
|
6393
6563
|
return [
|
|
6564
|
+
"set +x",
|
|
6394
6565
|
seedPrefix,
|
|
6395
6566
|
"set -eu",
|
|
6396
6567
|
'export HOME="${HOME:-/workspace}"',
|
|
@@ -6404,6 +6575,7 @@ export function gitCredentialBindingTokenRefreshCommand(
|
|
|
6404
6575
|
const seedPrefix = gitCredentialBindingSeedExportPrefix(bindings);
|
|
6405
6576
|
if (!seedPrefix) return "";
|
|
6406
6577
|
return [
|
|
6578
|
+
"set +x",
|
|
6407
6579
|
seedPrefix,
|
|
6408
6580
|
"set -eu",
|
|
6409
6581
|
'export HOME="${HOME:-/workspace}"',
|
|
@@ -6459,6 +6631,7 @@ export function repositoryCloneCommand(
|
|
|
6459
6631
|
): string {
|
|
6460
6632
|
assertUniqueResourceMountPaths(resources);
|
|
6461
6633
|
const commands = [
|
|
6634
|
+
"set +x",
|
|
6462
6635
|
"set -eu",
|
|
6463
6636
|
'export HOME="${HOME:-/workspace}"',
|
|
6464
6637
|
'export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"',
|
|
@@ -6568,6 +6741,7 @@ export function toolspaceTokenSeedCommand(
|
|
|
6568
6741
|
options: { tokenFile?: string; legacyTokenFile?: string } = {},
|
|
6569
6742
|
): string {
|
|
6570
6743
|
return [
|
|
6744
|
+
"set +x",
|
|
6571
6745
|
"set -eu",
|
|
6572
6746
|
'export HOME="${HOME:-/workspace}"',
|
|
6573
6747
|
'if [ -n "${OPENGENI_TOOLSPACE_TOKEN_SEED:-}" ]; then',
|
|
@@ -6597,7 +6771,7 @@ export async function runToolspaceTokenSeedHook(
|
|
|
6597
6771
|
if (!context.toolspaceTokenSeed) {
|
|
6598
6772
|
return;
|
|
6599
6773
|
}
|
|
6600
|
-
const command = `
|
|
6774
|
+
const command = `set +x\nexport OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(context.toolspaceTokenSeed)}\n${toolspaceTokenSeedCommand(
|
|
6601
6775
|
{
|
|
6602
6776
|
...(context.toolspaceTokenFile ? { tokenFile: context.toolspaceTokenFile } : {}),
|
|
6603
6777
|
...(context.toolspaceTokenFile && context.environment.OPENGENI_TOOLSPACE_TOKEN_FILE
|
|
@@ -6629,7 +6803,7 @@ export async function refreshToolspaceTokenFile(
|
|
|
6629
6803
|
legacyTokenFile?: string;
|
|
6630
6804
|
} = {},
|
|
6631
6805
|
): Promise<void> {
|
|
6632
|
-
const command = `
|
|
6806
|
+
const command = `set +x\nexport OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(token)}\n${toolspaceTokenSeedCommand(
|
|
6633
6807
|
{
|
|
6634
6808
|
...(options.tokenFile ? { tokenFile: options.tokenFile } : {}),
|
|
6635
6809
|
...(options.legacyTokenFile ? { legacyTokenFile: options.legacyTokenFile } : {}),
|
|
@@ -6842,7 +7016,7 @@ export async function runRepositoryCloneHook(
|
|
|
6842
7016
|
.filter(Boolean)
|
|
6843
7017
|
.join("\n");
|
|
6844
7018
|
const cloneCommand = repositoryCloneCommand(resources, gitCredentialBindings);
|
|
6845
|
-
const command = seedPrefix ?
|
|
7019
|
+
const command = seedPrefix ? `set +x\n${seedPrefix}\n${cloneCommand}` : cloneCommand;
|
|
6846
7020
|
const result = await runSandboxLifecycleCommand(
|
|
6847
7021
|
session,
|
|
6848
7022
|
{
|
|
@@ -6873,6 +7047,7 @@ export async function runRepositoryCloneHook(
|
|
|
6873
7047
|
|
|
6874
7048
|
export function azureCliLoginCommand(): string {
|
|
6875
7049
|
return [
|
|
7050
|
+
"set +x",
|
|
6876
7051
|
'export HOME="${HOME:-/workspace}"',
|
|
6877
7052
|
'mkdir -p "$HOME/.azure"',
|
|
6878
7053
|
'CLIENT_ID="${AZURE_CLIENT_ID:-${ARM_CLIENT_ID:-}}"',
|
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"
|
package/src/sandbox-computer.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
PointerButton,
|
|
40
40
|
type DesktopInputRequest,
|
|
41
41
|
} from "@opengeni/agent-proto";
|
|
42
|
+
import { redactSensitiveText } from "@opengeni/contracts";
|
|
42
43
|
|
|
43
44
|
import { sandboxCommandExitCode, sandboxCommandOutput, sandboxCommandStillRunning } from "./index";
|
|
44
45
|
// `stripExecBanner` is the SAME pure helper recording.ts uses to recover the raw
|
|
@@ -328,7 +329,7 @@ export class SandboxComputer implements Computer {
|
|
|
328
329
|
// produces empty bytes, and the retry loop eventually throws. The wire-level
|
|
329
330
|
// backstop in computerCallNormalizingFetch is also in place as a second net.
|
|
330
331
|
console.warn(
|
|
331
|
-
`[SandboxComputer] action command did not finish before the ${ACTION_YIELD_MS}ms yield window — proceeding to screenshot: ${cmd}`,
|
|
332
|
+
`[SandboxComputer] action command did not finish before the ${ACTION_YIELD_MS}ms yield window — proceeding to screenshot: ${redactSensitiveText(cmd)}`,
|
|
332
333
|
);
|
|
333
334
|
return output;
|
|
334
335
|
}
|
|
@@ -758,7 +759,9 @@ export class NativeDesktopComputer implements Computer {
|
|
|
758
759
|
// Exhausted the budget (or hit a terminal denial): FAIL LOUD. Log the specific
|
|
759
760
|
// reason so the failure is DIAGNOSABLE (not a silent blank the model misreads),
|
|
760
761
|
// then rethrow — never return "".
|
|
761
|
-
const reason =
|
|
762
|
+
const reason = redactSensitiveText(
|
|
763
|
+
lastError instanceof Error ? lastError.message : String(lastError),
|
|
764
|
+
);
|
|
762
765
|
console.warn(
|
|
763
766
|
`[NativeDesktopComputer] screenshot failed after ${attempt} attempt(s): ${reason}`,
|
|
764
767
|
);
|