@opengeni/runtime 0.7.1 → 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/THIRD_PARTY_NOTICES +32 -0
- package/dist/agent-UMUIWWD2.js +8 -0
- package/dist/agent-UMUIWWD2.js.map +1 -0
- package/dist/chunk-4YJ5Z52Z.js +1789 -0
- package/dist/chunk-4YJ5Z52Z.js.map +1 -0
- package/dist/chunk-65UDUFUO.js +22133 -0
- package/dist/chunk-65UDUFUO.js.map +1 -0
- package/dist/chunk-DPULLAJG.js +215 -0
- package/dist/chunk-DPULLAJG.js.map +1 -0
- package/dist/chunk-HAN5D27J.js +292 -0
- package/dist/chunk-HAN5D27J.js.map +1 -0
- package/dist/chunk-K4V4QYCL.js +37683 -0
- package/dist/chunk-K4V4QYCL.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/{index-DIiiwQY2.d.ts → index-DGUrY_yR.d.ts} +171 -18
- package/dist/index.d.ts +404 -125
- package/dist/index.js +8857 -412
- package/dist/index.js.map +1 -1
- package/dist/localSources-T3HZ6KY4.js +15 -0
- package/dist/localSources-T3HZ6KY4.js.map +1 -0
- 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 +42 -5
- package/dist/skill-library.d.ts +74 -0
- package/dist/skill-library.js +18 -0
- package/dist/skill-library.js.map +1 -0
- package/package.json +18 -5
- package/src/bundled_hashicorp_terraform_skills/README.md +3 -3
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +1 -1
- package/src/codex-tool-search.ts +20 -1
- package/src/context-compaction.ts +421 -24
- package/src/history-sanitizer.ts +14 -5
- package/src/index.ts +1806 -347
- package/src/mcp-network.ts +330 -0
- package/src/sandbox/channel-a.ts +526 -113
- package/src/sandbox/display-stack.ts +860 -23
- package/src/sandbox/exec-banner.ts +67 -0
- package/src/sandbox/index.ts +43 -2
- package/src/sandbox/providers/modal.ts +29 -2
- package/src/sandbox/routing/routing-session.ts +10 -2
- package/src/sandbox/run-credentials.ts +818 -0
- package/src/sandbox/select.ts +3 -3
- package/src/sandbox/selfhosted/capabilities.ts +1 -1
- package/src/sandbox/selfhosted/op-stream.ts +40 -0
- package/src/sandbox/selfhosted/op-testing.ts +50 -3
- package/src/sandbox/selfhosted/session.ts +17 -4
- package/src/sandbox/selfhosted/testing.ts +1 -1
- package/src/sandbox/stream-port.ts +1 -1
- package/src/sandbox/stream-token.ts +1 -1
- package/src/sandbox/terminal-server.ts +6 -2
- package/src/sandbox/toolspace-token.ts +147 -0
- package/src/sandbox/turn-tool-cancellation.ts +940 -0
- package/src/sandbox-computer.ts +3 -1
- package/src/skill-library.ts +323 -0
- package/src/usage-telemetry.ts +495 -51
- package/dist/chunk-RYDCVTMW.js +0 -5531
- package/dist/chunk-RYDCVTMW.js.map +0 -1
- /package/src/{bundled_hashicorp_terraform_skills → bundled_skill_library}/azure-verified-modules/SKILL.md +0 -0
package/src/index.ts
CHANGED
|
@@ -12,19 +12,45 @@ import {
|
|
|
12
12
|
sandboxLifecycleHookIds,
|
|
13
13
|
} from "@opengeni/config";
|
|
14
14
|
import {
|
|
15
|
+
approvalIdentifier,
|
|
15
16
|
CAPABILITY_DESCRIPTORS,
|
|
17
|
+
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
18
|
+
assertUniqueResourceMountPaths,
|
|
19
|
+
gitCredentialBindingIdForRepository,
|
|
20
|
+
gitCredentialProviderForRepository,
|
|
16
21
|
isClearedRunStateBlob,
|
|
22
|
+
normalizeRepositorySubpath,
|
|
23
|
+
normalizeResourceMountPath,
|
|
17
24
|
prefixedMcpToolName as sharedPrefixedMcpToolName,
|
|
25
|
+
resourceMountPath,
|
|
26
|
+
sessionEventMediaPreview,
|
|
27
|
+
sessionEventMediaPreviewFromDataUrl,
|
|
18
28
|
signDelegatedAccessToken,
|
|
29
|
+
RequestHumanInputToolInput,
|
|
19
30
|
type GitCredentialProvider,
|
|
31
|
+
type GitCredentialTransport,
|
|
32
|
+
type HumanInputResponse,
|
|
20
33
|
type McpServerConnectionRef,
|
|
21
34
|
type Permission,
|
|
22
35
|
type ReasoningEffort,
|
|
23
36
|
type ResourceRef,
|
|
24
37
|
type SessionEventType,
|
|
38
|
+
type SessionEventMediaPreview,
|
|
25
39
|
type ToolAuthNeededPayload,
|
|
26
40
|
type ToolRef,
|
|
27
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";
|
|
28
54
|
import {
|
|
29
55
|
Agent,
|
|
30
56
|
AgentsError,
|
|
@@ -52,6 +78,7 @@ import {
|
|
|
52
78
|
setDefaultOpenAIKey,
|
|
53
79
|
setOpenAIResponsesTransport,
|
|
54
80
|
setTracingDisabled,
|
|
81
|
+
tool as agentTool,
|
|
55
82
|
// Hosted web_search tool factory. Re-exported from @openai/agents-openai via
|
|
56
83
|
// `export * from '@openai/agents-openai'` in @openai/agents' index (0.11.6);
|
|
57
84
|
// it returns a { type: 'hosted_tool', providerData: { type: 'web_search' } }
|
|
@@ -104,11 +131,14 @@ import {
|
|
|
104
131
|
CODEX_APPS_MCP_SERVER_ID,
|
|
105
132
|
CODEX_MODEL_ID_PREFIX,
|
|
106
133
|
CODEX_ORIGINATOR,
|
|
134
|
+
CODEX_RESPONSE_SDK_OUTER_TIMEOUT_MS,
|
|
135
|
+
boundModelToolOutputItems,
|
|
107
136
|
codexAppsSanitizingFetch,
|
|
108
137
|
codexRequestStorage,
|
|
109
138
|
codexSubscriptionFetch,
|
|
110
139
|
} from "@opengeni/codex";
|
|
111
140
|
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
141
|
+
import { createHash } from "node:crypto";
|
|
112
142
|
import { dirname, isAbsolute, join, posix as posixPath, relative } from "node:path";
|
|
113
143
|
import { fileURLToPath } from "node:url";
|
|
114
144
|
|
|
@@ -121,22 +151,61 @@ import {
|
|
|
121
151
|
import { installCodexToolSearch } from "./codex-tool-search";
|
|
122
152
|
import {
|
|
123
153
|
CompactionNeededError,
|
|
154
|
+
CompactionProviderResponseError,
|
|
155
|
+
EmptyCompactionSummaryError,
|
|
124
156
|
SUMMARY_BUFFER_TOKENS,
|
|
125
157
|
compactionThresholdTokens,
|
|
126
|
-
|
|
158
|
+
estimateCompleteModelInput,
|
|
159
|
+
estimateSerializedValueTokens,
|
|
160
|
+
hasModelGeneratedItem,
|
|
127
161
|
renderCompactionPromptInputForChat,
|
|
162
|
+
type CompleteModelInputFootprint,
|
|
163
|
+
type ProviderContextTokenSignal,
|
|
128
164
|
} from "./context-compaction";
|
|
129
165
|
import {
|
|
130
166
|
createSandboxClient,
|
|
131
167
|
desktopCapableBackend,
|
|
132
168
|
restoredSandboxSessionStateFromEntry,
|
|
133
169
|
setSelfhostedApplyDiff,
|
|
170
|
+
toolspaceTokenFileFromEnvironment,
|
|
171
|
+
withToolspaceTokenClient,
|
|
172
|
+
withToolspaceTokenSession,
|
|
173
|
+
withRunCredentialsClient,
|
|
174
|
+
withRunCredentialsSession,
|
|
175
|
+
type RunCredentialSessionReady,
|
|
134
176
|
} from "./sandbox";
|
|
135
177
|
import { runWithToolCallCorrelation } from "./sandbox/op-correlation";
|
|
178
|
+
import {
|
|
179
|
+
createTurnToolCancellationController,
|
|
180
|
+
TurnSandboxCommandCancelledError,
|
|
181
|
+
wrapCapabilityToolsForTurnCancellation,
|
|
182
|
+
type TurnSandboxCommandArgs,
|
|
183
|
+
type TurnSandboxCommandSession,
|
|
184
|
+
type TurnToolCancellationFence,
|
|
185
|
+
} from "./sandbox/turn-tool-cancellation";
|
|
136
186
|
import { computerUse, type ComputerToolMode } from "./sandbox-computer";
|
|
137
187
|
import type { RuntimeMetricsHooks } from "./metrics";
|
|
188
|
+
export {
|
|
189
|
+
getSkillLibraryEntry,
|
|
190
|
+
isSkillLibraryEntryId,
|
|
191
|
+
listSkillLibraryEntries,
|
|
192
|
+
loadSkillLibrarySkill,
|
|
193
|
+
type SkillLibraryEntry,
|
|
194
|
+
type SkillLibraryFile,
|
|
195
|
+
type SkillLibrarySkill,
|
|
196
|
+
} from "./skill-library";
|
|
138
197
|
|
|
139
198
|
export type { RuntimeMetricsHooks } from "./metrics";
|
|
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";
|
|
140
209
|
|
|
141
210
|
// P4.3 computer-use surface (the agent's :0 driver). Re-exported from the barrel
|
|
142
211
|
// so callers (the worker, live proofs) reach SandboxComputer/ComputerUseCapability
|
|
@@ -188,17 +257,25 @@ export { OpenAIChatCompletionsModel, OpenAIResponsesModel } from "@openai/agents
|
|
|
188
257
|
|
|
189
258
|
export {
|
|
190
259
|
CompactionNeededError,
|
|
260
|
+
CompactionProviderResponseError,
|
|
261
|
+
EmptyCompactionSummaryError,
|
|
191
262
|
buildCompactionPromptInput,
|
|
192
263
|
buildCompactionReplacementHistory,
|
|
264
|
+
compactionReplacementFingerprint,
|
|
193
265
|
compactionThresholdTokens,
|
|
194
266
|
clampCompactionThresholdRatio,
|
|
195
267
|
decideCompaction,
|
|
196
268
|
buildSummaryItem,
|
|
197
269
|
findCompactionNeededError,
|
|
198
270
|
isCompactionSummary,
|
|
271
|
+
latestCompactionReplacementFingerprint,
|
|
272
|
+
prepareCompactionPromptInput,
|
|
199
273
|
isUserMessage,
|
|
200
274
|
estimateTokens,
|
|
201
275
|
estimateItemTokens,
|
|
276
|
+
estimateCompleteModelInput,
|
|
277
|
+
estimateSerializedValueTokens,
|
|
278
|
+
hasModelGeneratedItem,
|
|
202
279
|
renderCompactionPromptInputForChat,
|
|
203
280
|
COMPACTION_SUMMARY_MARKER,
|
|
204
281
|
COMPACTION_PROMPT,
|
|
@@ -211,9 +288,22 @@ export {
|
|
|
211
288
|
isEphemeralInternalContext,
|
|
212
289
|
USER_MESSAGE_TRUNCATION_MARKER,
|
|
213
290
|
} from "./context-compaction";
|
|
214
|
-
export type {
|
|
215
|
-
|
|
216
|
-
|
|
291
|
+
export type {
|
|
292
|
+
CompactionDecision,
|
|
293
|
+
CompactionItem,
|
|
294
|
+
PreparedCompactionPromptInput,
|
|
295
|
+
} from "./context-compaction";
|
|
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";
|
|
217
307
|
|
|
218
308
|
ensureReadableStreamFrom();
|
|
219
309
|
|
|
@@ -232,6 +322,18 @@ export type ModelResponseUsage = {
|
|
|
232
322
|
totalTokens?: number;
|
|
233
323
|
inputTokensDetails?: Record<string, number> | Array<Record<string, number>>;
|
|
234
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
|
+
}>;
|
|
235
337
|
};
|
|
236
338
|
};
|
|
237
339
|
|
|
@@ -245,6 +347,8 @@ export type ResolveConnectionCredentialInput = {
|
|
|
245
347
|
serverId: string;
|
|
246
348
|
toolName?: string;
|
|
247
349
|
connectionRef: McpServerConnectionRef;
|
|
350
|
+
/** Exact MCP destination whose request would receive the resolved headers. */
|
|
351
|
+
destinationUrl: string;
|
|
248
352
|
forceRefresh?: boolean;
|
|
249
353
|
};
|
|
250
354
|
|
|
@@ -259,9 +363,11 @@ export type ResolveConnectionCredentialResult =
|
|
|
259
363
|
status: "auth_needed";
|
|
260
364
|
reason: ToolAuthNeededPayload["reason"];
|
|
261
365
|
providerDomain: string;
|
|
366
|
+
provider?: string;
|
|
262
367
|
connectionId?: string;
|
|
263
368
|
scopes?: string[];
|
|
264
369
|
resource?: string;
|
|
370
|
+
selectedResources?: McpServerConnectionRef["selectedResources"];
|
|
265
371
|
authorizationUrl?: string;
|
|
266
372
|
};
|
|
267
373
|
|
|
@@ -316,6 +422,11 @@ export type AgentSegmentInput =
|
|
|
316
422
|
approvalId: string;
|
|
317
423
|
decision: "approve" | "reject";
|
|
318
424
|
message?: string;
|
|
425
|
+
}
|
|
426
|
+
| {
|
|
427
|
+
kind: "human_input";
|
|
428
|
+
serializedRunState: string;
|
|
429
|
+
toolCallId: string;
|
|
319
430
|
};
|
|
320
431
|
|
|
321
432
|
export type PreparedAgentInput = {
|
|
@@ -323,6 +434,13 @@ export type PreparedAgentInput = {
|
|
|
323
434
|
sandboxSessionState?: SandboxSessionState;
|
|
324
435
|
};
|
|
325
436
|
|
|
437
|
+
export const HUMAN_INPUT_TOOL_NAME = "request_human_input";
|
|
438
|
+
|
|
439
|
+
export type SerializedHumanInputInterruption = {
|
|
440
|
+
toolCallId: string;
|
|
441
|
+
input: ReturnType<typeof RequestHumanInputToolInput.parse>;
|
|
442
|
+
};
|
|
443
|
+
|
|
326
444
|
export type SandboxFileDownload = {
|
|
327
445
|
fileId: string;
|
|
328
446
|
mountPath: string;
|
|
@@ -381,6 +499,7 @@ export type OpenGeniRuntime = {
|
|
|
381
499
|
options?: RunAgentStreamOptions,
|
|
382
500
|
) => Promise<Awaited<ReturnType<typeof runAgentStream>>>;
|
|
383
501
|
serializeApprovals: (interruptions: unknown[]) => unknown[];
|
|
502
|
+
serializeHumanInputRequests?: (interruptions: unknown[]) => SerializedHumanInputInterruption[];
|
|
384
503
|
};
|
|
385
504
|
|
|
386
505
|
export type ProductionRuntimeOverrides = {
|
|
@@ -416,6 +535,7 @@ export function createProductionAgentRuntime(
|
|
|
416
535
|
sandboxClient: overrides.sandboxClient,
|
|
417
536
|
}),
|
|
418
537
|
serializeApprovals,
|
|
538
|
+
serializeHumanInputRequests,
|
|
419
539
|
};
|
|
420
540
|
}
|
|
421
541
|
|
|
@@ -491,6 +611,10 @@ export function buildProviderClient(provider: ResolvedModelProvider, settings: S
|
|
|
491
611
|
// SDK retries on network/5xx/partial streams can replay provider work
|
|
492
612
|
// or external tool side effects without a durable checkpoint.
|
|
493
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,
|
|
494
618
|
fetch: codexSubscriptionFetch(instrumentedModelFetch(provider.id, globalThis.fetch)),
|
|
495
619
|
})
|
|
496
620
|
: // ResolvedModelProvider.apiKey is already the resolved key (configuredProviders
|
|
@@ -552,7 +676,7 @@ export function resolveTurnModel(
|
|
|
552
676
|
return {
|
|
553
677
|
provider: resolved.provider,
|
|
554
678
|
client,
|
|
555
|
-
model: buildModelInstance(resolved.provider, client, resolved.model.
|
|
679
|
+
model: buildModelInstance(resolved.provider, client, resolved.model.upstreamModelId),
|
|
556
680
|
configured: resolved.model,
|
|
557
681
|
};
|
|
558
682
|
}
|
|
@@ -804,19 +928,28 @@ export async function summarizeForCompaction(
|
|
|
804
928
|
const maxTokens = options.maxOutputTokens ?? SUMMARY_BUFFER_TOKENS;
|
|
805
929
|
if (api === "chat") {
|
|
806
930
|
const transcript = renderCompactionPromptInputForChat(input);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
931
|
+
let completion: unknown;
|
|
932
|
+
try {
|
|
933
|
+
completion = await client.chat.completions.create({
|
|
934
|
+
model,
|
|
935
|
+
max_tokens: maxTokens,
|
|
936
|
+
messages: [{ role: "user", content: transcript }],
|
|
937
|
+
...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
|
|
938
|
+
} as any);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(error), error);
|
|
941
|
+
}
|
|
813
942
|
const usage = modelResponseUsageFromResponse(completion);
|
|
814
943
|
if (usage) {
|
|
815
944
|
await options.onUsage?.(usage);
|
|
816
945
|
}
|
|
817
946
|
const text = (completion as { choices?: Array<{ message?: { content?: unknown } }> })
|
|
818
947
|
.choices?.[0]?.message?.content;
|
|
819
|
-
|
|
948
|
+
const summary = typeof text === "string" ? text.trim() : "";
|
|
949
|
+
if (!summary) {
|
|
950
|
+
throw new EmptyCompactionSummaryError(compactionResponseDiagnostics(completion, summary));
|
|
951
|
+
}
|
|
952
|
+
return summary;
|
|
820
953
|
}
|
|
821
954
|
// Use the same SDK Responses adapter as the real agent call. It converts the
|
|
822
955
|
// structured AgentInputItems (callId/providerData/etc.) to provider wire
|
|
@@ -839,12 +972,205 @@ export async function summarizeForCompaction(
|
|
|
839
972
|
handoffs: [],
|
|
840
973
|
tracing: false,
|
|
841
974
|
};
|
|
842
|
-
|
|
975
|
+
let response: unknown;
|
|
976
|
+
try {
|
|
977
|
+
response = await new CompactionResponsesModel(client, model).fetchResponse(request);
|
|
978
|
+
} catch (error) {
|
|
979
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(error), error);
|
|
980
|
+
}
|
|
843
981
|
const usage = modelResponseUsageFromResponse(response);
|
|
844
982
|
if (usage) {
|
|
845
983
|
await options.onUsage?.(usage);
|
|
846
984
|
}
|
|
847
|
-
|
|
985
|
+
if (isFailedCompactionProviderResponse(response)) {
|
|
986
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(response));
|
|
987
|
+
}
|
|
988
|
+
const summary = extractResponseOutputText(response).trim();
|
|
989
|
+
if (!summary) {
|
|
990
|
+
throw new EmptyCompactionSummaryError(compactionResponseDiagnostics(response, summary));
|
|
991
|
+
}
|
|
992
|
+
return summary;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function isFailedCompactionProviderResponse(response: unknown): boolean {
|
|
996
|
+
if (!response || typeof response !== "object") return false;
|
|
997
|
+
const record = response as Record<string, unknown>;
|
|
998
|
+
return (
|
|
999
|
+
record.status === "failed" ||
|
|
1000
|
+
record.status === "incomplete" ||
|
|
1001
|
+
(record.error !== null && record.error !== undefined)
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/** Bounded provider diagnostics: identifiers and classifications, never messages or model data. */
|
|
1006
|
+
export function compactionProviderFailureDiagnostics(error: unknown): Record<string, unknown> {
|
|
1007
|
+
let current = error;
|
|
1008
|
+
let errorName: string | null = null;
|
|
1009
|
+
let httpStatus: number | null = null;
|
|
1010
|
+
let responseStatus: string | null = null;
|
|
1011
|
+
let responseId: string | null = null;
|
|
1012
|
+
let code: string | null = null;
|
|
1013
|
+
let type: string | null = null;
|
|
1014
|
+
let requestId: string | null = null;
|
|
1015
|
+
let eventType: string | null = null;
|
|
1016
|
+
const seen = new Set<object>();
|
|
1017
|
+
for (let depth = 0; depth < 6 && current && typeof current === "object"; depth += 1) {
|
|
1018
|
+
if (seen.has(current)) break;
|
|
1019
|
+
seen.add(current);
|
|
1020
|
+
const record = current as Record<string, unknown>;
|
|
1021
|
+
if (!errorName && current instanceof Error) {
|
|
1022
|
+
errorName = boundCompactionDiagnosticField(current.name);
|
|
1023
|
+
}
|
|
1024
|
+
if (
|
|
1025
|
+
httpStatus === null &&
|
|
1026
|
+
typeof record.status === "number" &&
|
|
1027
|
+
Number.isFinite(record.status)
|
|
1028
|
+
) {
|
|
1029
|
+
httpStatus = record.status;
|
|
1030
|
+
}
|
|
1031
|
+
if (responseStatus === null && typeof record.status === "string") {
|
|
1032
|
+
responseStatus = boundCompactionDiagnosticField(record.status);
|
|
1033
|
+
}
|
|
1034
|
+
const directResponseStatus = record.response_status ?? record.responseStatus;
|
|
1035
|
+
if (responseStatus === null && typeof directResponseStatus === "string") {
|
|
1036
|
+
responseStatus = boundCompactionDiagnosticField(directResponseStatus);
|
|
1037
|
+
}
|
|
1038
|
+
const directResponseId = record.response_id ?? record.responseId ?? record.id;
|
|
1039
|
+
if (responseId === null && typeof directResponseId === "string") {
|
|
1040
|
+
responseId = boundCompactionDiagnosticField(directResponseId);
|
|
1041
|
+
}
|
|
1042
|
+
if (code === null && typeof record.code === "string") {
|
|
1043
|
+
code = boundCompactionDiagnosticField(record.code);
|
|
1044
|
+
}
|
|
1045
|
+
if (type === null && typeof record.type === "string") {
|
|
1046
|
+
type = boundCompactionDiagnosticField(record.type);
|
|
1047
|
+
}
|
|
1048
|
+
const directEventType = record.event_type ?? record.eventType;
|
|
1049
|
+
if (eventType === null && typeof directEventType === "string") {
|
|
1050
|
+
eventType = boundCompactionDiagnosticField(directEventType);
|
|
1051
|
+
}
|
|
1052
|
+
if (requestId === null) {
|
|
1053
|
+
const directRequestId = record.request_id ?? record.requestId ?? record._request_id;
|
|
1054
|
+
if (typeof directRequestId === "string") {
|
|
1055
|
+
requestId = boundCompactionDiagnosticField(directRequestId);
|
|
1056
|
+
}
|
|
1057
|
+
const headers = record.headers;
|
|
1058
|
+
if (!requestId && headers && typeof headers === "object") {
|
|
1059
|
+
const get = (headers as { get?: unknown }).get;
|
|
1060
|
+
if (typeof get === "function") {
|
|
1061
|
+
const headerId = get.call(headers, "x-request-id");
|
|
1062
|
+
if (typeof headerId === "string") {
|
|
1063
|
+
requestId = boundCompactionDiagnosticField(headerId);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
const nestedError = record.error;
|
|
1069
|
+
if (nestedError && typeof nestedError === "object" && !seen.has(nestedError)) {
|
|
1070
|
+
const nested = nestedError as Record<string, unknown>;
|
|
1071
|
+
if (code === null && typeof nested.code === "string") {
|
|
1072
|
+
code = boundCompactionDiagnosticField(nested.code);
|
|
1073
|
+
}
|
|
1074
|
+
if (type === null && typeof nested.type === "string") {
|
|
1075
|
+
type = boundCompactionDiagnosticField(nested.type);
|
|
1076
|
+
}
|
|
1077
|
+
const nestedResponseStatus = nested.response_status ?? nested.responseStatus;
|
|
1078
|
+
if (responseStatus === null && typeof nestedResponseStatus === "string") {
|
|
1079
|
+
responseStatus = boundCompactionDiagnosticField(nestedResponseStatus);
|
|
1080
|
+
}
|
|
1081
|
+
const nestedResponseId = nested.response_id ?? nested.responseId ?? nested.id;
|
|
1082
|
+
if (responseId === null && typeof nestedResponseId === "string") {
|
|
1083
|
+
responseId = boundCompactionDiagnosticField(nestedResponseId);
|
|
1084
|
+
}
|
|
1085
|
+
const nestedEventType = nested.event_type ?? nested.eventType;
|
|
1086
|
+
if (eventType === null && typeof nestedEventType === "string") {
|
|
1087
|
+
eventType = boundCompactionDiagnosticField(nestedEventType);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
current = record.cause;
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
errorName,
|
|
1094
|
+
httpStatus,
|
|
1095
|
+
responseStatus,
|
|
1096
|
+
responseId,
|
|
1097
|
+
code,
|
|
1098
|
+
type,
|
|
1099
|
+
requestId,
|
|
1100
|
+
...(eventType ? { eventType } : {}),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES = 256;
|
|
1105
|
+
const COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER = "…[truncated]";
|
|
1106
|
+
|
|
1107
|
+
function boundCompactionDiagnosticField(value: string): string {
|
|
1108
|
+
const bytes = Buffer.from(value, "utf8");
|
|
1109
|
+
if (bytes.length <= COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES) return value;
|
|
1110
|
+
const markerBytes = Buffer.byteLength(COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER, "utf8");
|
|
1111
|
+
let end = COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES - markerBytes;
|
|
1112
|
+
while (end > 0 && isUtf8ContinuationByte(bytes[end]!)) end -= 1;
|
|
1113
|
+
return `${bytes.subarray(0, end).toString("utf8")}${COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER}`;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function isUtf8ContinuationByte(value: number): boolean {
|
|
1117
|
+
return (value & 0xc0) === 0x80;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Bounded, content-free diagnostics for a semantically empty checkpoint. */
|
|
1121
|
+
export function compactionResponseDiagnostics(
|
|
1122
|
+
response: unknown,
|
|
1123
|
+
extractedText = extractResponseOutputText(response).trim(),
|
|
1124
|
+
): Record<string, unknown> {
|
|
1125
|
+
if (!response || typeof response !== "object") {
|
|
1126
|
+
return { responseShape: typeof response, extractedTextLength: extractedText.length };
|
|
1127
|
+
}
|
|
1128
|
+
const record = response as Record<string, unknown>;
|
|
1129
|
+
const output = Array.isArray(record.output) ? record.output : [];
|
|
1130
|
+
const outputItems = output.slice(0, 50).map((item) => {
|
|
1131
|
+
if (!item || typeof item !== "object") return { type: typeof item };
|
|
1132
|
+
const value = item as Record<string, unknown>;
|
|
1133
|
+
const content = Array.isArray(value.content) ? value.content : [];
|
|
1134
|
+
return {
|
|
1135
|
+
type: typeof value.type === "string" ? value.type : null,
|
|
1136
|
+
role: typeof value.role === "string" ? value.role : null,
|
|
1137
|
+
status: typeof value.status === "string" ? value.status : null,
|
|
1138
|
+
contentPartTypes: content
|
|
1139
|
+
.slice(0, 50)
|
|
1140
|
+
.map((part) =>
|
|
1141
|
+
part && typeof part === "object" && typeof (part as { type?: unknown }).type === "string"
|
|
1142
|
+
? (part as { type: string }).type
|
|
1143
|
+
: typeof part,
|
|
1144
|
+
),
|
|
1145
|
+
contentPartCount: content.length,
|
|
1146
|
+
};
|
|
1147
|
+
});
|
|
1148
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
1149
|
+
const usage = modelResponseUsageFromResponse(response)?.usage;
|
|
1150
|
+
const incomplete =
|
|
1151
|
+
record.incomplete_details && typeof record.incomplete_details === "object"
|
|
1152
|
+
? (record.incomplete_details as Record<string, unknown>)
|
|
1153
|
+
: null;
|
|
1154
|
+
return {
|
|
1155
|
+
responseId: typeof record.id === "string" ? record.id : null,
|
|
1156
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
1157
|
+
outputItemCount: output.length,
|
|
1158
|
+
outputItems,
|
|
1159
|
+
choiceCount: choices.length,
|
|
1160
|
+
finishReasons: choices
|
|
1161
|
+
.slice(0, 20)
|
|
1162
|
+
.map((choice) =>
|
|
1163
|
+
choice && typeof choice === "object"
|
|
1164
|
+
? ((choice as Record<string, unknown>).finish_reason ?? null)
|
|
1165
|
+
: null,
|
|
1166
|
+
),
|
|
1167
|
+
incompleteReason:
|
|
1168
|
+
incomplete && typeof incomplete.reason === "string" ? incomplete.reason : null,
|
|
1169
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
1170
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
1171
|
+
totalTokens: usage?.totalTokens ?? null,
|
|
1172
|
+
extractedTextLength: extractedText.length,
|
|
1173
|
+
};
|
|
848
1174
|
}
|
|
849
1175
|
|
|
850
1176
|
/**
|
|
@@ -909,10 +1235,50 @@ class CompactionResponsesModel extends OpenAIResponsesModel {
|
|
|
909
1235
|
}
|
|
910
1236
|
|
|
911
1237
|
export type GitTokenSeeds = Partial<Record<GitCredentialProvider, string>>;
|
|
1238
|
+
export type GitCredentialBindingSeed = {
|
|
1239
|
+
credentialBindingId: string;
|
|
1240
|
+
provider: GitCredentialProvider;
|
|
1241
|
+
token: string;
|
|
1242
|
+
transport?: GitCredentialTransport;
|
|
1243
|
+
expiresAt?: string;
|
|
1244
|
+
/** Total active bindings for this provider, used to suppress unsafe aliases. */
|
|
1245
|
+
providerBindingCount?: number;
|
|
1246
|
+
};
|
|
912
1247
|
export type GitCredentialTokenWriterSession = SandboxSessionLike;
|
|
1248
|
+
export type ToolspaceTokenWriterSession = SandboxSessionLike;
|
|
1249
|
+
|
|
1250
|
+
export type EffectiveSkillSelection = Readonly<{
|
|
1251
|
+
id: string;
|
|
1252
|
+
name: string;
|
|
1253
|
+
source: "bundled" | "library" | "pack";
|
|
1254
|
+
version: string | null;
|
|
1255
|
+
contentSha256: string | null;
|
|
1256
|
+
reason: string;
|
|
1257
|
+
}>;
|
|
1258
|
+
|
|
1259
|
+
const agentSkillSelections = new WeakMap<object, readonly EffectiveSkillSelection[]>();
|
|
1260
|
+
const emptySkillSelections: readonly EffectiveSkillSelection[] = Object.freeze([]);
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Read-only, secret-free skill provenance for an already-built agent. This is
|
|
1264
|
+
* intentionally separate from the model instructions and sandbox manifest so
|
|
1265
|
+
* effective configuration inspection cannot accidentally expose credentials or
|
|
1266
|
+
* turn skill activation into an authorization change.
|
|
1267
|
+
*/
|
|
1268
|
+
export function effectiveSkillSelectionsForAgent(
|
|
1269
|
+
agent: object,
|
|
1270
|
+
): readonly EffectiveSkillSelection[] {
|
|
1271
|
+
return agentSkillSelections.get(agent) ?? emptySkillSelections;
|
|
1272
|
+
}
|
|
913
1273
|
|
|
914
1274
|
export type BuildAgentOptions = {
|
|
915
1275
|
model?: Model;
|
|
1276
|
+
/** Settled response for the one internal human-input interruption resumed by this run. */
|
|
1277
|
+
humanInputResponse?: {
|
|
1278
|
+
requestId: string;
|
|
1279
|
+
toolCallId: string;
|
|
1280
|
+
response: HumanInputResponse;
|
|
1281
|
+
};
|
|
916
1282
|
reasoningEffort?: ReasoningEffort;
|
|
917
1283
|
// Per-turn gating overrides for the multi-provider path. Each defaults to
|
|
918
1284
|
// today's settings-derived behaviour when omitted, so the legacy
|
|
@@ -1008,11 +1374,18 @@ export type BuildAgentOptions = {
|
|
|
1008
1374
|
// repository-clone hooks; runStream forwards them into the hook context, which
|
|
1009
1375
|
// seeds provider token FILES before the clone/setup runs.
|
|
1010
1376
|
gitTokenSeeds?: GitTokenSeeds;
|
|
1377
|
+
// Provider-neutral, independently mintable credentials. Binding ids remain
|
|
1378
|
+
// off-manifest and are hashed before they influence sandbox paths.
|
|
1379
|
+
gitCredentialBindings?: GitCredentialBindingSeed[];
|
|
1011
1380
|
// TOOLSPACE: the run-scoped delegated token to seed into
|
|
1012
1381
|
// $OPENGENI_TOOLSPACE_TOKEN_FILE. Like gitTokenSeed, this stays off the
|
|
1013
1382
|
// manifest/env delta and is written into the sandbox filesystem by a lifecycle
|
|
1014
1383
|
// hook before the agent starts.
|
|
1015
1384
|
toolspaceTokenSeed?: string;
|
|
1385
|
+
// Durable OpenGeni session identity used only to derive the off-manifest,
|
|
1386
|
+
// per-session token file. Required together with toolspaceTokenSeed so two
|
|
1387
|
+
// sessions sharing one box never overwrite the same pointer.
|
|
1388
|
+
toolspaceTokenSessionId?: string;
|
|
1016
1389
|
// Genesis turn only: inject a one-shot instruction into the FIRST model
|
|
1017
1390
|
// call telling it to title the session via opengeni__set_session_title.
|
|
1018
1391
|
// Keeping this out of the persistent Agent.instructions prevents every
|
|
@@ -1040,10 +1413,32 @@ export type BuildAgentOptions = {
|
|
|
1040
1413
|
// timeline message. Omitted ⇒ the composed instructions are byte-identical to
|
|
1041
1414
|
// a workspace-only persona.
|
|
1042
1415
|
sessionInstructions?: string;
|
|
1416
|
+
// Host context for this exact accepted turn. Composed system-level after the
|
|
1417
|
+
// durable session persona and omitted from all later turns.
|
|
1418
|
+
turnInstructions?: string;
|
|
1043
1419
|
// Skills delivered by enabled capability packs. They join the bundled
|
|
1044
1420
|
// skills in the sandbox skill index (mounted under .agents/) so
|
|
1045
1421
|
// skills/<name> references resolve like any other indexed skill.
|
|
1046
1422
|
packSkills?: PackSkill[];
|
|
1423
|
+
// Explicitly selected, immutable curated-library skill content. These are
|
|
1424
|
+
// separate from packSkills so repository-local/pack compatibility does not
|
|
1425
|
+
// turn a curated entry into a mutable override.
|
|
1426
|
+
skillLibrarySkills?: PackSkill[];
|
|
1427
|
+
// Secret-free provenance for effective-configuration inspection. The worker
|
|
1428
|
+
// resolves exact ids/versions/hashes before constructing the agent.
|
|
1429
|
+
skillLibrarySelections?: EffectiveSkillSelection[];
|
|
1430
|
+
/**
|
|
1431
|
+
* Internal per-attempt cancellation boundary. The worker supplies Temporal's
|
|
1432
|
+
* signal so an in-flight shell process is interrupted immediately instead of
|
|
1433
|
+
* holding Steer/Pause behind its requested yield or natural exit.
|
|
1434
|
+
*/
|
|
1435
|
+
turnCancellationSignal?: AbortSignal;
|
|
1436
|
+
/**
|
|
1437
|
+
* Receives the physical sandbox-tool fence at agent construction time. A
|
|
1438
|
+
* cancelled/fenced worker attempt must await it before publishing its durable
|
|
1439
|
+
* attempt-quiesced receipt.
|
|
1440
|
+
*/
|
|
1441
|
+
onToolCancellationFence?: (fence: TurnToolCancellationFence) => void;
|
|
1047
1442
|
};
|
|
1048
1443
|
|
|
1049
1444
|
export type PackSkillFile = {
|
|
@@ -1171,6 +1566,12 @@ export function appendSessionInstructions(composed: string, sessionInstructions?
|
|
|
1171
1566
|
return trimmed ? `${composed} ${trimmed}` : composed;
|
|
1172
1567
|
}
|
|
1173
1568
|
|
|
1569
|
+
/** Append system instructions that apply to this exact turn only. */
|
|
1570
|
+
export function appendTurnInstructions(composed: string, turnInstructions?: string): string {
|
|
1571
|
+
const trimmed = turnInstructions?.trim();
|
|
1572
|
+
return trimmed ? `${composed} ${trimmed}` : composed;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1174
1575
|
/**
|
|
1175
1576
|
* Append the durable session metadata the model otherwise cannot observe.
|
|
1176
1577
|
* This is deliberately declarative and excludes the title text: the title is
|
|
@@ -1239,7 +1640,9 @@ const agentRepositoryCloneHooks = new WeakMap<object, SandboxLifecycleHook[]>();
|
|
|
1239
1640
|
// provided-session env; runStream reads them to build the clone hook context.
|
|
1240
1641
|
// Absent when no brokered repo is attached / on the selfhosted path.
|
|
1241
1642
|
const agentGitTokenSeeds = new WeakMap<object, GitTokenSeeds>();
|
|
1643
|
+
const agentGitCredentialBindings = new WeakMap<object, GitCredentialBindingSeed[]>();
|
|
1242
1644
|
const agentToolspaceTokenSeed = new WeakMap<object, string>();
|
|
1645
|
+
const agentToolspaceTokenSessionId = new WeakMap<object, string>();
|
|
1243
1646
|
// A genesis directive is consumed by runAgentStream exactly once for the
|
|
1244
1647
|
// freshly-built agent. It must not remain in Agent.instructions: those
|
|
1245
1648
|
// instructions are presented again on every internal model/tool loop.
|
|
@@ -1308,6 +1711,9 @@ export function buildOpenGeniAgent(
|
|
|
1308
1711
|
resources: ResourceRef[],
|
|
1309
1712
|
options: BuildAgentOptions = {},
|
|
1310
1713
|
): Agent<any, any> {
|
|
1714
|
+
if (Boolean(options.toolspaceTokenSeed) !== Boolean(options.toolspaceTokenSessionId)) {
|
|
1715
|
+
throw new Error("toolspaceTokenSeed and toolspaceTokenSessionId must be supplied together");
|
|
1716
|
+
}
|
|
1311
1717
|
// Resolved per-turn gating. Each override defaults to today's settings-derived
|
|
1312
1718
|
// behaviour, so the legacy global-client callers (no resolved model) build the
|
|
1313
1719
|
// exact same agent as before; the multi-provider worker path passes the
|
|
@@ -1329,6 +1735,28 @@ export function buildOpenGeniAgent(
|
|
|
1329
1735
|
// [...agent.tools, ...capability.tools()]), so hosted web_search coexists with
|
|
1330
1736
|
// both rather than overriding them.
|
|
1331
1737
|
const hostedTools = hostedWebSearch ? [webSearchTool()] : [];
|
|
1738
|
+
const humanInputTool = agentTool({
|
|
1739
|
+
name: HUMAN_INPUT_TOOL_NAME,
|
|
1740
|
+
description:
|
|
1741
|
+
"Pause this turn and request structured human input. Use for decisions or missing information that only a person can provide. Supports free text, single-select, multi-select, an optional Other value, multiple questions, explicit skip policy, and an optional expiry.",
|
|
1742
|
+
parameters: RequestHumanInputToolInput,
|
|
1743
|
+
needsApproval: true,
|
|
1744
|
+
// A missing/mismatched durable response is a protocol integrity failure,
|
|
1745
|
+
// not model-visible tool output the agent may reason past.
|
|
1746
|
+
errorFunction: null,
|
|
1747
|
+
execute: (_input, _context, details) => {
|
|
1748
|
+
const settled = options.humanInputResponse;
|
|
1749
|
+
if (!settled) {
|
|
1750
|
+
throw new Error("Human-input tool resumed without a durable response");
|
|
1751
|
+
}
|
|
1752
|
+
const resumedCallId = details?.toolCall?.callId;
|
|
1753
|
+
if (resumedCallId && resumedCallId !== settled.toolCallId) {
|
|
1754
|
+
throw new Error("Human-input response does not belong to the resumed tool call");
|
|
1755
|
+
}
|
|
1756
|
+
return JSON.stringify({ requestId: settled.requestId, ...settled.response });
|
|
1757
|
+
},
|
|
1758
|
+
});
|
|
1759
|
+
const agentTools = [...hostedTools, humanInputTool];
|
|
1332
1760
|
const baseConfig = {
|
|
1333
1761
|
name: "OpenGeni Agent",
|
|
1334
1762
|
model: options.model ?? settings.openaiModel,
|
|
@@ -1351,24 +1779,28 @@ export function buildOpenGeniAgent(
|
|
|
1351
1779
|
// and the worker resolved a nonblank block — appendWorkspaceMemory,
|
|
1352
1780
|
// 4. + the per-session persona instructions (session-specific, so it
|
|
1353
1781
|
// refines both the workspace persona and the substrate note),
|
|
1354
|
-
// 5. +
|
|
1782
|
+
// 5. + host context for this exact turn, when supplied,
|
|
1783
|
+
// 6. + durable session-setting state (title present + child notification
|
|
1355
1784
|
// mode), when supplied by the worker,
|
|
1356
1785
|
// The genesis title directive is deliberately NOT part of this persistent
|
|
1357
1786
|
// string. runAgentStream injects it into the first model call only.
|
|
1358
1787
|
instructions: appendPersistentSessionSettings(
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1788
|
+
appendTurnInstructions(
|
|
1789
|
+
appendSessionInstructions(
|
|
1790
|
+
appendWorkspaceMemory(
|
|
1791
|
+
appendToolspaceInstructions(
|
|
1792
|
+
composeAgentInstructions(
|
|
1793
|
+
options.instructionsTemplate ?? settings.agentInstructionsTemplate,
|
|
1794
|
+
options.workspaceEnvironment,
|
|
1795
|
+
options.rig,
|
|
1796
|
+
),
|
|
1797
|
+
settings.toolspaceEnabled && Boolean(options.toolspaceTokenSeed),
|
|
1366
1798
|
),
|
|
1367
|
-
|
|
1799
|
+
options.workspaceMemory,
|
|
1368
1800
|
),
|
|
1369
|
-
options.
|
|
1801
|
+
options.sessionInstructions,
|
|
1370
1802
|
),
|
|
1371
|
-
options.
|
|
1803
|
+
options.turnInstructions,
|
|
1372
1804
|
),
|
|
1373
1805
|
options.persistentSessionSettings,
|
|
1374
1806
|
),
|
|
@@ -1391,7 +1823,7 @@ export function buildOpenGeniAgent(
|
|
|
1391
1823
|
// `new Agent(baseConfig)` path (sandboxBackend === "none") and the
|
|
1392
1824
|
// `new SandboxAgent({ ...baseConfig, ... })` path via the shared baseConfig
|
|
1393
1825
|
// spread; the SDK concatenates these with MCP and sandbox capability tools.
|
|
1394
|
-
|
|
1826
|
+
tools: agentTools,
|
|
1395
1827
|
...(options.mcpServers?.length ? { mcpServers: options.mcpServers } : {}),
|
|
1396
1828
|
// Surface FAILED MCP tool calls as `{ isError: true }` tool output (see
|
|
1397
1829
|
// mcpToolErrorFunction / mcpToolErrorOutput) instead of the SDK's default
|
|
@@ -1423,6 +1855,9 @@ export function buildOpenGeniAgent(
|
|
|
1423
1855
|
),
|
|
1424
1856
|
...(runAs ? { runAs } : {}),
|
|
1425
1857
|
capabilities: buildAgentCapabilities(settings, options.packSkills ?? [], {
|
|
1858
|
+
...(options.skillLibrarySkills?.length
|
|
1859
|
+
? { skillLibrarySkills: options.skillLibrarySkills }
|
|
1860
|
+
: {}),
|
|
1426
1861
|
...(options.structuredToolTransport !== undefined
|
|
1427
1862
|
? { structuredToolTransport: options.structuredToolTransport }
|
|
1428
1863
|
: {}),
|
|
@@ -1430,8 +1865,24 @@ export function buildOpenGeniAgent(
|
|
|
1430
1865
|
? { computerToolMode: options.computerToolMode }
|
|
1431
1866
|
: {}),
|
|
1432
1867
|
...(options.onComputerUseReady ? { onComputerUseReady: options.onComputerUseReady } : {}),
|
|
1868
|
+
...(options.turnCancellationSignal
|
|
1869
|
+
? { turnCancellationSignal: options.turnCancellationSignal }
|
|
1870
|
+
: {}),
|
|
1871
|
+
...(options.onToolCancellationFence
|
|
1872
|
+
? { onToolCancellationFence: options.onToolCancellationFence }
|
|
1873
|
+
: {}),
|
|
1433
1874
|
}),
|
|
1434
1875
|
});
|
|
1876
|
+
agentSkillSelections.set(
|
|
1877
|
+
agent,
|
|
1878
|
+
Object.freeze(
|
|
1879
|
+
effectiveSkillSelections(
|
|
1880
|
+
options.skillLibrarySelections ?? [],
|
|
1881
|
+
options.skillLibrarySkills ?? [],
|
|
1882
|
+
options.packSkills ?? [],
|
|
1883
|
+
).map((selection) => Object.freeze(selection)),
|
|
1884
|
+
),
|
|
1885
|
+
);
|
|
1435
1886
|
if (options.genesisTitleHint) {
|
|
1436
1887
|
agentsNeedingGenesisTitleDirective.add(agent);
|
|
1437
1888
|
}
|
|
@@ -1463,8 +1914,12 @@ export function buildOpenGeniAgent(
|
|
|
1463
1914
|
if (Object.keys(gitTokenSeeds).length > 0) {
|
|
1464
1915
|
agentGitTokenSeeds.set(agent, gitTokenSeeds);
|
|
1465
1916
|
}
|
|
1917
|
+
if (options.gitCredentialBindings && options.gitCredentialBindings.length > 0) {
|
|
1918
|
+
agentGitCredentialBindings.set(agent, options.gitCredentialBindings);
|
|
1919
|
+
}
|
|
1466
1920
|
if (options.toolspaceTokenSeed) {
|
|
1467
1921
|
agentToolspaceTokenSeed.set(agent, options.toolspaceTokenSeed);
|
|
1922
|
+
agentToolspaceTokenSessionId.set(agent, options.toolspaceTokenSessionId!);
|
|
1468
1923
|
}
|
|
1469
1924
|
// M3: stash the rig setup descriptor + RESOLVE the rig credential hooks now.
|
|
1470
1925
|
// sandboxLifecycleHooksForIds throws on an unknown hook name, so a typo'd rig
|
|
@@ -1504,14 +1959,17 @@ function maybeInstallCodexToolSearch(
|
|
|
1504
1959
|
}
|
|
1505
1960
|
|
|
1506
1961
|
/** True when the unprefixed tool `name` requires approval under `policy`. */
|
|
1507
|
-
function mcpToolRequiresApproval(
|
|
1508
|
-
|
|
1962
|
+
function mcpToolRequiresApproval(
|
|
1963
|
+
policy: boolean | ReadonlySet<string>,
|
|
1964
|
+
unprefixedName: string,
|
|
1965
|
+
): boolean {
|
|
1966
|
+
return policy === true || (policy !== false && policy.has(unprefixedName));
|
|
1509
1967
|
}
|
|
1510
1968
|
|
|
1511
1969
|
/** A per-server approval policy keyed by the server's `<id>__` tool prefix. */
|
|
1512
1970
|
type McpApprovalPolicy = {
|
|
1513
1971
|
prefix: string;
|
|
1514
|
-
requireApproval: boolean | string
|
|
1972
|
+
requireApproval: boolean | ReadonlySet<string>;
|
|
1515
1973
|
};
|
|
1516
1974
|
|
|
1517
1975
|
/** The subset of the agent surface the approval wrap needs — including `clone`. */
|
|
@@ -1602,7 +2060,8 @@ function applyMcpApprovalPolicy(agent: Agent<any, any>, settings: Settings): voi
|
|
|
1602
2060
|
)
|
|
1603
2061
|
.map((server) => ({
|
|
1604
2062
|
prefix: prefixedMcpToolName(server.id, ""),
|
|
1605
|
-
requireApproval:
|
|
2063
|
+
requireApproval:
|
|
2064
|
+
server.requireApproval === true ? true : new Set(server.requireApproval as string[]),
|
|
1606
2065
|
}))
|
|
1607
2066
|
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
1608
2067
|
if (policies.length === 0) {
|
|
@@ -1721,6 +2180,7 @@ export function buildAgentCapabilities(
|
|
|
1721
2180
|
settings: Settings,
|
|
1722
2181
|
packSkills: PackSkill[],
|
|
1723
2182
|
options: {
|
|
2183
|
+
skillLibrarySkills?: PackSkill[];
|
|
1724
2184
|
structuredToolTransport?: boolean;
|
|
1725
2185
|
// EXPLICIT computer-use transport (see BuildAgentOptions.computerToolMode). When
|
|
1726
2186
|
// present, computerUse() is handed the mode directly and its tools() obeys it
|
|
@@ -1728,8 +2188,15 @@ export function buildAgentCapabilities(
|
|
|
1728
2188
|
// imageFunctionResults path (driven by structuredToolTransport) is unchanged.
|
|
1729
2189
|
computerToolMode?: ComputerToolMode;
|
|
1730
2190
|
onComputerUseReady?: (session: SandboxSessionLike) => Promise<void>;
|
|
2191
|
+
turnCancellationSignal?: AbortSignal;
|
|
2192
|
+
onToolCancellationFence?: (fence: TurnToolCancellationFence) => void;
|
|
1731
2193
|
} = {},
|
|
1732
2194
|
): ReturnType<typeof Capabilities.default> {
|
|
2195
|
+
const toolCancellation =
|
|
2196
|
+
options.turnCancellationSignal || options.onToolCancellationFence
|
|
2197
|
+
? createTurnToolCancellationController(options.turnCancellationSignal)
|
|
2198
|
+
: null;
|
|
2199
|
+
if (toolCancellation) options.onToolCancellationFence?.(toolCancellation);
|
|
1733
2200
|
// The `filesystem()` capability picks hosted-vs-function tool variants from the
|
|
1734
2201
|
// bound model instance (supportsApplyPatchTransport / structured tool output).
|
|
1735
2202
|
// When the caller declares the backend does NOT support that structured/hosted
|
|
@@ -1748,9 +2215,13 @@ export function buildAgentCapabilities(
|
|
|
1748
2215
|
}
|
|
1749
2216
|
const caps: ReturnType<typeof Capabilities.default> = [
|
|
1750
2217
|
filesystemCapability,
|
|
1751
|
-
shell({ configureTools: withExecOpCorrelation }),
|
|
2218
|
+
shell({ ...(toolCancellation ? {} : { configureTools: withExecOpCorrelation }) }),
|
|
1752
2219
|
];
|
|
1753
|
-
caps.push(
|
|
2220
|
+
caps.push(
|
|
2221
|
+
skills({
|
|
2222
|
+
lazyFrom: lazySkillSourceWithPackSkills(packSkills, options.skillLibrarySkills ?? []),
|
|
2223
|
+
}),
|
|
2224
|
+
);
|
|
1754
2225
|
// P4.3 computer-use: the agent drives the SAME :0 humans watch (xdotool/XTEST +
|
|
1755
2226
|
// scrot), but only when the desktop tier is ON, computer-use is enabled, and the
|
|
1756
2227
|
// backend is one whose image carries the X stack (descriptorgate — honest about
|
|
@@ -1802,6 +2273,14 @@ export function buildAgentCapabilities(
|
|
|
1802
2273
|
}
|
|
1803
2274
|
caps.push(computerCapability as unknown as ReturnType<typeof Capabilities.default>[number]);
|
|
1804
2275
|
}
|
|
2276
|
+
if (toolCancellation) {
|
|
2277
|
+
for (const capability of caps) {
|
|
2278
|
+
wrapCapabilityToolsForTurnCancellation(
|
|
2279
|
+
capability as unknown as { tools(): Tool<unknown>[] },
|
|
2280
|
+
toolCancellation,
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
1805
2284
|
return caps;
|
|
1806
2285
|
}
|
|
1807
2286
|
|
|
@@ -1843,8 +2322,73 @@ export type PrepareToolsOptions = {
|
|
|
1843
2322
|
input: ResolveConnectionCredentialInput,
|
|
1844
2323
|
) => Promise<ResolveConnectionCredentialResult>;
|
|
1845
2324
|
onAuthNeeded?: (payload: ToolAuthNeededPayload) => Promise<void> | void;
|
|
2325
|
+
/** Injectable final MCP transport for tests and embedded hosts. */
|
|
2326
|
+
mcpFetchImpl?: FetchLike;
|
|
2327
|
+
};
|
|
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>;
|
|
1846
2336
|
};
|
|
1847
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
|
+
|
|
1848
2392
|
export async function prepareAgentTools(
|
|
1849
2393
|
settings: Settings,
|
|
1850
2394
|
tools: ToolRef[],
|
|
@@ -1854,25 +2398,36 @@ export async function prepareAgentTools(
|
|
|
1854
2398
|
// codex_apps sanitizing fetch so every tools/list this turn accumulates the
|
|
1855
2399
|
// account's connector namespaces. Surfaced on PreparedAgentTools for the worker.
|
|
1856
2400
|
const codexConnectorNamespaces = new Set<string>();
|
|
2401
|
+
assertMcpServerSelectionWithinBounds(tools);
|
|
1857
2402
|
if (tools.length === 0) {
|
|
1858
2403
|
return { mcpServers: [], close: async () => {}, codexConnectorNamespaces };
|
|
1859
2404
|
}
|
|
1860
2405
|
const registry = new Map(settings.mcpServers.map((server) => [server.id, server]));
|
|
1861
|
-
const
|
|
1862
|
-
|
|
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) => {
|
|
1863
2412
|
const config = registry.get(tool.id);
|
|
1864
2413
|
if (!config) {
|
|
1865
2414
|
throw new Error(`Unknown MCP server id: ${tool.id}`);
|
|
1866
2415
|
}
|
|
1867
2416
|
const url = firstPartyMcpServerUrlForRun(settings, config, options.workspaceId) ?? config.url;
|
|
2417
|
+
const firstParty = isFirstPartyMcpServer(settings, config);
|
|
1868
2418
|
const baseFetch = isCodexAppsMcpServer(config)
|
|
1869
|
-
? codexAppsSanitizingFetch(
|
|
1870
|
-
:
|
|
2419
|
+
? codexAppsSanitizingFetch(mcpFetchImpl, codexConnectorNamespaces)
|
|
2420
|
+
: mcpFetchImpl;
|
|
2421
|
+
const guardedFetch = guardedMcpFetch(
|
|
2422
|
+
firstParty ? { ...settings, integrationsAllowPrivateNetworkTargets: true } : settings,
|
|
2423
|
+
baseFetch,
|
|
2424
|
+
firstParty ? { requireHttpsOutsideLocalTest: false } : {},
|
|
2425
|
+
);
|
|
1871
2426
|
const fetchImpl = config.connectionRef
|
|
1872
|
-
? connectionBrokerFetch(
|
|
1873
|
-
:
|
|
1874
|
-
? firstPartyAuthFetch(
|
|
1875
|
-
:
|
|
2427
|
+
? connectionBrokerFetch(guardedFetch, config, options)
|
|
2428
|
+
: firstParty
|
|
2429
|
+
? firstPartyAuthFetch(guardedFetch, settings, options)
|
|
2430
|
+
: guardedFetch;
|
|
1876
2431
|
// A server is connected BEST-EFFORT (a connect OR tools-list failure drops
|
|
1877
2432
|
// it — its tools go unavailable for the turn — instead of failing the turn)
|
|
1878
2433
|
// in two cases:
|
|
@@ -1904,7 +2459,7 @@ export async function prepareAgentTools(
|
|
|
1904
2459
|
// MCP SDK's strict Tool schema rejects (fails the turn during tools/list);
|
|
1905
2460
|
// sanitize the response on the wire before validation. The namespace Set
|
|
1906
2461
|
// also captures each tool's original connector namespace (P4 Part B.1).
|
|
1907
|
-
|
|
2462
|
+
fetch: fetchImpl,
|
|
1908
2463
|
...(await mcpServerRequestInit(settings, config)),
|
|
1909
2464
|
...(config.timeoutMs
|
|
1910
2465
|
? {
|
|
@@ -1916,13 +2471,15 @@ export async function prepareAgentTools(
|
|
|
1916
2471
|
config.id,
|
|
1917
2472
|
config.allowedTools,
|
|
1918
2473
|
bestEffort,
|
|
2474
|
+
aggregateToolBudget,
|
|
2475
|
+
`${config.id}:${index}`,
|
|
1919
2476
|
);
|
|
1920
2477
|
return {
|
|
1921
2478
|
server,
|
|
1922
2479
|
bestEffort,
|
|
1923
2480
|
optional,
|
|
1924
2481
|
};
|
|
1925
|
-
}
|
|
2482
|
+
},
|
|
1926
2483
|
);
|
|
1927
2484
|
const requiredServers = servers.filter((entry) => !entry.bestEffort).map((entry) => entry.server);
|
|
1928
2485
|
const bestEffortServers = servers
|
|
@@ -1934,18 +2491,21 @@ export async function prepareAgentTools(
|
|
|
1934
2491
|
const optionalServerNames = new Set(
|
|
1935
2492
|
servers.filter((entry) => entry.optional).map((entry) => entry.server.name),
|
|
1936
2493
|
);
|
|
1937
|
-
const connectedRequired = await
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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
|
+
}
|
|
1947
2504
|
if (connectedBestEffort) {
|
|
1948
2505
|
for (const failed of connectedBestEffort.failed) {
|
|
2506
|
+
if (failed instanceof PrefixedMcpServer) {
|
|
2507
|
+
failed.releaseAggregateBudget();
|
|
2508
|
+
}
|
|
1949
2509
|
if (!optionalServerNames.has(failed.name)) {
|
|
1950
2510
|
continue;
|
|
1951
2511
|
}
|
|
@@ -1959,10 +2519,20 @@ export async function prepareAgentTools(
|
|
|
1959
2519
|
return {
|
|
1960
2520
|
mcpServers: [...connectedRequired.active, ...(connectedBestEffort?.active ?? [])],
|
|
1961
2521
|
close: async () => {
|
|
1962
|
-
|
|
2522
|
+
let firstError: unknown;
|
|
1963
2523
|
if (connectedBestEffort) {
|
|
1964
|
-
|
|
2524
|
+
try {
|
|
2525
|
+
await connectedBestEffort.close();
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
firstError ??= error;
|
|
2528
|
+
}
|
|
1965
2529
|
}
|
|
2530
|
+
try {
|
|
2531
|
+
await connectedRequired.close();
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
firstError ??= error;
|
|
2534
|
+
}
|
|
2535
|
+
if (firstError !== undefined) throw firstError;
|
|
1966
2536
|
},
|
|
1967
2537
|
codexConnectorNamespaces,
|
|
1968
2538
|
};
|
|
@@ -1979,10 +2549,12 @@ function connectionBrokerFetch(
|
|
|
1979
2549
|
}
|
|
1980
2550
|
return async (input, init) => {
|
|
1981
2551
|
const request = await mcpRequestInfo(input, init);
|
|
2552
|
+
const destinationUrl = mcpRequestDestinationUrl(input);
|
|
1982
2553
|
const first = await resolveConnectionForRequest(
|
|
1983
2554
|
options,
|
|
1984
2555
|
config.id,
|
|
1985
2556
|
connectionRef,
|
|
2557
|
+
destinationUrl,
|
|
1986
2558
|
request.toolName,
|
|
1987
2559
|
false,
|
|
1988
2560
|
);
|
|
@@ -1994,10 +2566,12 @@ function connectionBrokerFetch(
|
|
|
1994
2566
|
withConnectionHeaders(input, init, first.headers),
|
|
1995
2567
|
);
|
|
1996
2568
|
if (response.status === 401) {
|
|
2569
|
+
await cancelMcpResponseBody(response);
|
|
1997
2570
|
const refreshed = await resolveConnectionForRequest(
|
|
1998
2571
|
options,
|
|
1999
2572
|
config.id,
|
|
2000
2573
|
connectionRef,
|
|
2574
|
+
destinationUrl,
|
|
2001
2575
|
request.toolName,
|
|
2002
2576
|
true,
|
|
2003
2577
|
);
|
|
@@ -2010,11 +2584,14 @@ function connectionBrokerFetch(
|
|
|
2010
2584
|
);
|
|
2011
2585
|
if (retry.status === 403) {
|
|
2012
2586
|
const auth = insufficientScopeAuth(retry.headers, connectionRef, refreshed.connectionId);
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2587
|
+
if (auth) {
|
|
2588
|
+
await cancelMcpResponseBody(retry);
|
|
2589
|
+
return await authNeededFetchResponse(options, config.id, request, auth, connectionRef);
|
|
2590
|
+
}
|
|
2591
|
+
return retry;
|
|
2016
2592
|
}
|
|
2017
2593
|
if (retry.status === 401) {
|
|
2594
|
+
await cancelMcpResponseBody(retry);
|
|
2018
2595
|
return await authNeededFetchResponse(
|
|
2019
2596
|
options,
|
|
2020
2597
|
config.id,
|
|
@@ -2023,9 +2600,13 @@ function connectionBrokerFetch(
|
|
|
2023
2600
|
status: "auth_needed",
|
|
2024
2601
|
reason: "expired",
|
|
2025
2602
|
providerDomain: connectionRef.providerDomain,
|
|
2603
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2026
2604
|
connectionId: refreshed.connectionId,
|
|
2027
2605
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2028
2606
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2607
|
+
...(connectionRef.selectedResources
|
|
2608
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2609
|
+
: {}),
|
|
2029
2610
|
},
|
|
2030
2611
|
connectionRef,
|
|
2031
2612
|
);
|
|
@@ -2034,9 +2615,11 @@ function connectionBrokerFetch(
|
|
|
2034
2615
|
}
|
|
2035
2616
|
if (response.status === 403) {
|
|
2036
2617
|
const auth = insufficientScopeAuth(response.headers, connectionRef, first.connectionId);
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2618
|
+
if (auth) {
|
|
2619
|
+
await cancelMcpResponseBody(response);
|
|
2620
|
+
return await authNeededFetchResponse(options, config.id, request, auth, connectionRef);
|
|
2621
|
+
}
|
|
2622
|
+
return response;
|
|
2040
2623
|
}
|
|
2041
2624
|
return response;
|
|
2042
2625
|
};
|
|
@@ -2046,6 +2629,7 @@ async function resolveConnectionForRequest(
|
|
|
2046
2629
|
options: PrepareToolsOptions,
|
|
2047
2630
|
serverId: string,
|
|
2048
2631
|
connectionRef: McpServerConnectionRef,
|
|
2632
|
+
destinationUrl: string,
|
|
2049
2633
|
toolName: string | undefined,
|
|
2050
2634
|
forceRefresh: boolean,
|
|
2051
2635
|
): Promise<ResolveConnectionCredentialResult> {
|
|
@@ -2054,15 +2638,20 @@ async function resolveConnectionForRequest(
|
|
|
2054
2638
|
status: "auth_needed",
|
|
2055
2639
|
reason: "missing_connection",
|
|
2056
2640
|
providerDomain: connectionRef.providerDomain,
|
|
2641
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2057
2642
|
...(connectionRef.connectionId ? { connectionId: connectionRef.connectionId } : {}),
|
|
2058
2643
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2059
2644
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2645
|
+
...(connectionRef.selectedResources
|
|
2646
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2647
|
+
: {}),
|
|
2060
2648
|
};
|
|
2061
2649
|
}
|
|
2062
2650
|
const request: ResolveConnectionCredentialInput = {
|
|
2063
2651
|
workspaceId: options.workspaceId,
|
|
2064
2652
|
serverId,
|
|
2065
2653
|
connectionRef,
|
|
2654
|
+
destinationUrl,
|
|
2066
2655
|
forceRefresh,
|
|
2067
2656
|
...(toolName ? { toolName } : {}),
|
|
2068
2657
|
...(options.subjectId ? { subjectId: options.subjectId } : {}),
|
|
@@ -2074,9 +2663,13 @@ async function resolveConnectionForRequest(
|
|
|
2074
2663
|
status: "auth_needed",
|
|
2075
2664
|
reason: "refresh_failed",
|
|
2076
2665
|
providerDomain: connectionRef.providerDomain,
|
|
2666
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2077
2667
|
...(connectionRef.connectionId ? { connectionId: connectionRef.connectionId } : {}),
|
|
2078
2668
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2079
2669
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2670
|
+
...(connectionRef.selectedResources
|
|
2671
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2672
|
+
: {}),
|
|
2080
2673
|
};
|
|
2081
2674
|
}
|
|
2082
2675
|
}
|
|
@@ -2094,6 +2687,7 @@ function insufficientScopeAuth(
|
|
|
2094
2687
|
status: "auth_needed",
|
|
2095
2688
|
reason: "insufficient_scope",
|
|
2096
2689
|
providerDomain: connectionRef.providerDomain,
|
|
2690
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2097
2691
|
connectionId,
|
|
2098
2692
|
...(challenge.scope?.length
|
|
2099
2693
|
? { scopes: challenge.scope }
|
|
@@ -2105,6 +2699,9 @@ function insufficientScopeAuth(
|
|
|
2105
2699
|
: connectionRef.resource
|
|
2106
2700
|
? { resource: connectionRef.resource }
|
|
2107
2701
|
: {}),
|
|
2702
|
+
...(connectionRef.selectedResources
|
|
2703
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2704
|
+
: {}),
|
|
2108
2705
|
};
|
|
2109
2706
|
}
|
|
2110
2707
|
|
|
@@ -2120,6 +2717,11 @@ async function authNeededFetchResponse(
|
|
|
2120
2717
|
serverId,
|
|
2121
2718
|
toolName: request.toolName ?? null,
|
|
2122
2719
|
providerDomain: auth.providerDomain,
|
|
2720
|
+
...(auth.provider
|
|
2721
|
+
? { provider: auth.provider }
|
|
2722
|
+
: connectionRef.provider
|
|
2723
|
+
? { provider: connectionRef.provider }
|
|
2724
|
+
: {}),
|
|
2123
2725
|
reason: auth.reason,
|
|
2124
2726
|
...(connectionId ? { connectionId } : {}),
|
|
2125
2727
|
...(auth.scopes
|
|
@@ -2132,6 +2734,11 @@ async function authNeededFetchResponse(
|
|
|
2132
2734
|
: connectionRef.resource
|
|
2133
2735
|
? { resource: connectionRef.resource }
|
|
2134
2736
|
: {}),
|
|
2737
|
+
...(auth.selectedResources
|
|
2738
|
+
? { selectedResources: auth.selectedResources }
|
|
2739
|
+
: connectionRef.selectedResources
|
|
2740
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2741
|
+
: {}),
|
|
2135
2742
|
...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
|
|
2136
2743
|
...(options.subjectId ? { subjectId: options.subjectId } : {}),
|
|
2137
2744
|
});
|
|
@@ -2161,6 +2768,10 @@ type McpRequestInfo = {
|
|
|
2161
2768
|
toolName?: string;
|
|
2162
2769
|
};
|
|
2163
2770
|
|
|
2771
|
+
function mcpRequestDestinationUrl(input: string | URL | Request): string {
|
|
2772
|
+
return new URL(input instanceof Request ? input.url : input.toString()).toString();
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2164
2775
|
async function mcpRequestInfo(
|
|
2165
2776
|
input: string | URL | Request,
|
|
2166
2777
|
init?: RequestInit,
|
|
@@ -2389,7 +3000,7 @@ async function signFirstPartyDelegatedBearer(
|
|
|
2389
3000
|
workspaceId: options.workspaceId,
|
|
2390
3001
|
subjectId: options.subjectId ?? "worker:first-party-mcp",
|
|
2391
3002
|
...(options.subjectLabel ? { subjectLabel: options.subjectLabel } : {}),
|
|
2392
|
-
permissions: options.firstPartyPermissions ??
|
|
3003
|
+
permissions: options.firstPartyPermissions ?? [...DEFAULT_FIRST_PARTY_MCP_PERMISSIONS],
|
|
2393
3004
|
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
2394
3005
|
...(options.turnId ? { turnId: options.turnId } : {}),
|
|
2395
3006
|
...(options.attemptId ? { attemptId: options.attemptId } : {}),
|
|
@@ -2473,22 +3084,6 @@ async function codexAppsMcpRequestInit(
|
|
|
2473
3084
|
// level scopes (billing/account/members/api_keys/workspace:admin) are
|
|
2474
3085
|
// intentionally excluded: they gate no first-party tool and are not agent
|
|
2475
3086
|
// capabilities. (A finer-grained capability model comes later.)
|
|
2476
|
-
const firstPartyMcpPermissions: Permission[] = [
|
|
2477
|
-
"workspace:read",
|
|
2478
|
-
"files:read",
|
|
2479
|
-
"documents:search",
|
|
2480
|
-
"scheduled_tasks:manage",
|
|
2481
|
-
"scheduled_tasks:run",
|
|
2482
|
-
"goals:manage",
|
|
2483
|
-
"sessions:read",
|
|
2484
|
-
"sessions:create",
|
|
2485
|
-
"sessions:control",
|
|
2486
|
-
"variable-sets:use",
|
|
2487
|
-
"variable-sets:manage",
|
|
2488
|
-
"rigs:use",
|
|
2489
|
-
"github:use",
|
|
2490
|
-
];
|
|
2491
|
-
|
|
2492
3087
|
// codex_apps is third-party-by-trust (the external ChatGPT connectors backend)
|
|
2493
3088
|
// but needs DYNAMIC auth, so it is its own category — deliberately NOT folded
|
|
2494
3089
|
// into the first-party allowlist, which would wrongly sign an OpenGeni delegated
|
|
@@ -2588,12 +3183,15 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2588
3183
|
// doesn't spam the log when the SDK re-lists across model turns.
|
|
2589
3184
|
private readonly bestEffort: boolean;
|
|
2590
3185
|
private loggedListToolsFailure = false;
|
|
3186
|
+
private listedToolSchemaTokens = 0;
|
|
2591
3187
|
|
|
2592
3188
|
constructor(
|
|
2593
3189
|
private readonly inner: MCPServer,
|
|
2594
3190
|
registryId: string,
|
|
2595
3191
|
allowedTools?: string[],
|
|
2596
3192
|
bestEffort = false,
|
|
3193
|
+
private readonly aggregateToolBudget?: McpAggregateToolListBudget,
|
|
3194
|
+
private readonly aggregateSourceId = registryId,
|
|
2597
3195
|
) {
|
|
2598
3196
|
this.name = registryId;
|
|
2599
3197
|
this.prefix = prefixedMcpToolName(registryId, "");
|
|
@@ -2607,13 +3205,27 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2607
3205
|
}
|
|
2608
3206
|
|
|
2609
3207
|
close(): Promise<void> {
|
|
3208
|
+
this.releaseAggregateBudget();
|
|
2610
3209
|
return this.inner.close();
|
|
2611
3210
|
}
|
|
2612
3211
|
|
|
3212
|
+
releaseAggregateBudget(): void {
|
|
3213
|
+
this.aggregateToolBudget?.remove(this.aggregateSourceId);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
2613
3216
|
async listTools(): Promise<RuntimeMcpTool[]> {
|
|
2614
|
-
let tools: RuntimeMcpTool[];
|
|
2615
3217
|
try {
|
|
2616
|
-
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;
|
|
2617
3229
|
} catch (error) {
|
|
2618
3230
|
// A REQUIRED server's tools/list failure is fatal (fail-loud default): the
|
|
2619
3231
|
// caller explicitly requested it, so its absence must fail the turn.
|
|
@@ -2642,14 +3254,14 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2642
3254
|
{ serverId: this.name, ...safeMcpErrorFields(error) },
|
|
2643
3255
|
);
|
|
2644
3256
|
}
|
|
3257
|
+
this.releaseAggregateBudget();
|
|
2645
3258
|
return [];
|
|
2646
3259
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
}));
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
/** Latest exact tools/list projection used to build the model request. */
|
|
3263
|
+
modelToolSchemaTokens(): number {
|
|
3264
|
+
return this.listedToolSchemaTokens;
|
|
2653
3265
|
}
|
|
2654
3266
|
|
|
2655
3267
|
async callTool(
|
|
@@ -2662,7 +3274,9 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2662
3274
|
throw new Error(`MCP tool ${unprefixed} is not allowed for server ${this.name}`);
|
|
2663
3275
|
}
|
|
2664
3276
|
try {
|
|
2665
|
-
|
|
3277
|
+
const output = await this.inner.callTool(unprefixed, args, meta);
|
|
3278
|
+
assertMcpPayloadWithinBytes(output, MCP_MAX_TOOL_RESULT_BYTES, "MCP tool result");
|
|
3279
|
+
return output;
|
|
2666
3280
|
} catch (error) {
|
|
2667
3281
|
// The connection broker's auth-needed short-circuit arrives as a thrown
|
|
2668
3282
|
// JSON-RPC error (an inline isError result would be stripped by the SDK
|
|
@@ -2813,22 +3427,25 @@ export async function prepareRunInput(
|
|
|
2813
3427
|
...(sandboxSessionState ? { sandboxSessionState } : {}),
|
|
2814
3428
|
};
|
|
2815
3429
|
}
|
|
2816
|
-
// An
|
|
3430
|
+
// An interrupted tool can only be resumed against a real saved run state. If the
|
|
2817
3431
|
// latest blob is the cleared sentinel the awaiting turn was wiped (the API
|
|
2818
3432
|
// refuses clear in requires_action, so this is a defensive guard) — fail with
|
|
2819
3433
|
// an honest message instead of the cryptic SDK "missing schema version".
|
|
2820
3434
|
if (isClearedRunStateBlob(input.serializedRunState)) {
|
|
2821
3435
|
throw new Error(
|
|
2822
|
-
"Cannot resume an
|
|
3436
|
+
"Cannot resume an interrupted tool: the session context was cleared, so the awaiting run state no longer exists.",
|
|
2823
3437
|
);
|
|
2824
3438
|
}
|
|
2825
3439
|
const state = await RunState.fromString(agent, input.serializedRunState);
|
|
2826
3440
|
const interruptions = state.getInterruptions();
|
|
2827
|
-
const
|
|
3441
|
+
const interruptionId = input.kind === "human_input" ? input.toolCallId : input.approvalId;
|
|
3442
|
+
const target = interruptions.find((item: any) => approvalIdentifier(item) === interruptionId);
|
|
2828
3443
|
if (!target) {
|
|
2829
|
-
throw new Error(`
|
|
3444
|
+
throw new Error(`Interrupted tool not found in saved run state: ${interruptionId}`);
|
|
2830
3445
|
}
|
|
2831
|
-
if (input.
|
|
3446
|
+
if (input.kind === "human_input") {
|
|
3447
|
+
state.approve(target as any);
|
|
3448
|
+
} else if (input.decision === "approve") {
|
|
2832
3449
|
state.approve(target as any);
|
|
2833
3450
|
} else {
|
|
2834
3451
|
state.reject(target as any, input.message ? { message: input.message } : undefined);
|
|
@@ -2842,13 +3459,21 @@ export type RunAgentStreamOptions = {
|
|
|
2842
3459
|
sandboxClient?: unknown;
|
|
2843
3460
|
sandboxEnvironment?: Record<string, string>;
|
|
2844
3461
|
onRuntimeEvent?: (event: NormalizedRuntimeEvent) => Promise<void> | void;
|
|
2845
|
-
|
|
3462
|
+
contextCompactionSignal?: () => ProviderContextTokenSignal | null | undefined;
|
|
2846
3463
|
contextCompactionRequested?: () => boolean | Promise<boolean>;
|
|
2847
3464
|
// Host-managed git credential renewal registration. Called only after the
|
|
2848
3465
|
// initial token-file seed completed on a real provisioned box. The worker
|
|
2849
3466
|
// owns the multi-day timer and uses this pinned, un-proxied session to
|
|
2850
3467
|
// atomically replace token files; runtime never mints credentials itself.
|
|
2851
3468
|
onGitCredentialSessionReady?: (session: GitCredentialTokenWriterSession) => Promise<void> | void;
|
|
3469
|
+
// OpenGeni-minted Toolspace token renewal registration. Called only after the
|
|
3470
|
+
// initial token file reached the real sandbox session.
|
|
3471
|
+
onToolspaceTokenSessionReady?: (session: ToolspaceTokenWriterSession) => Promise<void> | void;
|
|
3472
|
+
// Host-owned run material is seeded off-manifest before setup and every
|
|
3473
|
+
// agent-created process sources the active immutable generation. The worker
|
|
3474
|
+
// owns resolution/renewal/fencing; runtime owns sandbox transport.
|
|
3475
|
+
runCredentialSessionId?: string;
|
|
3476
|
+
onRunCredentialSessionReady?: RunCredentialSessionReady;
|
|
2852
3477
|
// OWNERSHIP INVERSION (P1.2): an externally-owned, already-live sandbox
|
|
2853
3478
|
// session resolved by the per-turn resume-by-id path. When present,
|
|
2854
3479
|
// runAgentStream does NOT build (or resume, or discard) a client — it threads
|
|
@@ -2879,6 +3504,13 @@ export type RunAgentStreamOptions = {
|
|
|
2879
3504
|
// after establish, so runAgentStream must not run it eagerly here.
|
|
2880
3505
|
deferredSetup?: boolean;
|
|
2881
3506
|
};
|
|
3507
|
+
/**
|
|
3508
|
+
* The attempt's authoritative physical sandbox-operation fence. Platform
|
|
3509
|
+
* lifecycle commands use its command runner too, so Steer/Pause can interrupt
|
|
3510
|
+
* repository clone, rig setup, file materialization, and credential seeding
|
|
3511
|
+
* before the attempt-quiesced receipt opens queue admission.
|
|
3512
|
+
*/
|
|
3513
|
+
turnToolCancellationFence?: TurnToolCancellationFence;
|
|
2882
3514
|
// A per-turn model-input filter chained AFTER the provider-item-id strip.
|
|
2883
3515
|
// Used by the genesis-title injection to prepend a hidden, NON-PERSISTED
|
|
2884
3516
|
// directive: a callModelInputFilter mutates only `modelData.input` for each
|
|
@@ -2888,7 +3520,7 @@ export type RunAgentStreamOptions = {
|
|
|
2888
3520
|
};
|
|
2889
3521
|
|
|
2890
3522
|
export type ContextRobustnessFilterOptions = {
|
|
2891
|
-
|
|
3523
|
+
contextCompactionSignal?: () => ProviderContextTokenSignal | null | undefined;
|
|
2892
3524
|
contextCompactionRequested?: () => boolean | Promise<boolean>;
|
|
2893
3525
|
throwOnCompactionNeeded?: boolean;
|
|
2894
3526
|
};
|
|
@@ -2934,9 +3566,10 @@ function takeGenesisTitleInputFilter(agent: Agent<any, any>): CallModelInputFilt
|
|
|
2934
3566
|
// Generic substrate prompting for programmatic tool calling (toolspace). Same
|
|
2935
3567
|
// text for every host; gated per-turn by appendToolspaceInstructions on the
|
|
2936
3568
|
// presence of a minted toolspace token, so it only appears when the sandbox
|
|
2937
|
-
//
|
|
3569
|
+
// exposes $OPENGENI_TOOLSPACE_URL/_TOKEN_FILE. Stock images carry ogtool;
|
|
3570
|
+
// custom environments get only an exact deployment-pinned bootstrap hint.
|
|
2938
3571
|
export const TOOLSPACE_PROGRAMMATIC_DIRECTIVE =
|
|
2939
|
-
"Every tool on your MCP surface is also callable programmatically from the sandbox shell, so scripts can invoke tools without a model round trip per call.
|
|
3572
|
+
"Every tool on your MCP surface is also callable programmatically from the sandbox shell, so scripts can invoke tools without a model round trip per call. If `ogtool` is installed, run `ogtool list` to see the available tools and their input schemas (from tools/list), then `ogtool call <tool-name> '<json-args>'`. If it is absent and both npm and $OPENGENI_OGTOOL_PACKAGE_SPEC are available, run the exact deployment-pinned package with `npm exec --yes --package=\"$OPENGENI_OGTOOL_PACKAGE_SPEC\" -- ogtool ...`; never guess a version or install `latest`. Otherwise POST MCP JSON-RPC directly to $OPENGENI_TOOLSPACE_URL with the bearer token read from $OPENGENI_TOOLSPACE_TOKEN_FILE. Prefer programmatic calls for loops, polling, and bulk filtering: their results stay in the sandbox and do not consume your context window. Tools that require human approval must still be invoked normally — called programmatically they return a typed error.";
|
|
2940
3573
|
|
|
2941
3574
|
/**
|
|
2942
3575
|
* callModelInputFilter that removes provider-assigned item ids (rs_/msg_/fc_…)
|
|
@@ -2995,24 +3628,89 @@ export const elideSupersededViewImagesFilter: CallModelInputFilter = ({ modelDat
|
|
|
2995
3628
|
) as unknown as AgentInputItem[],
|
|
2996
3629
|
});
|
|
2997
3630
|
|
|
3631
|
+
/**
|
|
3632
|
+
* Canonical Codex-style tool-result bound at the final model-input seam. The
|
|
3633
|
+
* identical pure normalizer also runs before conversation rows are persisted,
|
|
3634
|
+
* so this is a live-turn defense rather than a request-only alternate history.
|
|
3635
|
+
*/
|
|
3636
|
+
export function boundModelToolOutputsFilterForSettings(settings: Settings): CallModelInputFilter {
|
|
3637
|
+
return ({ modelData }) => ({
|
|
3638
|
+
...modelData,
|
|
3639
|
+
input: boundModelToolOutputItems(
|
|
3640
|
+
modelData.input as unknown as Array<Record<string, unknown>>,
|
|
3641
|
+
settings.modelToolOutputTruncationTokens,
|
|
3642
|
+
) as unknown as AgentInputItem[],
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
function estimateAgentToolSchemaTokens(agent: Agent<any, any>): number {
|
|
3647
|
+
const localTools = Array.isArray((agent as { tools?: unknown }).tools)
|
|
3648
|
+
? ((agent as { tools: unknown[] }).tools ?? [])
|
|
3649
|
+
: [];
|
|
3650
|
+
const localDescriptors = localTools.map((candidate) => {
|
|
3651
|
+
if (!candidate || typeof candidate !== "object") return candidate;
|
|
3652
|
+
const tool = candidate as Record<string, unknown>;
|
|
3653
|
+
return {
|
|
3654
|
+
type: tool.type,
|
|
3655
|
+
name: tool.name,
|
|
3656
|
+
description: tool.description,
|
|
3657
|
+
parameters: tool.parameters,
|
|
3658
|
+
inputSchema: tool.inputSchema,
|
|
3659
|
+
strict: tool.strict,
|
|
3660
|
+
providerData: tool.providerData,
|
|
3661
|
+
};
|
|
3662
|
+
});
|
|
3663
|
+
const mcpServers = Array.isArray((agent as { mcpServers?: unknown }).mcpServers)
|
|
3664
|
+
? ((agent as { mcpServers: unknown[] }).mcpServers ?? [])
|
|
3665
|
+
: [];
|
|
3666
|
+
const mcpTokens = mcpServers.reduce<number>((total, server) => {
|
|
3667
|
+
const getter = (server as { modelToolSchemaTokens?: () => number } | null)
|
|
3668
|
+
?.modelToolSchemaTokens;
|
|
3669
|
+
return total + (typeof getter === "function" ? getter.call(server) : 0);
|
|
3670
|
+
}, 0);
|
|
3671
|
+
return estimateSerializedValueTokens(localDescriptors) + mcpTokens;
|
|
3672
|
+
}
|
|
3673
|
+
|
|
2998
3674
|
export function contextRobustnessFilterForSettings(
|
|
2999
3675
|
settings: Settings,
|
|
3000
3676
|
options: ContextRobustnessFilterOptions = {},
|
|
3001
3677
|
): CallModelInputFilter {
|
|
3002
3678
|
const thresholdTokens = compactionThresholdTokens(settings);
|
|
3003
|
-
|
|
3679
|
+
let previousRequest: { revision: number; footprint: CompleteModelInputFootprint } | null = null;
|
|
3680
|
+
let requestRevision = 0;
|
|
3681
|
+
return async ({ modelData, agent }) => {
|
|
3004
3682
|
const input = modelData.input;
|
|
3005
3683
|
if (options.throwOnCompactionNeeded) {
|
|
3006
|
-
const reported = options.
|
|
3007
|
-
const
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
:
|
|
3684
|
+
const reported = options.contextCompactionSignal?.();
|
|
3685
|
+
const current: CompleteModelInputFootprint = {
|
|
3686
|
+
input: input as unknown as Array<Record<string, unknown>>,
|
|
3687
|
+
instructionsTokens: estimateSerializedValueTokens(modelData.instructions ?? ""),
|
|
3688
|
+
toolSchemaTokens: estimateAgentToolSchemaTokens(agent),
|
|
3689
|
+
};
|
|
3690
|
+
// Stream consumption can lag the SDK's background model loop. A provider
|
|
3691
|
+
// usage signal is safe only when its response revision belongs to the
|
|
3692
|
+
// immediately preceding request. Never attach a delayed response count to
|
|
3693
|
+
// a newer footprint: doing so can hide all model output produced between
|
|
3694
|
+
// them and recreate an under-counting compaction loop.
|
|
3695
|
+
const boundProvider =
|
|
3696
|
+
reported &&
|
|
3697
|
+
previousRequest &&
|
|
3698
|
+
reported.revision === previousRequest.revision &&
|
|
3699
|
+
hasModelGeneratedItem(current.input)
|
|
3700
|
+
? reported
|
|
3701
|
+
: null;
|
|
3702
|
+
const estimate = estimateCompleteModelInput({
|
|
3703
|
+
current,
|
|
3704
|
+
provider: boundProvider,
|
|
3705
|
+
providerRequestFootprint: boundProvider ? (previousRequest?.footprint ?? null) : null,
|
|
3706
|
+
});
|
|
3707
|
+
const signalTokens = estimate.tokens;
|
|
3708
|
+
previousRequest = { revision: ++requestRevision, footprint: current };
|
|
3011
3709
|
if (await options.contextCompactionRequested?.()) {
|
|
3012
3710
|
throw new CompactionNeededError({
|
|
3013
3711
|
signalTokens,
|
|
3014
3712
|
thresholdTokens,
|
|
3015
|
-
signalSource:
|
|
3713
|
+
signalSource: boundProvider ? "provider" : "estimate",
|
|
3016
3714
|
trigger: "operator",
|
|
3017
3715
|
});
|
|
3018
3716
|
}
|
|
@@ -3020,7 +3718,7 @@ export function contextRobustnessFilterForSettings(
|
|
|
3020
3718
|
throw new CompactionNeededError({
|
|
3021
3719
|
signalTokens,
|
|
3022
3720
|
thresholdTokens,
|
|
3023
|
-
signalSource:
|
|
3721
|
+
signalSource: boundProvider ? "provider" : "estimate",
|
|
3024
3722
|
});
|
|
3025
3723
|
}
|
|
3026
3724
|
}
|
|
@@ -3057,6 +3755,7 @@ export function callModelInputFilterForSettings(
|
|
|
3057
3755
|
const filters: CallModelInputFilter[] = [
|
|
3058
3756
|
normalizeComputerCallsFilter,
|
|
3059
3757
|
elideSupersededViewImagesFilter,
|
|
3758
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
3060
3759
|
];
|
|
3061
3760
|
if (settings.openaiProviderItemIds === "strip") {
|
|
3062
3761
|
filters.push(stripProviderItemIdsFilter);
|
|
@@ -3074,7 +3773,11 @@ export async function runAgentStream(
|
|
|
3074
3773
|
const prepared: PreparedAgentInput =
|
|
3075
3774
|
typeof input === "string" || input instanceof RunState ? { input } : input;
|
|
3076
3775
|
const environment = overrides.sandboxEnvironment ?? collectSandboxEnvironment(settings);
|
|
3776
|
+
const toolspaceTokenFile = toolspaceTokenFileForAgent(agent, environment);
|
|
3077
3777
|
const genesisTitleInputFilter = takeGenesisTitleInputFilter(agent);
|
|
3778
|
+
if (overrides.onRunCredentialSessionReady && !overrides.runCredentialSessionId) {
|
|
3779
|
+
throw new Error("runCredentialSessionId is required when run credential setup is enabled");
|
|
3780
|
+
}
|
|
3078
3781
|
|
|
3079
3782
|
// OWNED PATH (P1.2 ownership inversion): the per-turn resume path injected a
|
|
3080
3783
|
// live, externally-owned box. We thread the live `session` straight into
|
|
@@ -3093,11 +3796,24 @@ export async function runAgentStream(
|
|
|
3093
3796
|
// whose per-op pointer re-read could land these execs on a machine swapped in
|
|
3094
3797
|
// mid-turn.
|
|
3095
3798
|
const setupSession = (overrides.ownedSandbox.setupSession ?? session) as SandboxSessionLike;
|
|
3799
|
+
const credentialAgentSession = overrides.runCredentialSessionId
|
|
3800
|
+
? withRunCredentialsSession(session as SandboxSessionLike, overrides.runCredentialSessionId)
|
|
3801
|
+
: (session as SandboxSessionLike);
|
|
3802
|
+
const agentSession = toolspaceTokenFile
|
|
3803
|
+
? withToolspaceTokenSession(credentialAgentSession, toolspaceTokenFile)
|
|
3804
|
+
: credentialAgentSession;
|
|
3805
|
+
const credentialSetupSession = overrides.runCredentialSessionId
|
|
3806
|
+
? withRunCredentialsSession(setupSession, overrides.runCredentialSessionId)
|
|
3807
|
+
: setupSession;
|
|
3808
|
+
const decoratedSetupSession = toolspaceTokenFile
|
|
3809
|
+
? withToolspaceTokenSession(credentialSetupSession, toolspaceTokenFile)
|
|
3810
|
+
: credentialSetupSession;
|
|
3096
3811
|
// Platform setup (manifest-env pin + beforeAgentStart hooks + file downloads)
|
|
3097
3812
|
// against the UN-proxied established box — the ONE-TRUTH helper shared with the
|
|
3098
3813
|
// lazy provisioner. Eager path: runs here, before the run starts (unchanged).
|
|
3099
3814
|
if (!overrides.ownedSandbox.deferredSetup) {
|
|
3100
|
-
await
|
|
3815
|
+
await overrides.onRunCredentialSessionReady?.(session as SandboxSessionLike);
|
|
3816
|
+
await runOwnedSandboxSetup(agent, session as SandboxSessionLike, decoratedSetupSession, {
|
|
3101
3817
|
settings,
|
|
3102
3818
|
environment,
|
|
3103
3819
|
preparedInput: prepared,
|
|
@@ -3105,7 +3821,17 @@ export async function runAgentStream(
|
|
|
3105
3821
|
? { fileDownloadsMaterialized: true }
|
|
3106
3822
|
: {}),
|
|
3107
3823
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3824
|
+
...(overrides.turnToolCancellationFence
|
|
3825
|
+
? {
|
|
3826
|
+
commandRunner: overrides.turnToolCancellationFence.runSandboxCommand.bind(
|
|
3827
|
+
overrides.turnToolCancellationFence,
|
|
3828
|
+
),
|
|
3829
|
+
}
|
|
3830
|
+
: {}),
|
|
3108
3831
|
});
|
|
3832
|
+
if (toolspaceTokenSeedForAgent(agent)) {
|
|
3833
|
+
await overrides.onToolspaceTokenSessionReady?.(agentSession);
|
|
3834
|
+
}
|
|
3109
3835
|
await overrides.onGitCredentialSessionReady?.(setupSession);
|
|
3110
3836
|
}
|
|
3111
3837
|
const runAs = sandboxRunAs(settings);
|
|
@@ -3115,11 +3841,19 @@ export async function runAgentStream(
|
|
|
3115
3841
|
? withSandboxFileDownloads(ownedClient as SandboxClient, fileDownloads, {
|
|
3116
3842
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3117
3843
|
...(runAs ? { runAs } : {}),
|
|
3844
|
+
...(overrides.turnToolCancellationFence
|
|
3845
|
+
? {
|
|
3846
|
+
commandRunner: overrides.turnToolCancellationFence.runSandboxCommand.bind(
|
|
3847
|
+
overrides.turnToolCancellationFence,
|
|
3848
|
+
),
|
|
3849
|
+
}
|
|
3850
|
+
: {}),
|
|
3118
3851
|
})
|
|
3119
3852
|
: (ownedClient as SandboxClient);
|
|
3120
3853
|
// TOKEN-BROKER (B1): the per-turn git token seed, forwarded OFF-MANIFEST so the
|
|
3121
3854
|
// repository-clone hook seeds it to the box's token file before the clone.
|
|
3122
3855
|
const ownedGitTokenSeeds = gitTokenSeedsForAgent(agent);
|
|
3856
|
+
const ownedGitCredentialBindings = gitCredentialBindingsForAgent(agent);
|
|
3123
3857
|
const ownedToolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
|
|
3124
3858
|
const ownedRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3125
3859
|
const ownedHooks = [
|
|
@@ -3132,6 +3866,9 @@ export async function runAgentStream(
|
|
|
3132
3866
|
rigCredentialHooksForAgent(agent),
|
|
3133
3867
|
),
|
|
3134
3868
|
...sandboxToolspaceTokenHooksForAgent(agent),
|
|
3869
|
+
...toolspaceTokenSessionRegistrationHooks(
|
|
3870
|
+
ownedToolspaceTokenSeed ? overrides.onToolspaceTokenSessionReady : undefined,
|
|
3871
|
+
),
|
|
3135
3872
|
...sandboxRepositoryCloneHooksForAgent(agent),
|
|
3136
3873
|
...gitCredentialSessionRegistrationHooks(overrides.onGitCredentialSessionReady),
|
|
3137
3874
|
];
|
|
@@ -3140,31 +3877,49 @@ export async function runAgentStream(
|
|
|
3140
3877
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3141
3878
|
...(runAs ? { runAs } : {}),
|
|
3142
3879
|
...(ownedGitTokenSeeds ? { gitTokenSeeds: ownedGitTokenSeeds } : {}),
|
|
3880
|
+
...(ownedGitCredentialBindings ? { gitCredentialBindings: ownedGitCredentialBindings } : {}),
|
|
3143
3881
|
...(ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {}),
|
|
3882
|
+
...(toolspaceTokenFile ? { toolspaceTokenFile } : {}),
|
|
3144
3883
|
...(ownedRigSetup ? { rigSetup: ownedRigSetup } : {}),
|
|
3145
3884
|
};
|
|
3146
|
-
// Keep
|
|
3147
|
-
//
|
|
3148
|
-
|
|
3885
|
+
// Keep both credential seeding and lifecycle decoration as a safety net for
|
|
3886
|
+
// any session the SDK does create/resume during this run. They are inert for
|
|
3887
|
+
// the provided session, which remains the normal ownership-inverted path.
|
|
3888
|
+
const credentialResourceClient = overrides.runCredentialSessionId
|
|
3889
|
+
? withRunCredentialsClient(
|
|
3890
|
+
resourceClient,
|
|
3891
|
+
overrides.runCredentialSessionId,
|
|
3892
|
+
overrides.onRunCredentialSessionReady,
|
|
3893
|
+
)
|
|
3894
|
+
: resourceClient;
|
|
3895
|
+
const toolspaceResourceClient = toolspaceTokenFile
|
|
3896
|
+
? withToolspaceTokenClient(credentialResourceClient, toolspaceTokenFile)
|
|
3897
|
+
: credentialResourceClient;
|
|
3898
|
+
const decoratedClient = withSandboxLifecycleHooks(
|
|
3899
|
+
toolspaceResourceClient,
|
|
3900
|
+
ownedHooks,
|
|
3901
|
+
ownedHookContext,
|
|
3902
|
+
);
|
|
3149
3903
|
const ownedFilter = composeCallModelInputFilters(
|
|
3150
3904
|
[
|
|
3151
|
-
callModelInputFilterForSettings(settings,
|
|
3905
|
+
callModelInputFilterForSettings(settings),
|
|
3906
|
+
genesisTitleInputFilter,
|
|
3907
|
+
overrides.callModelInputFilter,
|
|
3908
|
+
// A caller filter may synthesize model input. Re-apply the idempotent
|
|
3909
|
+
// canonical bound at the literal final seam before accounting/provider
|
|
3910
|
+
// serialization so no extension can bypass the policy.
|
|
3911
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
3912
|
+
contextRobustnessFilterForSettings(settings, {
|
|
3152
3913
|
throwOnCompactionNeeded: Boolean(
|
|
3153
|
-
overrides.
|
|
3914
|
+
overrides.contextCompactionSignal || overrides.contextCompactionRequested,
|
|
3154
3915
|
),
|
|
3155
|
-
...(overrides.
|
|
3156
|
-
? {
|
|
3157
|
-
contextCompactionSignalTokens: overrides.contextCompactionSignalTokens,
|
|
3158
|
-
}
|
|
3916
|
+
...(overrides.contextCompactionSignal
|
|
3917
|
+
? { contextCompactionSignal: overrides.contextCompactionSignal }
|
|
3159
3918
|
: {}),
|
|
3160
3919
|
...(overrides.contextCompactionRequested
|
|
3161
|
-
? {
|
|
3162
|
-
contextCompactionRequested: overrides.contextCompactionRequested,
|
|
3163
|
-
}
|
|
3920
|
+
? { contextCompactionRequested: overrides.contextCompactionRequested }
|
|
3164
3921
|
: {}),
|
|
3165
3922
|
}),
|
|
3166
|
-
genesisTitleInputFilter,
|
|
3167
|
-
overrides.callModelInputFilter,
|
|
3168
3923
|
].filter((f): f is CallModelInputFilter => Boolean(f)),
|
|
3169
3924
|
);
|
|
3170
3925
|
const ownedRunOptions: Parameters<typeof run>[2] = {
|
|
@@ -3175,7 +3930,7 @@ export async function runAgentStream(
|
|
|
3175
3930
|
};
|
|
3176
3931
|
ownedRunOptions.sandbox = {
|
|
3177
3932
|
client: decoratedClient,
|
|
3178
|
-
session,
|
|
3933
|
+
session: agentSession,
|
|
3179
3934
|
...(sessionState ? { sessionState } : {}),
|
|
3180
3935
|
} as SandboxRunConfig;
|
|
3181
3936
|
return await runScopedRunner(settings).run(agent, prepared.input, ownedRunOptions);
|
|
@@ -3200,14 +3955,27 @@ export async function runAgentStream(
|
|
|
3200
3955
|
...(runAs ? { runAs } : {}),
|
|
3201
3956
|
})
|
|
3202
3957
|
: refreshedClient;
|
|
3958
|
+
const credentialClient =
|
|
3959
|
+
resourceClient && overrides.runCredentialSessionId
|
|
3960
|
+
? withRunCredentialsClient(
|
|
3961
|
+
resourceClient,
|
|
3962
|
+
overrides.runCredentialSessionId,
|
|
3963
|
+
overrides.onRunCredentialSessionReady,
|
|
3964
|
+
)
|
|
3965
|
+
: resourceClient;
|
|
3966
|
+
const toolspaceClient =
|
|
3967
|
+
credentialClient && toolspaceTokenFile
|
|
3968
|
+
? withToolspaceTokenClient(credentialClient, toolspaceTokenFile)
|
|
3969
|
+
: credentialClient;
|
|
3203
3970
|
// TOKEN-BROKER (B1): the per-turn git token seed, forwarded OFF-MANIFEST so the
|
|
3204
3971
|
// repository-clone hook seeds it to the box's token file before the clone.
|
|
3205
3972
|
const gitTokenSeeds = gitTokenSeedsForAgent(agent);
|
|
3973
|
+
const gitCredentialBindings = gitCredentialBindingsForAgent(agent);
|
|
3206
3974
|
const toolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
|
|
3207
3975
|
const legacyRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3208
|
-
const client =
|
|
3976
|
+
const client = toolspaceClient
|
|
3209
3977
|
? withSandboxLifecycleHooks(
|
|
3210
|
-
|
|
3978
|
+
toolspaceClient,
|
|
3211
3979
|
[
|
|
3212
3980
|
// M3: same rig-setup-first ordering + credential-hook union as the owned
|
|
3213
3981
|
// path (this legacy create/resume decoration path is byte-for-byte today
|
|
@@ -3218,6 +3986,9 @@ export async function runAgentStream(
|
|
|
3218
3986
|
rigCredentialHooksForAgent(agent),
|
|
3219
3987
|
),
|
|
3220
3988
|
...sandboxToolspaceTokenHooksForAgent(agent),
|
|
3989
|
+
...toolspaceTokenSessionRegistrationHooks(
|
|
3990
|
+
toolspaceTokenSeed ? overrides.onToolspaceTokenSessionReady : undefined,
|
|
3991
|
+
),
|
|
3221
3992
|
...sandboxRepositoryCloneHooksForAgent(agent),
|
|
3222
3993
|
...gitCredentialSessionRegistrationHooks(overrides.onGitCredentialSessionReady),
|
|
3223
3994
|
],
|
|
@@ -3226,7 +3997,9 @@ export async function runAgentStream(
|
|
|
3226
3997
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3227
3998
|
...(runAs ? { runAs } : {}),
|
|
3228
3999
|
...(gitTokenSeeds ? { gitTokenSeeds } : {}),
|
|
4000
|
+
...(gitCredentialBindings ? { gitCredentialBindings } : {}),
|
|
3229
4001
|
...(toolspaceTokenSeed ? { toolspaceTokenSeed } : {}),
|
|
4002
|
+
...(toolspaceTokenFile ? { toolspaceTokenFile } : {}),
|
|
3230
4003
|
...(legacyRigSetup ? { rigSetup: legacyRigSetup } : {}),
|
|
3231
4004
|
},
|
|
3232
4005
|
)
|
|
@@ -3239,21 +4012,21 @@ export async function runAgentStream(
|
|
|
3239
4012
|
// OpenGeni's durable conversation truth is still reconciled explicitly below.
|
|
3240
4013
|
const callModelInputFilter = composeCallModelInputFilters(
|
|
3241
4014
|
[
|
|
3242
|
-
callModelInputFilterForSettings(settings,
|
|
4015
|
+
callModelInputFilterForSettings(settings),
|
|
4016
|
+
genesisTitleInputFilter,
|
|
4017
|
+
overrides.callModelInputFilter,
|
|
4018
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
4019
|
+
contextRobustnessFilterForSettings(settings, {
|
|
3243
4020
|
throwOnCompactionNeeded: Boolean(
|
|
3244
|
-
overrides.
|
|
4021
|
+
overrides.contextCompactionSignal || overrides.contextCompactionRequested,
|
|
3245
4022
|
),
|
|
3246
|
-
...(overrides.
|
|
3247
|
-
? {
|
|
3248
|
-
contextCompactionSignalTokens: overrides.contextCompactionSignalTokens,
|
|
3249
|
-
}
|
|
4023
|
+
...(overrides.contextCompactionSignal
|
|
4024
|
+
? { contextCompactionSignal: overrides.contextCompactionSignal }
|
|
3250
4025
|
: {}),
|
|
3251
4026
|
...(overrides.contextCompactionRequested
|
|
3252
4027
|
? { contextCompactionRequested: overrides.contextCompactionRequested }
|
|
3253
4028
|
: {}),
|
|
3254
4029
|
}),
|
|
3255
|
-
genesisTitleInputFilter,
|
|
3256
|
-
overrides.callModelInputFilter,
|
|
3257
4030
|
].filter((f): f is CallModelInputFilter => Boolean(f)),
|
|
3258
4031
|
);
|
|
3259
4032
|
const runOptions: Parameters<typeof run>[2] = {
|
|
@@ -3656,6 +4429,9 @@ export async function runOwnedSandboxSetup(
|
|
|
3656
4429
|
onRuntimeEvent?: SandboxLifecycleHookContext["onRuntimeEvent"];
|
|
3657
4430
|
gitTokenSeedsOverride?: GitTokenSeeds;
|
|
3658
4431
|
gitTokenSeedOverride?: string;
|
|
4432
|
+
gitCredentialBindingsOverride?: GitCredentialBindingSeed[];
|
|
4433
|
+
toolspaceTokenSeedOverride?: string;
|
|
4434
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
3659
4435
|
},
|
|
3660
4436
|
): Promise<void> {
|
|
3661
4437
|
const { settings, environment } = opts;
|
|
@@ -3677,7 +4453,11 @@ export async function runOwnedSandboxSetup(
|
|
|
3677
4453
|
...(opts.gitTokenSeedOverride ? { github: opts.gitTokenSeedOverride } : {}),
|
|
3678
4454
|
...(opts.gitTokenSeedsOverride ?? {}),
|
|
3679
4455
|
} satisfies GitTokenSeeds;
|
|
3680
|
-
const
|
|
4456
|
+
const ownedGitCredentialBindings =
|
|
4457
|
+
opts.gitCredentialBindingsOverride ?? gitCredentialBindingsForAgent(agent);
|
|
4458
|
+
const ownedToolspaceTokenSeed =
|
|
4459
|
+
opts.toolspaceTokenSeedOverride ?? toolspaceTokenSeedForAgent(agent);
|
|
4460
|
+
const ownedToolspaceTokenFile = toolspaceTokenFileForAgent(agent, environment);
|
|
3681
4461
|
const ownedRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3682
4462
|
const ownedHooks = [
|
|
3683
4463
|
// M3: rig setup runs FIRST so any tooling it installs is present for the
|
|
@@ -3699,8 +4479,11 @@ export async function runOwnedSandboxSetup(
|
|
|
3699
4479
|
...(opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {}),
|
|
3700
4480
|
...(runAs ? { runAs } : {}),
|
|
3701
4481
|
...(Object.keys(ownedGitTokenSeeds).length > 0 ? { gitTokenSeeds: ownedGitTokenSeeds } : {}),
|
|
4482
|
+
...(ownedGitCredentialBindings ? { gitCredentialBindings: ownedGitCredentialBindings } : {}),
|
|
3702
4483
|
...(ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {}),
|
|
4484
|
+
...(ownedToolspaceTokenFile ? { toolspaceTokenFile: ownedToolspaceTokenFile } : {}),
|
|
3703
4485
|
...(ownedRigSetup ? { rigSetup: ownedRigSetup } : {}),
|
|
4486
|
+
...(opts.commandRunner ? { commandRunner: opts.commandRunner } : {}),
|
|
3704
4487
|
};
|
|
3705
4488
|
// OWNED-PATH HOOKS: run the beforeAgentStart hooks directly against the provided
|
|
3706
4489
|
// box, once per turn, BEFORE the run starts (repository-clone hook seeds the B1
|
|
@@ -3720,6 +4503,7 @@ export async function runOwnedSandboxSetup(
|
|
|
3720
4503
|
const materialized = await materializeSandboxFileDownloads(setupSession, fileDownloads, {
|
|
3721
4504
|
...(opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {}),
|
|
3722
4505
|
...(runAs ? { runAs } : {}),
|
|
4506
|
+
...(opts.commandRunner ? { commandRunner: opts.commandRunner } : {}),
|
|
3723
4507
|
});
|
|
3724
4508
|
if (opts.preparedInput) {
|
|
3725
4509
|
appendSandboxFileDownloadFailureNote(opts.preparedInput, materialized.failures);
|
|
@@ -3753,7 +4537,7 @@ function mergePathGrants(
|
|
|
3753
4537
|
export function withSandboxFileDownloads(
|
|
3754
4538
|
client: SandboxClient,
|
|
3755
4539
|
downloads: SandboxFileDownload[],
|
|
3756
|
-
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs"> = {},
|
|
4540
|
+
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs" | "commandRunner"> = {},
|
|
3757
4541
|
): SandboxClient {
|
|
3758
4542
|
const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
|
|
3759
4543
|
if (normalizedDownloads.length === 0) {
|
|
@@ -3819,7 +4603,7 @@ export function withSandboxFileDownloads(
|
|
|
3819
4603
|
export async function materializeSandboxFileDownloads(
|
|
3820
4604
|
session: SandboxSessionLike,
|
|
3821
4605
|
downloads: SandboxFileDownload[],
|
|
3822
|
-
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs"> = {},
|
|
4606
|
+
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs" | "commandRunner"> = {},
|
|
3823
4607
|
): Promise<SandboxFileDownloadMaterializationResult> {
|
|
3824
4608
|
const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
|
|
3825
4609
|
if (normalizedDownloads.length === 0) {
|
|
@@ -3857,27 +4641,24 @@ export async function materializeSandboxFileDownloads(
|
|
|
3857
4641
|
}
|
|
3858
4642
|
let result: unknown;
|
|
3859
4643
|
try {
|
|
3860
|
-
result =
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
3872
|
-
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
3873
|
-
maxOutputTokens: 20_000,
|
|
3874
|
-
});
|
|
4644
|
+
result = await runSandboxLifecycleCommand(
|
|
4645
|
+
session,
|
|
4646
|
+
{
|
|
4647
|
+
cmd: sandboxFileDownloadCommand(download, targetPath),
|
|
4648
|
+
workdir: "/workspace",
|
|
4649
|
+
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
4650
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
4651
|
+
maxOutputTokens: 20_000,
|
|
4652
|
+
},
|
|
4653
|
+
context.commandRunner,
|
|
4654
|
+
);
|
|
3875
4655
|
assertSandboxCommandSucceeded(result, `Sandbox file resource download ${download.fileId}`);
|
|
3876
4656
|
await context.onRuntimeEvent?.({
|
|
3877
4657
|
type: "sandbox.operation.completed",
|
|
3878
4658
|
payload: { name: "file-resource-download", ...payload },
|
|
3879
4659
|
});
|
|
3880
4660
|
} catch (error) {
|
|
4661
|
+
if (error instanceof TurnSandboxCommandCancelledError) throw error;
|
|
3881
4662
|
const failure = sandboxFileDownloadFailure(download, targetPath, error, result);
|
|
3882
4663
|
failures.push(failure);
|
|
3883
4664
|
await context.onRuntimeEvent?.({
|
|
@@ -3954,86 +4735,87 @@ function ensureManifest(
|
|
|
3954
4735
|
});
|
|
3955
4736
|
}
|
|
3956
4737
|
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
4738
|
+
function base64DecodedByteLength(value: string): number {
|
|
4739
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
4740
|
+
return Math.max(0, Math.floor((value.length * 3) / 4) - padding);
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
/** Determine image byte length without allocating another binary/base64 copy. */
|
|
4744
|
+
function imageDataByteLength(data: unknown): number | null {
|
|
4745
|
+
if (ArrayBuffer.isView(data)) return data.byteLength;
|
|
4746
|
+
if (data instanceof ArrayBuffer) return data.byteLength;
|
|
3965
4747
|
if (Array.isArray(data)) {
|
|
3966
|
-
return data.every((
|
|
4748
|
+
return data.every((value) => typeof value === "number") ? data.length : null;
|
|
3967
4749
|
}
|
|
3968
|
-
if (data
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
}
|
|
4750
|
+
if (!data || typeof data !== "object") return null;
|
|
4751
|
+
const record = data as Record<string, unknown>;
|
|
4752
|
+
if (record.type === "Buffer" && Array.isArray(record.data)) {
|
|
4753
|
+
return record.data.every((value) => typeof value === "number") ? record.data.length : null;
|
|
3973
4754
|
}
|
|
3974
|
-
|
|
4755
|
+
const keys = Object.keys(record);
|
|
4756
|
+
return keys.length > 0 &&
|
|
4757
|
+
keys.every((key) => /^\d+$/.test(key) && typeof record[key] === "number")
|
|
4758
|
+
? keys.length
|
|
4759
|
+
: null;
|
|
3975
4760
|
}
|
|
3976
4761
|
|
|
3977
|
-
/**
|
|
3978
|
-
*
|
|
3979
|
-
*
|
|
3980
|
-
*
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
if (
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
url
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4762
|
+
/**
|
|
4763
|
+
* Convert one image-shaped tool result into a content-free audit fact. This is
|
|
4764
|
+
* intentionally different from model history: the model keeps its structured
|
|
4765
|
+
* image item, while `session_events` never becomes an implicit image blob store.
|
|
4766
|
+
*/
|
|
4767
|
+
function toolOutputMediaPreview(value: unknown): SessionEventMediaPreview | null {
|
|
4768
|
+
if (typeof value === "string") {
|
|
4769
|
+
return sessionEventMediaPreviewFromDataUrl(value);
|
|
4770
|
+
}
|
|
4771
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4772
|
+
const record = value as Record<string, unknown>;
|
|
4773
|
+
if (record.type === "input_image") {
|
|
4774
|
+
const source = record.image ?? record.image_url ?? record.imageUrl;
|
|
4775
|
+
const url =
|
|
4776
|
+
typeof source === "string"
|
|
4777
|
+
? source
|
|
4778
|
+
: source && typeof source === "object"
|
|
4779
|
+
? (source as Record<string, unknown>).url
|
|
4780
|
+
: null;
|
|
4781
|
+
if (typeof url !== "string" || url.length === 0) return null;
|
|
4782
|
+
return sessionEventMediaPreviewFromDataUrl(url) ?? sessionEventMediaPreview("image/*", null);
|
|
4783
|
+
}
|
|
4784
|
+
if (record.type !== "image" || !record.image || typeof record.image !== "object") return null;
|
|
4785
|
+
const image = record.image as Record<string, unknown>;
|
|
4001
4786
|
const mediaType =
|
|
4002
4787
|
typeof image.mediaType === "string" && image.mediaType.length > 0
|
|
4003
4788
|
? image.mediaType
|
|
4004
4789
|
: "image/png";
|
|
4790
|
+
if (typeof image.url === "string" && image.url.length > 0) {
|
|
4791
|
+
return (
|
|
4792
|
+
sessionEventMediaPreviewFromDataUrl(image.url) ?? sessionEventMediaPreview(mediaType, null)
|
|
4793
|
+
);
|
|
4794
|
+
}
|
|
4005
4795
|
if (typeof image.data === "string") {
|
|
4006
|
-
return
|
|
4796
|
+
return (
|
|
4797
|
+
sessionEventMediaPreviewFromDataUrl(image.data) ??
|
|
4798
|
+
sessionEventMediaPreview(mediaType, base64DecodedByteLength(image.data))
|
|
4799
|
+
);
|
|
4007
4800
|
}
|
|
4008
|
-
const
|
|
4009
|
-
return
|
|
4801
|
+
const byteLength = imageDataByteLength(image.data);
|
|
4802
|
+
return byteLength === null ? null : sessionEventMediaPreview(mediaType, byteLength);
|
|
4010
4803
|
}
|
|
4011
4804
|
|
|
4012
4805
|
/**
|
|
4013
|
-
*
|
|
4014
|
-
*
|
|
4015
|
-
*
|
|
4016
|
-
*
|
|
4017
|
-
* per screenshot in session_events — ~10x the base64 form). This mirrors the desktop
|
|
4018
|
-
* screenshot to the SAME compact `data:<mediaType>;base64,…` STRING the HOSTED
|
|
4019
|
-
* `computer_call` event already carries (agents-core sets its output to that data-URL),
|
|
4020
|
-
* so both computer-use transports emit one representation. The full data-URL is kept
|
|
4021
|
-
* (not truncated) because the web timeline RENDERS the screenshot from this event
|
|
4022
|
-
* payload — packages/react/src/timeline/tool-renderers.tsx ComputerCallRenderer
|
|
4023
|
-
* (`out.startsWith("data:image")` → <ScreenshotFigure src={out}/>) and ViewImageRenderer.
|
|
4024
|
-
* Non-image outputs (text strings, MCP `{isError,content}` objects, hosted computer_call
|
|
4025
|
-
* data-URL strings) pass through unchanged.
|
|
4806
|
+
* Normalize a tool-call output for the lossy `agent.toolCall.output` audit event.
|
|
4807
|
+
* Inline image bytes/data URLs become a compact `media_preview` with exact byte
|
|
4808
|
+
* length where knowable and `fullOutputAvailable:false`. The model-facing output
|
|
4809
|
+
* is not changed here, and mixed arrays retain their non-image text/error facts.
|
|
4026
4810
|
*/
|
|
4027
4811
|
export function normalizeToolOutputForEvent(output: unknown): unknown {
|
|
4028
|
-
const single =
|
|
4812
|
+
const single = toolOutputMediaPreview(output);
|
|
4029
4813
|
if (single !== null) {
|
|
4030
4814
|
return single;
|
|
4031
4815
|
}
|
|
4032
4816
|
if (Array.isArray(output)) {
|
|
4033
|
-
const normalized = output.map((el) =>
|
|
4034
|
-
|
|
4035
|
-
// image renderers expect; a mixed/multi array keeps its (now-compact) shape.
|
|
4036
|
-
if (normalized.length === 1 && typeof normalized[0] === "string") {
|
|
4817
|
+
const normalized = output.map((el) => toolOutputMediaPreview(el) ?? el);
|
|
4818
|
+
if (normalized.length === 1 && normalized[0]?.type === "media_preview") {
|
|
4037
4819
|
return normalized[0];
|
|
4038
4820
|
}
|
|
4039
4821
|
return normalized;
|
|
@@ -4090,8 +4872,8 @@ export function normalizeSdkEvent(event: RunStreamEvent): NormalizedRuntimeEvent
|
|
|
4090
4872
|
type: "agent.toolCall.output",
|
|
4091
4873
|
payload: {
|
|
4092
4874
|
id: item.rawItem?.callId ?? item.id ?? null,
|
|
4093
|
-
//
|
|
4094
|
-
//
|
|
4875
|
+
// Inline media becomes a content-free audit fact. Model history keeps
|
|
4876
|
+
// the provider's real structured image output on its separate path.
|
|
4095
4877
|
output: normalizeToolOutputForEvent(item.output),
|
|
4096
4878
|
},
|
|
4097
4879
|
});
|
|
@@ -4204,6 +4986,7 @@ function usageFromResponse(response: unknown): ModelResponseUsage["usage"] | nul
|
|
|
4204
4986
|
...numberProp(record, "totalTokens", "totalTokens", "total_tokens"),
|
|
4205
4987
|
...inputTokenDetailsProp(record),
|
|
4206
4988
|
...outputTokenDetailsProp(record),
|
|
4989
|
+
...requestUsageEntriesProp(record),
|
|
4207
4990
|
};
|
|
4208
4991
|
return Object.keys(usage).length > 0 ? usage : null;
|
|
4209
4992
|
}
|
|
@@ -4214,7 +4997,11 @@ function numberProp(
|
|
|
4214
4997
|
...keys: string[]
|
|
4215
4998
|
): Partial<ModelResponseUsage["usage"]> {
|
|
4216
4999
|
const value = keys.map((key) => raw[key]).find((candidate) => candidate !== undefined);
|
|
4217
|
-
|
|
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 } : {};
|
|
4218
5005
|
}
|
|
4219
5006
|
|
|
4220
5007
|
function inputTokenDetailsProp(raw: Record<string, unknown>): Partial<ModelResponseUsage["usage"]> {
|
|
@@ -4223,7 +5010,7 @@ function inputTokenDetailsProp(raw: Record<string, unknown>): Partial<ModelRespo
|
|
|
4223
5010
|
raw.input_tokens_details ??
|
|
4224
5011
|
raw.promptTokensDetails ??
|
|
4225
5012
|
raw.prompt_tokens_details;
|
|
4226
|
-
if (
|
|
5013
|
+
if (details === undefined || details === null) {
|
|
4227
5014
|
return {};
|
|
4228
5015
|
}
|
|
4229
5016
|
return {
|
|
@@ -4236,7 +5023,7 @@ function outputTokenDetailsProp(
|
|
|
4236
5023
|
): Partial<ModelResponseUsage["usage"]> {
|
|
4237
5024
|
const details = raw.outputTokensDetails ?? raw.output_tokens_details;
|
|
4238
5025
|
const normalized = details ?? raw.completionTokensDetails ?? raw.completion_tokens_details;
|
|
4239
|
-
if (
|
|
5026
|
+
if (normalized === undefined || normalized === null) {
|
|
4240
5027
|
return {};
|
|
4241
5028
|
}
|
|
4242
5029
|
return {
|
|
@@ -4244,18 +5031,61 @@ function outputTokenDetailsProp(
|
|
|
4244
5031
|
};
|
|
4245
5032
|
}
|
|
4246
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
|
+
|
|
4247
5049
|
export function serializeApprovals(interruptions: unknown[]): unknown[] {
|
|
4248
|
-
return interruptions
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
5050
|
+
return interruptions
|
|
5051
|
+
.filter((item) => interruptionToolName(item) !== HUMAN_INPUT_TOOL_NAME)
|
|
5052
|
+
.map((item: any) => {
|
|
5053
|
+
if (typeof item?.toJSON === "function") {
|
|
5054
|
+
return item.toJSON();
|
|
5055
|
+
}
|
|
5056
|
+
return {
|
|
5057
|
+
id: approvalIdentifier(item) ?? "approval",
|
|
5058
|
+
name: item?.name ?? item?.rawItem?.name ?? "tool",
|
|
5059
|
+
arguments: item?.arguments ?? item?.rawItem?.arguments ?? null,
|
|
5060
|
+
raw: item,
|
|
5061
|
+
};
|
|
5062
|
+
});
|
|
5063
|
+
}
|
|
5064
|
+
|
|
5065
|
+
export function serializeHumanInputRequests(
|
|
5066
|
+
interruptions: unknown[],
|
|
5067
|
+
): SerializedHumanInputInterruption[] {
|
|
5068
|
+
return interruptions
|
|
5069
|
+
.filter((item) => interruptionToolName(item) === HUMAN_INPUT_TOOL_NAME)
|
|
5070
|
+
.map((item: any) => {
|
|
5071
|
+
const rawArguments = item?.arguments ?? item?.rawItem?.arguments;
|
|
5072
|
+
let parsedArguments: unknown = rawArguments;
|
|
5073
|
+
if (typeof rawArguments === "string") {
|
|
5074
|
+
try {
|
|
5075
|
+
parsedArguments = JSON.parse(rawArguments);
|
|
5076
|
+
} catch {
|
|
5077
|
+
throw new Error("Human-input interruption contains invalid JSON arguments");
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
const toolCallId = approvalIdentifier(item);
|
|
5081
|
+
if (!toolCallId) {
|
|
5082
|
+
throw new Error("Human-input interruption is missing a stable tool-call identity");
|
|
5083
|
+
}
|
|
5084
|
+
return {
|
|
5085
|
+
toolCallId,
|
|
5086
|
+
input: RequestHumanInputToolInput.parse(parsedArguments),
|
|
5087
|
+
};
|
|
5088
|
+
});
|
|
4259
5089
|
}
|
|
4260
5090
|
|
|
4261
5091
|
export function buildManifest(
|
|
@@ -4264,6 +5094,7 @@ export function buildManifest(
|
|
|
4264
5094
|
environment = collectSandboxEnvironment(settings),
|
|
4265
5095
|
fileResourceDownloads: SandboxFileDownload[] = [],
|
|
4266
5096
|
): Manifest {
|
|
5097
|
+
assertUniqueResourceMountPaths(resources);
|
|
4267
5098
|
const entries: Record<string, any> = {};
|
|
4268
5099
|
const downloadsByFileId = new Map(
|
|
4269
5100
|
normalizeSandboxFileDownloads(fileResourceDownloads).map((download) => [
|
|
@@ -4274,9 +5105,9 @@ export function buildManifest(
|
|
|
4274
5105
|
for (const resource of resources) {
|
|
4275
5106
|
if (resource.kind === "repository") {
|
|
4276
5107
|
const url = new URL(resource.uri);
|
|
4277
|
-
const host = url.
|
|
5108
|
+
const host = url.host.toLowerCase();
|
|
4278
5109
|
const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
4279
|
-
const mountPath =
|
|
5110
|
+
const mountPath = resourceMountPath(resource);
|
|
4280
5111
|
if (repositoryUsesSandboxClone(settings, resource)) {
|
|
4281
5112
|
entries[mountPath] = dir();
|
|
4282
5113
|
continue;
|
|
@@ -4285,12 +5116,12 @@ export function buildManifest(
|
|
|
4285
5116
|
host,
|
|
4286
5117
|
repo,
|
|
4287
5118
|
ref: resource.ref,
|
|
4288
|
-
...(resource.subpath ? { subpath:
|
|
5119
|
+
...(resource.subpath ? { subpath: normalizeRepositorySubpath(resource.subpath) } : {}),
|
|
4289
5120
|
});
|
|
4290
5121
|
continue;
|
|
4291
5122
|
}
|
|
4292
5123
|
if (resource.kind === "file") {
|
|
4293
|
-
const mountPath =
|
|
5124
|
+
const mountPath = resourceMountPath(resource);
|
|
4294
5125
|
const download = downloadsByFileId.get(resource.fileId);
|
|
4295
5126
|
entries[mountPath] = download
|
|
4296
5127
|
? sandboxDownloadDirectory(download, mountPath)
|
|
@@ -4450,11 +5281,7 @@ function parseAzureConnectionString(value: string): Record<string, string> {
|
|
|
4450
5281
|
}
|
|
4451
5282
|
|
|
4452
5283
|
function normalizeManifestPath(path: string): string {
|
|
4453
|
-
|
|
4454
|
-
if (!normalized || normalized.includes("..")) {
|
|
4455
|
-
throw new Error(`Invalid sandbox resource path: ${path}`);
|
|
4456
|
-
}
|
|
4457
|
-
return normalized;
|
|
5284
|
+
return normalizeResourceMountPath(path);
|
|
4458
5285
|
}
|
|
4459
5286
|
|
|
4460
5287
|
function normalizeSandboxFileDownloads(downloads: SandboxFileDownload[]): SandboxFileDownload[] {
|
|
@@ -4517,6 +5344,11 @@ function shellQuote(value: string): string {
|
|
|
4517
5344
|
|
|
4518
5345
|
export type SandboxLifecycleHookPhase = "beforeAgentStart";
|
|
4519
5346
|
|
|
5347
|
+
export type SandboxLifecycleCommandRunner = (
|
|
5348
|
+
session: TurnSandboxCommandSession,
|
|
5349
|
+
args: TurnSandboxCommandArgs,
|
|
5350
|
+
) => Promise<unknown>;
|
|
5351
|
+
|
|
4520
5352
|
export type SandboxLifecycleHookContext = {
|
|
4521
5353
|
environment: Record<string, string>;
|
|
4522
5354
|
onRuntimeEvent?: (event: NormalizedRuntimeEvent) => Promise<void> | void;
|
|
@@ -4528,12 +5360,28 @@ export type SandboxLifecycleHookContext = {
|
|
|
4528
5360
|
// NEVER the box/agent manifest env (validateNoEnvironmentDelta must never see
|
|
4529
5361
|
// rotating values).
|
|
4530
5362
|
gitTokenSeeds?: GitTokenSeeds;
|
|
5363
|
+
gitCredentialBindings?: GitCredentialBindingSeed[];
|
|
4531
5364
|
toolspaceTokenSeed?: string;
|
|
5365
|
+
toolspaceTokenFile?: string;
|
|
4532
5366
|
// M3: the rig setup descriptor for the rig-setup hook (the script + marker
|
|
4533
5367
|
// version id + the rig's own timeout). Present only on a rig-bound turn.
|
|
4534
5368
|
rigSetup?: RigSetupDescriptor;
|
|
5369
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
4535
5370
|
};
|
|
4536
5371
|
|
|
5372
|
+
async function runSandboxLifecycleCommand(
|
|
5373
|
+
session: SandboxSessionLike,
|
|
5374
|
+
args: TurnSandboxCommandArgs,
|
|
5375
|
+
commandRunner?: SandboxLifecycleCommandRunner,
|
|
5376
|
+
): Promise<unknown> {
|
|
5377
|
+
if (commandRunner) {
|
|
5378
|
+
return await commandRunner(session as TurnSandboxCommandSession, args);
|
|
5379
|
+
}
|
|
5380
|
+
if (session.exec) return await session.exec(args);
|
|
5381
|
+
if (session.execCommand) return await session.execCommand(args);
|
|
5382
|
+
throw new Error("Sandbox session does not support command execution");
|
|
5383
|
+
}
|
|
5384
|
+
|
|
4537
5385
|
// M3: everything the rig-setup hook needs to run the frozen rig version's setup
|
|
4538
5386
|
// script exactly once per box. `versionId` keys the idempotence marker
|
|
4539
5387
|
// (/var/opengeni/rig-setup-<versionId>.done); `timeoutMs` is the rig-specific
|
|
@@ -4683,10 +5531,32 @@ function gitTokenSeedsForAgent(agent: Agent<any, any>): GitTokenSeeds | undefine
|
|
|
4683
5531
|
return agentGitTokenSeeds.get(agent);
|
|
4684
5532
|
}
|
|
4685
5533
|
|
|
5534
|
+
function gitCredentialBindingsForAgent(
|
|
5535
|
+
agent: Agent<any, any>,
|
|
5536
|
+
): GitCredentialBindingSeed[] | undefined {
|
|
5537
|
+
return agentGitCredentialBindings.get(agent);
|
|
5538
|
+
}
|
|
5539
|
+
|
|
4686
5540
|
function toolspaceTokenSeedForAgent(agent: Agent<any, any>): string | undefined {
|
|
4687
5541
|
return agentToolspaceTokenSeed.get(agent);
|
|
4688
5542
|
}
|
|
4689
5543
|
|
|
5544
|
+
function toolspaceTokenSessionIdForAgent(agent: Agent<any, any>): string | undefined {
|
|
5545
|
+
return agentToolspaceTokenSessionId.get(agent);
|
|
5546
|
+
}
|
|
5547
|
+
|
|
5548
|
+
function toolspaceTokenFileForAgent(
|
|
5549
|
+
agent: Agent<any, any>,
|
|
5550
|
+
environment: Readonly<Record<string, string>>,
|
|
5551
|
+
): string | undefined {
|
|
5552
|
+
if (!toolspaceTokenSeedForAgent(agent)) return undefined;
|
|
5553
|
+
const sessionId = toolspaceTokenSessionIdForAgent(agent);
|
|
5554
|
+
if (!sessionId) {
|
|
5555
|
+
throw new Error("Toolspace token seed is missing its session identity");
|
|
5556
|
+
}
|
|
5557
|
+
return toolspaceTokenFileFromEnvironment(environment, sessionId);
|
|
5558
|
+
}
|
|
5559
|
+
|
|
4690
5560
|
function sandboxToolspaceTokenHooksForAgent(agent: Agent<any, any>): SandboxLifecycleHook[] {
|
|
4691
5561
|
return toolspaceTokenSeedForAgent(agent)
|
|
4692
5562
|
? [
|
|
@@ -4756,6 +5626,22 @@ function gitCredentialSessionRegistrationHooks(
|
|
|
4756
5626
|
: [];
|
|
4757
5627
|
}
|
|
4758
5628
|
|
|
5629
|
+
function toolspaceTokenSessionRegistrationHooks(
|
|
5630
|
+
callback: RunAgentStreamOptions["onToolspaceTokenSessionReady"],
|
|
5631
|
+
): SandboxLifecycleHook[] {
|
|
5632
|
+
return callback
|
|
5633
|
+
? [
|
|
5634
|
+
{
|
|
5635
|
+
id: "toolspace-token-renewal-registration",
|
|
5636
|
+
phase: "beforeAgentStart",
|
|
5637
|
+
run: async (session) => {
|
|
5638
|
+
await callback(session);
|
|
5639
|
+
},
|
|
5640
|
+
},
|
|
5641
|
+
]
|
|
5642
|
+
: [];
|
|
5643
|
+
}
|
|
5644
|
+
|
|
4759
5645
|
function sandboxRepositoryCloneHooks(
|
|
4760
5646
|
settings: Settings,
|
|
4761
5647
|
resources: ResourceRef[],
|
|
@@ -4823,6 +5709,20 @@ function gitProviderSeedEnv(provider: GitCredentialProvider): string {
|
|
|
4823
5709
|
return `OPENGENI_GIT_${provider.toUpperCase()}_TOKEN_SEED`;
|
|
4824
5710
|
}
|
|
4825
5711
|
|
|
5712
|
+
export function gitCredentialBindingHash(credentialBindingId: string): string {
|
|
5713
|
+
return createHash("sha256").update(credentialBindingId, "utf8").digest("hex").slice(0, 32);
|
|
5714
|
+
}
|
|
5715
|
+
|
|
5716
|
+
function gitBindingSeedEnv(binding: GitCredentialBindingSeed): string {
|
|
5717
|
+
return `OPENGENI_GIT_BINDING_${gitCredentialBindingHash(binding.credentialBindingId).toUpperCase()}_TOKEN_SEED`;
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
function gitCredentialBindingSeedExportPrefix(bindings: GitCredentialBindingSeed[]): string {
|
|
5721
|
+
return bindings
|
|
5722
|
+
.map((binding) => `export ${gitBindingSeedEnv(binding)}=${shellQuote(binding.token)}`)
|
|
5723
|
+
.join("\n");
|
|
5724
|
+
}
|
|
5725
|
+
|
|
4826
5726
|
function gitTokenSeedExportPrefix(seeds: GitTokenSeeds): string {
|
|
4827
5727
|
const lines: string[] = [];
|
|
4828
5728
|
for (const provider of GIT_CREDENTIAL_PROVIDERS) {
|
|
@@ -4838,37 +5738,268 @@ function gitTokenSeedExportPrefix(seeds: GitTokenSeeds): string {
|
|
|
4838
5738
|
return lines.join("\n");
|
|
4839
5739
|
}
|
|
4840
5740
|
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
5741
|
+
type RuntimeGitBindingDescriptor = {
|
|
5742
|
+
provider: GitCredentialProvider;
|
|
5743
|
+
credentialBindingId: string;
|
|
5744
|
+
bindingHash: string;
|
|
5745
|
+
protocol: string;
|
|
5746
|
+
host: string;
|
|
5747
|
+
path: string;
|
|
5748
|
+
uri: string;
|
|
5749
|
+
};
|
|
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
|
+
|
|
5762
|
+
function runtimeGitBindingDescriptors(
|
|
5763
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5764
|
+
): RuntimeGitBindingDescriptor[] {
|
|
5765
|
+
const remoteBindings = new Map<string, string>();
|
|
5766
|
+
const bindingProviders = new Map<string, GitCredentialProvider>();
|
|
5767
|
+
return resources.map((resource) => {
|
|
5768
|
+
const url = new URL(resource.uri);
|
|
5769
|
+
const credentialProvider = gitCredentialProviderForRepository(resource);
|
|
5770
|
+
// Provider-less public/legacy resources retain the historical GitHub
|
|
5771
|
+
// askpass fallback, but credential-bound resources derive through the
|
|
5772
|
+
// shared contracts helper used by the worker and core.
|
|
5773
|
+
const provider = credentialProvider ?? "github";
|
|
5774
|
+
const credentialBindingId =
|
|
5775
|
+
gitCredentialBindingIdForRepository(resource, credentialProvider) ?? provider;
|
|
5776
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "");
|
|
5777
|
+
const remote = `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}/${path.replace(/\.git$/, "")}`;
|
|
5778
|
+
const bindingKey = `${provider}\u0000${credentialBindingId}`;
|
|
5779
|
+
const boundProvider = bindingProviders.get(credentialBindingId);
|
|
5780
|
+
if (boundProvider && boundProvider !== provider) {
|
|
5781
|
+
throw new Error(
|
|
5782
|
+
`credential binding ${credentialBindingId} is assigned to multiple Git providers`,
|
|
5783
|
+
);
|
|
5784
|
+
}
|
|
5785
|
+
bindingProviders.set(credentialBindingId, provider);
|
|
5786
|
+
const claimed = remoteBindings.get(remote);
|
|
5787
|
+
if (claimed && claimed !== bindingKey) {
|
|
5788
|
+
throw new Error(
|
|
5789
|
+
`repository remote ${resource.uri} is claimed by multiple credential bindings`,
|
|
5790
|
+
);
|
|
5791
|
+
}
|
|
5792
|
+
remoteBindings.set(remote, bindingKey);
|
|
5793
|
+
return {
|
|
5794
|
+
provider,
|
|
5795
|
+
credentialBindingId,
|
|
5796
|
+
bindingHash: gitCredentialBindingHash(credentialBindingId),
|
|
5797
|
+
protocol: url.protocol.replace(/:$/, "").toLowerCase(),
|
|
5798
|
+
host: url.host.toLowerCase(),
|
|
5799
|
+
path,
|
|
5800
|
+
uri: resource.uri,
|
|
5801
|
+
};
|
|
5802
|
+
});
|
|
4845
5803
|
}
|
|
4846
5804
|
|
|
4847
|
-
function
|
|
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(
|
|
4848
5823
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
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
|
+
);
|
|
4856
5896
|
}
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
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
|
+
});
|
|
4861
5909
|
}
|
|
4862
5910
|
}
|
|
4863
|
-
return
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
5911
|
+
return routes;
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
function gitUsernameForProvider(provider: GitCredentialProvider): string {
|
|
5915
|
+
if (provider === "github") return "x-access-token";
|
|
5916
|
+
if (provider === "gitlab") return "oauth2";
|
|
5917
|
+
return "opengeni";
|
|
5918
|
+
}
|
|
5919
|
+
|
|
5920
|
+
function gitCredentialHelperBindingCaseLines(
|
|
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[],
|
|
5947
|
+
): string[] {
|
|
5948
|
+
return routes.flatMap((route) => {
|
|
5949
|
+
const paths = new Set([
|
|
5950
|
+
route.path,
|
|
5951
|
+
route.path.replace(/\.git$/, ""),
|
|
5952
|
+
`${route.path.replace(/\.git$/, "")}.git`,
|
|
5953
|
+
]);
|
|
5954
|
+
return [...paths].map(
|
|
5955
|
+
(path) =>
|
|
5956
|
+
` ${shellQuote(`${route.protocol}|${route.host}|${path}`)}) username=opengeni; token_file="$credential_dir/${route.bindingHash}-token" ;;`,
|
|
4868
5957
|
);
|
|
5958
|
+
});
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5961
|
+
function gitAskpassHostProviderCaseLines(
|
|
5962
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5963
|
+
brokerRoutes: RuntimeGitHttpBrokerRouteDescriptor[],
|
|
5964
|
+
): string[] {
|
|
5965
|
+
const hosts = new Map<string, { provider: GitCredentialProvider; bindings: Set<string> }>();
|
|
5966
|
+
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
5967
|
+
const entry = hosts.get(descriptor.host) ?? {
|
|
5968
|
+
provider: descriptor.provider,
|
|
5969
|
+
bindings: new Set<string>(),
|
|
5970
|
+
};
|
|
5971
|
+
entry.bindings.add(`${descriptor.provider}\u0000${descriptor.credentialBindingId}`);
|
|
5972
|
+
hosts.set(descriptor.host, entry);
|
|
5973
|
+
}
|
|
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
|
+
];
|
|
4869
5987
|
}
|
|
4870
5988
|
|
|
4871
|
-
function gitCredentialTokenWriterCommandLines(): string[] {
|
|
5989
|
+
function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed[] = []): string[] {
|
|
5990
|
+
const bindingWrites = bindings.flatMap((binding) => {
|
|
5991
|
+
const hash = gitCredentialBindingHash(binding.credentialBindingId);
|
|
5992
|
+
const seedEnv = gitBindingSeedEnv(binding);
|
|
5993
|
+
const count = Math.max(1, binding.providerBindingCount ?? 1);
|
|
5994
|
+
const provider = binding.provider;
|
|
5995
|
+
const lines = [`write_git_binding_token ${shellQuote(hash)} "\${${seedEnv}:-}"`];
|
|
5996
|
+
if (count === 1 && binding.transport?.kind !== "http_broker") {
|
|
5997
|
+
lines.push(`write_git_provider_token ${shellQuote(provider)} "\${${seedEnv}:-}"`);
|
|
5998
|
+
} else {
|
|
5999
|
+
lines.push(`remove_git_provider_token ${shellQuote(provider)}`);
|
|
6000
|
+
}
|
|
6001
|
+
return lines;
|
|
6002
|
+
});
|
|
4872
6003
|
return [
|
|
4873
6004
|
// TOKEN-BROKER (B1/B2): seed run-scoped provider tokens into stable files and
|
|
4874
6005
|
// atomically replace each provider file. Token VALUES are supplied only by
|
|
@@ -4882,6 +6013,23 @@ function gitCredentialTokenWriterCommandLines(): string[] {
|
|
|
4882
6013
|
" *) printf '%s\\n' \"${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$provider-token\" ;;",
|
|
4883
6014
|
" esac",
|
|
4884
6015
|
"}",
|
|
6016
|
+
"write_git_binding_token() {",
|
|
6017
|
+
' binding_hash="$1"',
|
|
6018
|
+
' token="$2"',
|
|
6019
|
+
' [ -n "$token" ] || return 0',
|
|
6020
|
+
' credential_dir="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}"',
|
|
6021
|
+
' mkdir -p "$credential_dir"',
|
|
6022
|
+
' token_file="$credential_dir/$binding_hash-token"',
|
|
6023
|
+
' printf \'%s\' "$token" > "$token_file.tmp.$$"',
|
|
6024
|
+
' mv -f "$token_file.tmp.$$" "$token_file"',
|
|
6025
|
+
"}",
|
|
6026
|
+
"remove_git_provider_token() {",
|
|
6027
|
+
' provider="$1"',
|
|
6028
|
+
' rm -f "$(git_provider_token_file "$provider")"',
|
|
6029
|
+
' if [ "$provider" = github ]; then',
|
|
6030
|
+
' rm -f "${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/github-token"',
|
|
6031
|
+
" fi",
|
|
6032
|
+
"}",
|
|
4885
6033
|
"write_git_provider_token() {",
|
|
4886
6034
|
' provider="$1"',
|
|
4887
6035
|
' token="$2"',
|
|
@@ -4902,16 +6050,109 @@ function gitCredentialTokenWriterCommandLines(): string[] {
|
|
|
4902
6050
|
'write_git_provider_token github "${OPENGENI_GIT_GITHUB_TOKEN_SEED:-${OPENGENI_GIT_TOKEN_SEED:-}}"',
|
|
4903
6051
|
'write_git_provider_token gitlab "${OPENGENI_GIT_GITLAB_TOKEN_SEED:-}"',
|
|
4904
6052
|
'write_git_provider_token azure_devops "${OPENGENI_GIT_AZURE_DEVOPS_TOKEN_SEED:-}"',
|
|
6053
|
+
...bindingWrites,
|
|
4905
6054
|
'umask "$seed_umask"',
|
|
4906
6055
|
];
|
|
4907
6056
|
}
|
|
4908
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
|
+
|
|
4909
6076
|
function gitCredentialHelperCommandLines(
|
|
4910
6077
|
resources: Extract<ResourceRef, { kind: "repository" }>[] = [],
|
|
6078
|
+
bindings: GitCredentialBindingSeed[] = [],
|
|
4911
6079
|
): string[] {
|
|
4912
|
-
const
|
|
6080
|
+
const brokerRoutes = runtimeGitHttpBrokerRouteDescriptors(resources, bindings);
|
|
6081
|
+
const hostProviderCases = gitAskpassHostProviderCaseLines(resources, brokerRoutes);
|
|
6082
|
+
const bindingCases = [
|
|
6083
|
+
...gitCredentialHelperBindingCaseLines(resources, bindings),
|
|
6084
|
+
...gitCredentialHelperBrokerCaseLines(brokerRoutes),
|
|
6085
|
+
];
|
|
6086
|
+
const descriptors = runtimeGitBindingDescriptors(resources);
|
|
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
|
+
: [];
|
|
6097
|
+
const bindingProviders = new Map<GitCredentialProvider, Set<string>>();
|
|
6098
|
+
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
6099
|
+
const ids = bindingProviders.get(descriptor.provider) ?? new Set<string>();
|
|
6100
|
+
ids.add(descriptor.credentialBindingId);
|
|
6101
|
+
bindingProviders.set(descriptor.provider, ids);
|
|
6102
|
+
}
|
|
6103
|
+
const strictAskpass = [...bindingProviders.values()].some((ids) => ids.size > 1);
|
|
6104
|
+
const allowedWrapperHashes = [
|
|
6105
|
+
...new Set(wrapperDescriptors.map((item) => `${item.provider}|${item.bindingHash}`)),
|
|
6106
|
+
].map((key) => ` ${shellQuote(key)}) return 0 ;;`);
|
|
6107
|
+
const originWrapperHashes = wrapperDescriptors.flatMap((item) => {
|
|
6108
|
+
const base = item.uri.replace(/\.git$/, "");
|
|
6109
|
+
return [...new Set([item.uri, base, `${base}.git`])].map(
|
|
6110
|
+
(uri) =>
|
|
6111
|
+
` ${shellQuote(`${item.provider}|${uri}`)}) printf '%s\\n' ${shellQuote(item.bindingHash)}; return 0 ;;`,
|
|
6112
|
+
);
|
|
6113
|
+
});
|
|
6114
|
+
const soleWrapperHashes = [...bindingProviders.entries()].flatMap(([provider, ids]) => {
|
|
6115
|
+
if (ids.size !== 1) return [];
|
|
6116
|
+
const descriptor = wrapperDescriptors.find((item) => item.provider === provider);
|
|
6117
|
+
return descriptor
|
|
6118
|
+
? [
|
|
6119
|
+
` ${shellQuote(provider)}) printf '%s\\n' ${shellQuote(descriptor.bindingHash)}; return 0 ;;`,
|
|
6120
|
+
]
|
|
6121
|
+
: [];
|
|
6122
|
+
});
|
|
6123
|
+
const multiWrapperProviders =
|
|
6124
|
+
bindings.length > 0
|
|
6125
|
+
? [...bindingProviders.entries()]
|
|
6126
|
+
.filter(([, ids]) => ids.size > 1)
|
|
6127
|
+
.map(([provider]) => provider)
|
|
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);
|
|
4913
6154
|
return [
|
|
4914
|
-
...gitCredentialTokenWriterCommandLines(),
|
|
6155
|
+
...gitCredentialTokenWriterCommandLines(bindings),
|
|
4915
6156
|
// Provision git/provider-CLI helpers at SETUP (runtime) before any clone
|
|
4916
6157
|
// runs. Renewal updates only token files and deliberately leaves these
|
|
4917
6158
|
// repository-specific host mappings intact.
|
|
@@ -4928,8 +6169,8 @@ function gitCredentialHelperCommandLines(
|
|
|
4928
6169
|
' rest="${prompt_lower#*://}"',
|
|
4929
6170
|
' rest="${rest#*@}"',
|
|
4930
6171
|
' host="${rest%%/*}"',
|
|
4931
|
-
' host="${host%%:*}"',
|
|
4932
6172
|
' host="$(printf \'%s\\n\' "$host" | tr -d "\'")"',
|
|
6173
|
+
' host="${host%:}"',
|
|
4933
6174
|
" printf '%s\\n' \"$host\"",
|
|
4934
6175
|
"}",
|
|
4935
6176
|
"provider_for_prompt() {",
|
|
@@ -4941,7 +6182,7 @@ function gitCredentialHelperCommandLines(
|
|
|
4941
6182
|
" *github.com*|*githubusercontent.com*) printf '%s\\n' github ;;",
|
|
4942
6183
|
" *gitlab*) printf '%s\\n' gitlab ;;",
|
|
4943
6184
|
" *dev.azure.com*|*.visualstudio.com*) printf '%s\\n' azure_devops ;;",
|
|
4944
|
-
" *) printf '%s\\n' github ;;",
|
|
6185
|
+
strictAskpass ? " *) printf '\\n' ;;" : " *) printf '%s\\n' github ;;",
|
|
4945
6186
|
" esac",
|
|
4946
6187
|
"}",
|
|
4947
6188
|
"token_file_for_provider() {",
|
|
@@ -4967,6 +6208,41 @@ function gitCredentialHelperCommandLines(
|
|
|
4967
6208
|
"ASKPASS_EOF",
|
|
4968
6209
|
'chmod 0755 "$git_askpass.tmp.$$"',
|
|
4969
6210
|
'mv -f "$git_askpass.tmp.$$" "$git_askpass"',
|
|
6211
|
+
'git_credential_helper="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/helper"',
|
|
6212
|
+
'mkdir -p "$(dirname "$git_credential_helper")"',
|
|
6213
|
+
"cat > \"$git_credential_helper.tmp.$$\" <<'GIT_CREDENTIAL_HELPER_EOF'",
|
|
6214
|
+
"#!/usr/bin/env sh",
|
|
6215
|
+
"set -eu",
|
|
6216
|
+
'[ "${1:-get}" = get ] || exit 0',
|
|
6217
|
+
"protocol= host= path=",
|
|
6218
|
+
"while IFS='=' read -r key value; do",
|
|
6219
|
+
' case "$key" in',
|
|
6220
|
+
" protocol) protocol=\"$(printf '%s' \"$value\" | tr '[:upper:]' '[:lower:]')\" ;;",
|
|
6221
|
+
" host) host=\"$(printf '%s' \"$value\" | tr '[:upper:]' '[:lower:]')\" ;;",
|
|
6222
|
+
' path) path="${value#/}" ;;',
|
|
6223
|
+
" esac",
|
|
6224
|
+
"done",
|
|
6225
|
+
'credential_dir="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}"',
|
|
6226
|
+
"username= token_file=",
|
|
6227
|
+
'case "$protocol|$host|$path" in',
|
|
6228
|
+
...bindingCases,
|
|
6229
|
+
" *) exit 0 ;;",
|
|
6230
|
+
"esac",
|
|
6231
|
+
'[ -r "$token_file" ] || exit 0',
|
|
6232
|
+
'password="$(cat "$token_file" 2>/dev/null || true)"',
|
|
6233
|
+
'[ -n "$password" ] || exit 0',
|
|
6234
|
+
'printf \'username=%s\\npassword=%s\\n\' "$username" "$password"',
|
|
6235
|
+
"GIT_CREDENTIAL_HELPER_EOF",
|
|
6236
|
+
'chmod 0755 "$git_credential_helper.tmp.$$"',
|
|
6237
|
+
'mv -f "$git_credential_helper.tmp.$$" "$git_credential_helper"',
|
|
6238
|
+
// Empty helper resets lower-priority/system helpers; our exact path-aware
|
|
6239
|
+
// helper returns no credential for an unbound remote, so multi-binding
|
|
6240
|
+
// sessions fail closed instead of falling through to ambient credentials.
|
|
6241
|
+
"git config --global --unset-all credential.helper >/dev/null 2>&1 || true",
|
|
6242
|
+
"git config --global --add credential.helper ''",
|
|
6243
|
+
'git config --global --add credential.helper "$git_credential_helper"',
|
|
6244
|
+
"git config --global credential.useHttpPath true",
|
|
6245
|
+
...gitHttpBrokerConfigCommandLines(brokerRoutes),
|
|
4970
6246
|
'wrapper_dir="${OPENGENI_GIT_CLI_WRAPPER_DIR:-$HOME/.opengeni/bin}"',
|
|
4971
6247
|
'mkdir -p "$wrapper_dir"',
|
|
4972
6248
|
"for opengeni_git_cli_tool in gh glab az; do",
|
|
@@ -4981,11 +6257,71 @@ function gitCredentialHelperCommandLines(
|
|
|
4981
6257
|
" az) provider=azure_devops; token_env=AZURE_DEVOPS_EXT_PAT ;;",
|
|
4982
6258
|
" *) provider=; token_env= ;;",
|
|
4983
6259
|
"esac",
|
|
4984
|
-
|
|
6260
|
+
"hash_binding_id() {",
|
|
6261
|
+
" if command -v sha256sum >/dev/null 2>&1; then printf '%s' \"$1\" | sha256sum | cut -c1-32; return; fi",
|
|
6262
|
+
" if command -v shasum >/dev/null 2>&1; then printf '%s' \"$1\" | shasum -a 256 | cut -c1-32; return; fi",
|
|
6263
|
+
" if command -v openssl >/dev/null 2>&1; then printf '%s' \"$1\" | openssl dgst -sha256 | sed 's/^.*= //' | cut -c1-32; return; fi",
|
|
6264
|
+
" printf '%s\\n' 'No SHA-256 utility is available to select OPENGENI_GIT_BINDING' >&2",
|
|
6265
|
+
" return 127",
|
|
6266
|
+
"}",
|
|
6267
|
+
"binding_hash_allowed() {",
|
|
6268
|
+
' case "$provider|$1" in',
|
|
6269
|
+
...allowedWrapperHashes,
|
|
6270
|
+
" *) return 1 ;;",
|
|
6271
|
+
" esac",
|
|
6272
|
+
"}",
|
|
6273
|
+
"binding_hash_is_brokered() {",
|
|
6274
|
+
' case "$provider|$1" in',
|
|
6275
|
+
...brokeredBindingHashCases,
|
|
6276
|
+
" *) return 1 ;;",
|
|
6277
|
+
" esac",
|
|
6278
|
+
"}",
|
|
6279
|
+
"binding_hash_for_origin() {",
|
|
6280
|
+
' case "$provider|$1" in',
|
|
6281
|
+
...originWrapperHashes,
|
|
6282
|
+
" *) return 1 ;;",
|
|
6283
|
+
" esac",
|
|
6284
|
+
"}",
|
|
6285
|
+
"origin_is_brokered() {",
|
|
6286
|
+
' case "$provider|$1" in',
|
|
6287
|
+
...brokeredOriginCases,
|
|
6288
|
+
" *) return 1 ;;",
|
|
6289
|
+
" esac",
|
|
6290
|
+
"}",
|
|
6291
|
+
"sole_binding_hash() {",
|
|
4985
6292
|
' case "$provider" in',
|
|
4986
|
-
|
|
4987
|
-
|
|
6293
|
+
...soleWrapperHashes,
|
|
6294
|
+
" *) return 1 ;;",
|
|
4988
6295
|
" esac",
|
|
6296
|
+
"}",
|
|
6297
|
+
`multi_binding_providers=${shellQuote(multiWrapperProviders.join(" "))}`,
|
|
6298
|
+
`broker_only_providers=${shellQuote(brokerOnlyProviders.join(" "))}`,
|
|
6299
|
+
'if [ -n "$provider" ]; then',
|
|
6300
|
+
" binding_hash=",
|
|
6301
|
+
' if [ -n "${OPENGENI_GIT_BINDING:-}" ]; then',
|
|
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; }',
|
|
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; }',
|
|
6305
|
+
" elif command -v git >/dev/null 2>&1; then",
|
|
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; }',
|
|
6308
|
+
' [ -z "$origin" ] || binding_hash="$(binding_hash_for_origin "$origin" 2>/dev/null || true)"',
|
|
6309
|
+
" fi",
|
|
6310
|
+
' [ -n "$binding_hash" ] || binding_hash="$(sole_binding_hash 2>/dev/null || true)"',
|
|
6311
|
+
' if [ -n "$binding_hash" ]; then',
|
|
6312
|
+
' token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$binding_hash-token"',
|
|
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",
|
|
6317
|
+
' case " $multi_binding_providers " in',
|
|
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 ;;',
|
|
6319
|
+
" esac",
|
|
6320
|
+
' case "$provider" in',
|
|
6321
|
+
' github) token_file="${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" ;;',
|
|
6322
|
+
' *) token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$provider-token" ;;',
|
|
6323
|
+
" esac",
|
|
6324
|
+
" fi",
|
|
4989
6325
|
' if [ -f "$token_file" ]; then',
|
|
4990
6326
|
' token="$(cat "$token_file" 2>/dev/null || true)"',
|
|
4991
6327
|
' if [ -n "$token" ]; then',
|
|
@@ -5040,10 +6376,23 @@ export function gitProviderTokenRefreshCommand(seeds: GitTokenSeeds): string {
|
|
|
5040
6376
|
].join("\n");
|
|
5041
6377
|
}
|
|
5042
6378
|
|
|
6379
|
+
export function gitCredentialBindingTokenRefreshCommand(
|
|
6380
|
+
bindings: GitCredentialBindingSeed[],
|
|
6381
|
+
): string {
|
|
6382
|
+
const seedPrefix = gitCredentialBindingSeedExportPrefix(bindings);
|
|
6383
|
+
if (!seedPrefix) return "";
|
|
6384
|
+
return [
|
|
6385
|
+
seedPrefix,
|
|
6386
|
+
"set -eu",
|
|
6387
|
+
'export HOME="${HOME:-/workspace}"',
|
|
6388
|
+
...gitCredentialTokenWriterCommandLines(bindings),
|
|
6389
|
+
].join("\n");
|
|
6390
|
+
}
|
|
6391
|
+
|
|
5043
6392
|
export async function refreshGitProviderTokenFiles(
|
|
5044
6393
|
session: GitCredentialTokenWriterSession,
|
|
5045
6394
|
seeds: GitTokenSeeds,
|
|
5046
|
-
options: { runAs?: string } = {},
|
|
6395
|
+
options: { runAs?: string; commandRunner?: SandboxLifecycleCommandRunner } = {},
|
|
5047
6396
|
): Promise<void> {
|
|
5048
6397
|
const command = gitProviderTokenRefreshCommand(seeds);
|
|
5049
6398
|
if (!command) {
|
|
@@ -5056,23 +6405,41 @@ export async function refreshGitProviderTokenFiles(
|
|
|
5056
6405
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5057
6406
|
maxOutputTokens: 4_000,
|
|
5058
6407
|
};
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
6408
|
+
assertSandboxCommandSucceeded(
|
|
6409
|
+
await runSandboxLifecycleCommand(session, args, options.commandRunner),
|
|
6410
|
+
"Git credential refresh",
|
|
6411
|
+
);
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6414
|
+
export async function refreshGitCredentialBindingTokenFiles(
|
|
6415
|
+
session: GitCredentialTokenWriterSession,
|
|
6416
|
+
bindings: GitCredentialBindingSeed[],
|
|
6417
|
+
options: { runAs?: string; commandRunner?: SandboxLifecycleCommandRunner } = {},
|
|
6418
|
+
): Promise<void> {
|
|
6419
|
+
const command = gitCredentialBindingTokenRefreshCommand(bindings);
|
|
6420
|
+
if (!command) return;
|
|
6421
|
+
const args = {
|
|
6422
|
+
cmd: command,
|
|
6423
|
+
workdir: "/workspace",
|
|
6424
|
+
...(options.runAs ? { runAs: options.runAs } : {}),
|
|
6425
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
6426
|
+
maxOutputTokens: 4_000,
|
|
6427
|
+
};
|
|
6428
|
+
assertSandboxCommandSucceeded(
|
|
6429
|
+
await runSandboxLifecycleCommand(session, args, options.commandRunner),
|
|
6430
|
+
"Git credential binding refresh",
|
|
6431
|
+
);
|
|
5066
6432
|
}
|
|
5067
6433
|
|
|
5068
6434
|
export function repositoryCloneCommand(
|
|
5069
6435
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
6436
|
+
bindings: GitCredentialBindingSeed[] = [],
|
|
5070
6437
|
): string {
|
|
6438
|
+
assertUniqueResourceMountPaths(resources);
|
|
5071
6439
|
const commands = [
|
|
5072
6440
|
"set -eu",
|
|
5073
6441
|
'export HOME="${HOME:-/workspace}"',
|
|
5074
6442
|
'export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"',
|
|
5075
|
-
...gitCredentialHelperCommandLines(resources),
|
|
5076
6443
|
"ensure_git() {",
|
|
5077
6444
|
" if command -v git >/dev/null 2>&1; then",
|
|
5078
6445
|
" return 0",
|
|
@@ -5088,6 +6455,7 @@ export function repositoryCloneCommand(
|
|
|
5088
6455
|
" exit 127",
|
|
5089
6456
|
"}",
|
|
5090
6457
|
"ensure_git",
|
|
6458
|
+
...gitCredentialHelperCommandLines(resources, bindings),
|
|
5091
6459
|
"clone_repository() {",
|
|
5092
6460
|
' target="$1"',
|
|
5093
6461
|
' uri="$2"',
|
|
@@ -5160,33 +6528,41 @@ export function repositoryCloneCommand(
|
|
|
5160
6528
|
"}",
|
|
5161
6529
|
];
|
|
5162
6530
|
for (const resource of resources) {
|
|
5163
|
-
const
|
|
5164
|
-
const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
5165
|
-
const mountPath = normalizeManifestPath(resource.mountPath ?? `repos/${repo}`);
|
|
6531
|
+
const mountPath = resourceMountPath(resource);
|
|
5166
6532
|
commands.push(
|
|
5167
6533
|
[
|
|
5168
6534
|
"clone_repository",
|
|
5169
6535
|
shellQuote(posixPath.join("/workspace", mountPath)),
|
|
5170
6536
|
shellQuote(resource.uri),
|
|
5171
6537
|
shellQuote(resource.ref),
|
|
5172
|
-
shellQuote(resource.subpath ?
|
|
6538
|
+
shellQuote(resource.subpath ? normalizeRepositorySubpath(resource.subpath) : ""),
|
|
5173
6539
|
].join(" "),
|
|
5174
6540
|
);
|
|
5175
6541
|
}
|
|
5176
6542
|
return commands.join("\n");
|
|
5177
6543
|
}
|
|
5178
6544
|
|
|
5179
|
-
export function toolspaceTokenSeedCommand(
|
|
6545
|
+
export function toolspaceTokenSeedCommand(
|
|
6546
|
+
options: { tokenFile?: string; legacyTokenFile?: string } = {},
|
|
6547
|
+
): string {
|
|
5180
6548
|
return [
|
|
5181
6549
|
"set -eu",
|
|
5182
6550
|
'export HOME="${HOME:-/workspace}"',
|
|
5183
6551
|
'if [ -n "${OPENGENI_TOOLSPACE_TOKEN_SEED:-}" ]; then',
|
|
5184
6552
|
' seed_umask="$(umask)"',
|
|
5185
6553
|
" umask 077",
|
|
5186
|
-
|
|
6554
|
+
options.tokenFile
|
|
6555
|
+
? ` token_file=${shellQuote(options.tokenFile)}`
|
|
6556
|
+
: ' token_file="${OPENGENI_TOOLSPACE_TOKEN_FILE:-$HOME/.opengeni/toolspace-token}"',
|
|
6557
|
+
options.legacyTokenFile
|
|
6558
|
+
? ` legacy_token_file=${shellQuote(options.legacyTokenFile)}`
|
|
6559
|
+
: ' legacy_token_file=""',
|
|
5187
6560
|
' mkdir -p "$(dirname "$token_file")"',
|
|
5188
6561
|
' printf \'%s\' "$OPENGENI_TOOLSPACE_TOKEN_SEED" > "$token_file.tmp.$$"',
|
|
5189
6562
|
' mv -f "$token_file.tmp.$$" "$token_file"',
|
|
6563
|
+
' if [ -n "$legacy_token_file" ] && [ "$legacy_token_file" != "$token_file" ]; then',
|
|
6564
|
+
' rm -f -- "$legacy_token_file"',
|
|
6565
|
+
" fi",
|
|
5190
6566
|
' umask "$seed_umask"',
|
|
5191
6567
|
"fi",
|
|
5192
6568
|
].join("\n");
|
|
@@ -5199,28 +6575,56 @@ export async function runToolspaceTokenSeedHook(
|
|
|
5199
6575
|
if (!context.toolspaceTokenSeed) {
|
|
5200
6576
|
return;
|
|
5201
6577
|
}
|
|
5202
|
-
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(context.toolspaceTokenSeed)}\n${toolspaceTokenSeedCommand(
|
|
5203
|
-
|
|
5204
|
-
|
|
6578
|
+
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(context.toolspaceTokenSeed)}\n${toolspaceTokenSeedCommand(
|
|
6579
|
+
{
|
|
6580
|
+
...(context.toolspaceTokenFile ? { tokenFile: context.toolspaceTokenFile } : {}),
|
|
6581
|
+
...(context.toolspaceTokenFile && context.environment.OPENGENI_TOOLSPACE_TOKEN_FILE
|
|
6582
|
+
? { legacyTokenFile: context.environment.OPENGENI_TOOLSPACE_TOKEN_FILE }
|
|
6583
|
+
: {}),
|
|
6584
|
+
},
|
|
6585
|
+
)}`;
|
|
6586
|
+
const result = await runSandboxLifecycleCommand(
|
|
6587
|
+
session,
|
|
6588
|
+
{
|
|
5205
6589
|
cmd: command,
|
|
5206
6590
|
workdir: "/workspace",
|
|
5207
6591
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5208
6592
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5209
6593
|
maxOutputTokens: 4_000,
|
|
5210
|
-
}
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
6594
|
+
},
|
|
6595
|
+
context.commandRunner,
|
|
6596
|
+
);
|
|
6597
|
+
assertSandboxCommandSucceeded(result, "Toolspace token seed hook");
|
|
6598
|
+
}
|
|
6599
|
+
|
|
6600
|
+
export async function refreshToolspaceTokenFile(
|
|
6601
|
+
session: ToolspaceTokenWriterSession,
|
|
6602
|
+
token: string,
|
|
6603
|
+
options: {
|
|
6604
|
+
runAs?: string;
|
|
6605
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
6606
|
+
tokenFile?: string;
|
|
6607
|
+
legacyTokenFile?: string;
|
|
6608
|
+
} = {},
|
|
6609
|
+
): Promise<void> {
|
|
6610
|
+
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(token)}\n${toolspaceTokenSeedCommand(
|
|
6611
|
+
{
|
|
6612
|
+
...(options.tokenFile ? { tokenFile: options.tokenFile } : {}),
|
|
6613
|
+
...(options.legacyTokenFile ? { legacyTokenFile: options.legacyTokenFile } : {}),
|
|
6614
|
+
},
|
|
6615
|
+
)}`;
|
|
6616
|
+
const result = await runSandboxLifecycleCommand(
|
|
6617
|
+
session,
|
|
6618
|
+
{
|
|
5214
6619
|
cmd: command,
|
|
5215
6620
|
workdir: "/workspace",
|
|
5216
|
-
...(
|
|
6621
|
+
...(options.runAs ? { runAs: options.runAs } : {}),
|
|
5217
6622
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5218
6623
|
maxOutputTokens: 4_000,
|
|
5219
|
-
}
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
}
|
|
6624
|
+
},
|
|
6625
|
+
options.commandRunner,
|
|
6626
|
+
);
|
|
6627
|
+
assertSandboxCommandSucceeded(result, "Toolspace token refresh");
|
|
5224
6628
|
}
|
|
5225
6629
|
|
|
5226
6630
|
// Bounds the setup output tail carried on a rig.setup failure event/error so a
|
|
@@ -5329,13 +6733,7 @@ export async function runRigSetupHook(
|
|
|
5329
6733
|
};
|
|
5330
6734
|
let result: unknown;
|
|
5331
6735
|
try {
|
|
5332
|
-
|
|
5333
|
-
result = await session.exec(execArgs);
|
|
5334
|
-
} else if (session.execCommand) {
|
|
5335
|
-
result = await session.execCommand(execArgs);
|
|
5336
|
-
} else {
|
|
5337
|
-
throw new Error("Sandbox session does not support command execution");
|
|
5338
|
-
}
|
|
6736
|
+
result = await runSandboxLifecycleCommand(session, execArgs, context.commandRunner);
|
|
5339
6737
|
} catch (error) {
|
|
5340
6738
|
const message = error instanceof Error ? error.message : String(error);
|
|
5341
6739
|
await context.onRuntimeEvent?.({
|
|
@@ -5414,31 +6812,27 @@ export async function runRepositoryCloneHook(
|
|
|
5414
6812
|
...(context.gitTokenSeeds ?? {}),
|
|
5415
6813
|
...(context.gitTokenSeed ? { github: context.gitTokenSeed } : {}),
|
|
5416
6814
|
} satisfies GitTokenSeeds;
|
|
5417
|
-
const
|
|
5418
|
-
const
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
assertSandboxCommandSucceeded(result, "Repository clone hook");
|
|
5430
|
-
} else if (session.execCommand) {
|
|
5431
|
-
const result = await session.execCommand({
|
|
6815
|
+
const gitCredentialBindings = context.gitCredentialBindings ?? [];
|
|
6816
|
+
const seedPrefix = [
|
|
6817
|
+
gitTokenSeedExportPrefix(gitTokenSeeds),
|
|
6818
|
+
gitCredentialBindingSeedExportPrefix(gitCredentialBindings),
|
|
6819
|
+
]
|
|
6820
|
+
.filter(Boolean)
|
|
6821
|
+
.join("\n");
|
|
6822
|
+
const cloneCommand = repositoryCloneCommand(resources, gitCredentialBindings);
|
|
6823
|
+
const command = seedPrefix ? `${seedPrefix}\n${cloneCommand}` : cloneCommand;
|
|
6824
|
+
const result = await runSandboxLifecycleCommand(
|
|
6825
|
+
session,
|
|
6826
|
+
{
|
|
5432
6827
|
cmd: command,
|
|
5433
6828
|
workdir: "/workspace",
|
|
5434
6829
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5435
6830
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5436
6831
|
maxOutputTokens: 20_000,
|
|
5437
|
-
}
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
}
|
|
6832
|
+
},
|
|
6833
|
+
context.commandRunner,
|
|
6834
|
+
);
|
|
6835
|
+
assertSandboxCommandSucceeded(result, "Repository clone hook");
|
|
5442
6836
|
await context.onRuntimeEvent?.({
|
|
5443
6837
|
type: "sandbox.operation.completed",
|
|
5444
6838
|
payload,
|
|
@@ -5564,27 +6958,18 @@ export async function runAzureCliLoginHook(
|
|
|
5564
6958
|
payload,
|
|
5565
6959
|
});
|
|
5566
6960
|
try {
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
workdir: "/workspace",
|
|
5571
|
-
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5572
|
-
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5573
|
-
maxOutputTokens: 20_000,
|
|
5574
|
-
});
|
|
5575
|
-
assertSandboxCommandSucceeded(result, "Azure CLI login hook");
|
|
5576
|
-
} else if (session.execCommand) {
|
|
5577
|
-
const result = await session.execCommand({
|
|
6961
|
+
const result = await runSandboxLifecycleCommand(
|
|
6962
|
+
session,
|
|
6963
|
+
{
|
|
5578
6964
|
cmd: azureCliLoginCommand(),
|
|
5579
6965
|
workdir: "/workspace",
|
|
5580
6966
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5581
6967
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5582
6968
|
maxOutputTokens: 20_000,
|
|
5583
|
-
}
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
}
|
|
6969
|
+
},
|
|
6970
|
+
context.commandRunner,
|
|
6971
|
+
);
|
|
6972
|
+
assertSandboxCommandSucceeded(result, "Azure CLI login hook");
|
|
5588
6973
|
await context.onRuntimeEvent?.({
|
|
5589
6974
|
type: "sandbox.operation.completed",
|
|
5590
6975
|
payload,
|
|
@@ -5674,26 +7059,46 @@ function isPathWithin(root: string, candidate: string): boolean {
|
|
|
5674
7059
|
}
|
|
5675
7060
|
|
|
5676
7061
|
/**
|
|
5677
|
-
* The skill source fed to the SDK Skills capability. Without pack
|
|
5678
|
-
* is the plain bundled local-dir source, byte-for-byte the
|
|
5679
|
-
* With
|
|
5680
|
-
* skill directories (as local_dir
|
|
5681
|
-
*
|
|
5682
|
-
* index, one `## Skills` instruction section, lazy
|
|
5683
|
-
* them. A pack skill shadows a bundled
|
|
7062
|
+
* The skill source fed to the SDK Skills capability. Without pack or curated
|
|
7063
|
+
* skills this is the plain bundled local-dir source, byte-for-byte the
|
|
7064
|
+
* pre-pack behavior. With either selected source it becomes a single
|
|
7065
|
+
* in-memory dir source combining bundled skill directories (as local_dir
|
|
7066
|
+
* entries the SDK materializes lazily) with selected in-memory skill
|
|
7067
|
+
* directories — one skill index, one `## Skills` instruction section, lazy
|
|
7068
|
+
* `load_skill` for all of them. A pack skill shadows a bundled or curated
|
|
7069
|
+
* skill with the same directory name, case-insensitively.
|
|
5684
7070
|
*/
|
|
5685
|
-
export function lazySkillSourceWithPackSkills(
|
|
7071
|
+
export function lazySkillSourceWithPackSkills(
|
|
7072
|
+
packSkills: PackSkill[],
|
|
7073
|
+
skillLibrarySkills: PackSkill[] = [],
|
|
7074
|
+
): LocalDirLazySkillSource {
|
|
5686
7075
|
const bundledDir = bundledSkillsDir();
|
|
5687
7076
|
const bundled = localDirLazySkillSource({ src: bundledDir });
|
|
5688
|
-
if (packSkills.length === 0) {
|
|
7077
|
+
if (packSkills.length === 0 && skillLibrarySkills.length === 0) {
|
|
5689
7078
|
return bundled;
|
|
5690
7079
|
}
|
|
5691
7080
|
const children: Record<string, Entry> = {};
|
|
5692
7081
|
for (const name of bundledSkillDirNames(bundledDir)) {
|
|
5693
7082
|
children[name] = localDir({ src: join(bundledDir, name) });
|
|
5694
7083
|
}
|
|
7084
|
+
const libraryIndex: SkillIndexEntry[] = [];
|
|
7085
|
+
const libraryNameKeys = new Set<string>();
|
|
7086
|
+
for (const skill of skillLibrarySkills) {
|
|
7087
|
+
assertSafePackSkillName(skill.name);
|
|
7088
|
+
const key = skill.name.toLowerCase();
|
|
7089
|
+
if (libraryNameKeys.has(key)) {
|
|
7090
|
+
throw new Error(`Duplicate curated skill name: ${skill.name}`);
|
|
7091
|
+
}
|
|
7092
|
+
libraryNameKeys.add(key);
|
|
7093
|
+
removeSkillChildByNameKey(children, key);
|
|
7094
|
+
children[skill.name] = packSkillDirEntry(skill);
|
|
7095
|
+
libraryIndex.push({
|
|
7096
|
+
name: skill.name,
|
|
7097
|
+
description: packSkillDescription(skill),
|
|
7098
|
+
path: skill.name,
|
|
7099
|
+
});
|
|
7100
|
+
}
|
|
5695
7101
|
const packIndex: SkillIndexEntry[] = [];
|
|
5696
|
-
const packNames = new Set<string>();
|
|
5697
7102
|
const packNameKeys = new Set<string>();
|
|
5698
7103
|
for (const skill of packSkills) {
|
|
5699
7104
|
assertSafePackSkillName(skill.name);
|
|
@@ -5701,7 +7106,7 @@ export function lazySkillSourceWithPackSkills(packSkills: PackSkill[]): LocalDir
|
|
|
5701
7106
|
throw new Error(`Duplicate pack skill name: ${skill.name}`);
|
|
5702
7107
|
}
|
|
5703
7108
|
packNameKeys.add(skill.name.toLowerCase());
|
|
5704
|
-
|
|
7109
|
+
removeSkillChildByNameKey(children, skill.name.toLowerCase());
|
|
5705
7110
|
children[skill.name] = packSkillDirEntry(skill);
|
|
5706
7111
|
packIndex.push({
|
|
5707
7112
|
name: skill.name,
|
|
@@ -5713,13 +7118,62 @@ export function lazySkillSourceWithPackSkills(packSkills: PackSkill[]): LocalDir
|
|
|
5713
7118
|
source: dir({ children }),
|
|
5714
7119
|
getIndex: (manifest, skillsPath) => [
|
|
5715
7120
|
...(bundled.getIndex?.(manifest, skillsPath) ?? []).filter(
|
|
5716
|
-
(entry) =>
|
|
7121
|
+
(entry) =>
|
|
7122
|
+
!packNameKeys.has((entry.path ?? entry.name).toLowerCase()) &&
|
|
7123
|
+
!libraryNameKeys.has((entry.path ?? entry.name).toLowerCase()),
|
|
7124
|
+
),
|
|
7125
|
+
...libraryIndex.filter(
|
|
7126
|
+
(entry) => !packNameKeys.has((entry.path ?? entry.name).toLowerCase()),
|
|
5717
7127
|
),
|
|
5718
7128
|
...packIndex,
|
|
5719
7129
|
],
|
|
5720
7130
|
};
|
|
5721
7131
|
}
|
|
5722
7132
|
|
|
7133
|
+
function effectiveSkillSelections(
|
|
7134
|
+
librarySelections: readonly EffectiveSkillSelection[],
|
|
7135
|
+
librarySkills: readonly PackSkill[],
|
|
7136
|
+
packSkills: readonly PackSkill[],
|
|
7137
|
+
): readonly EffectiveSkillSelection[] {
|
|
7138
|
+
const defaultSelections = bundledSkillDirNames(bundledSkillsDir()).map((name) => ({
|
|
7139
|
+
id: `bundled:${name}`,
|
|
7140
|
+
name,
|
|
7141
|
+
source: "bundled" as const,
|
|
7142
|
+
version: null,
|
|
7143
|
+
contentSha256: null,
|
|
7144
|
+
reason: "deployment default skill bundle",
|
|
7145
|
+
}));
|
|
7146
|
+
const libraryNameKeys = new Set(librarySkills.map((skill) => skill.name.toLowerCase()));
|
|
7147
|
+
const packNameKeys = new Set(packSkills.map((skill) => skill.name.toLowerCase()));
|
|
7148
|
+
const selected = librarySelections.filter((selection) =>
|
|
7149
|
+
libraryNameKeys.has(selection.name.toLowerCase()),
|
|
7150
|
+
);
|
|
7151
|
+
return [
|
|
7152
|
+
...defaultSelections.filter(
|
|
7153
|
+
(selection) =>
|
|
7154
|
+
!libraryNameKeys.has(selection.name.toLowerCase()) &&
|
|
7155
|
+
!packNameKeys.has(selection.name.toLowerCase()),
|
|
7156
|
+
),
|
|
7157
|
+
...selected.filter((selection) => !packNameKeys.has(selection.name.toLowerCase())),
|
|
7158
|
+
...packSkills.map((skill) => ({
|
|
7159
|
+
id: `pack:${skill.name}`,
|
|
7160
|
+
name: skill.name,
|
|
7161
|
+
source: "pack" as const,
|
|
7162
|
+
version: null,
|
|
7163
|
+
contentSha256: null,
|
|
7164
|
+
reason: "enabled capability pack",
|
|
7165
|
+
})),
|
|
7166
|
+
];
|
|
7167
|
+
}
|
|
7168
|
+
|
|
7169
|
+
function removeSkillChildByNameKey(children: Record<string, Entry>, nameKey: string): void {
|
|
7170
|
+
for (const name of Object.keys(children)) {
|
|
7171
|
+
if (name.toLowerCase() === nameKey) {
|
|
7172
|
+
delete children[name];
|
|
7173
|
+
}
|
|
7174
|
+
}
|
|
7175
|
+
}
|
|
7176
|
+
|
|
5723
7177
|
function bundledSkillDirNames(root: string): string[] {
|
|
5724
7178
|
return readdirSync(root, { withFileTypes: true })
|
|
5725
7179
|
.filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "SKILL.md")))
|
|
@@ -5863,7 +7317,12 @@ function sortJson(value: unknown): unknown {
|
|
|
5863
7317
|
}
|
|
5864
7318
|
return value;
|
|
5865
7319
|
}
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
7320
|
+
function interruptionToolName(item: unknown): string {
|
|
7321
|
+
const candidate = item as {
|
|
7322
|
+
toolName?: unknown;
|
|
7323
|
+
name?: unknown;
|
|
7324
|
+
rawItem?: { name?: unknown };
|
|
7325
|
+
};
|
|
7326
|
+
const name = candidate?.toolName ?? candidate?.name ?? candidate?.rawItem?.name;
|
|
7327
|
+
return typeof name === "string" ? name : "";
|
|
5869
7328
|
}
|