@opengeni/runtime 0.7.0 → 0.11.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-DPULLAJG.js +215 -0
- package/dist/chunk-DPULLAJG.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/chunk-YUAFQEFY.js +21522 -0
- package/dist/chunk-YUAFQEFY.js.map +1 -0
- package/dist/{index-DIiiwQY2.d.ts → index-D6yi-JcU.d.ts} +138 -15
- package/dist/index.d.ts +333 -122
- package/dist/index.js +8260 -349
- package/dist/index.js.map +1 -1
- package/dist/localSources-T3HZ6KY4.js +15 -0
- package/dist/localSources-T3HZ6KY4.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 +13 -5
- package/src/bundled_hashicorp_terraform_skills/README.md +3 -3
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +1 -1
- package/src/context-compaction.ts +421 -24
- package/src/history-sanitizer.ts +14 -5
- package/src/index.ts +1451 -304
- package/src/sandbox/channel-a.ts +535 -97
- package/src/sandbox/index.ts +36 -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/skill-library.ts +323 -0
- 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,16 +12,28 @@ import {
|
|
|
12
12
|
sandboxLifecycleHookIds,
|
|
13
13
|
} from "@opengeni/config";
|
|
14
14
|
import {
|
|
15
|
+
approvalIdentifier,
|
|
15
16
|
CAPABILITY_DESCRIPTORS,
|
|
17
|
+
assertUniqueResourceMountPaths,
|
|
18
|
+
gitCredentialBindingIdForRepository,
|
|
19
|
+
gitCredentialProviderForRepository,
|
|
16
20
|
isClearedRunStateBlob,
|
|
21
|
+
normalizeRepositorySubpath,
|
|
22
|
+
normalizeResourceMountPath,
|
|
17
23
|
prefixedMcpToolName as sharedPrefixedMcpToolName,
|
|
24
|
+
resourceMountPath,
|
|
25
|
+
sessionEventMediaPreview,
|
|
26
|
+
sessionEventMediaPreviewFromDataUrl,
|
|
18
27
|
signDelegatedAccessToken,
|
|
28
|
+
RequestHumanInputToolInput,
|
|
19
29
|
type GitCredentialProvider,
|
|
30
|
+
type HumanInputResponse,
|
|
20
31
|
type McpServerConnectionRef,
|
|
21
32
|
type Permission,
|
|
22
33
|
type ReasoningEffort,
|
|
23
34
|
type ResourceRef,
|
|
24
35
|
type SessionEventType,
|
|
36
|
+
type SessionEventMediaPreview,
|
|
25
37
|
type ToolAuthNeededPayload,
|
|
26
38
|
type ToolRef,
|
|
27
39
|
} from "@opengeni/contracts";
|
|
@@ -52,6 +64,7 @@ import {
|
|
|
52
64
|
setDefaultOpenAIKey,
|
|
53
65
|
setOpenAIResponsesTransport,
|
|
54
66
|
setTracingDisabled,
|
|
67
|
+
tool as agentTool,
|
|
55
68
|
// Hosted web_search tool factory. Re-exported from @openai/agents-openai via
|
|
56
69
|
// `export * from '@openai/agents-openai'` in @openai/agents' index (0.11.6);
|
|
57
70
|
// it returns a { type: 'hosted_tool', providerData: { type: 'web_search' } }
|
|
@@ -104,11 +117,13 @@ import {
|
|
|
104
117
|
CODEX_APPS_MCP_SERVER_ID,
|
|
105
118
|
CODEX_MODEL_ID_PREFIX,
|
|
106
119
|
CODEX_ORIGINATOR,
|
|
120
|
+
boundModelToolOutputItems,
|
|
107
121
|
codexAppsSanitizingFetch,
|
|
108
122
|
codexRequestStorage,
|
|
109
123
|
codexSubscriptionFetch,
|
|
110
124
|
} from "@opengeni/codex";
|
|
111
125
|
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
126
|
+
import { createHash } from "node:crypto";
|
|
112
127
|
import { dirname, isAbsolute, join, posix as posixPath, relative } from "node:path";
|
|
113
128
|
import { fileURLToPath } from "node:url";
|
|
114
129
|
|
|
@@ -121,22 +136,52 @@ import {
|
|
|
121
136
|
import { installCodexToolSearch } from "./codex-tool-search";
|
|
122
137
|
import {
|
|
123
138
|
CompactionNeededError,
|
|
139
|
+
CompactionProviderResponseError,
|
|
140
|
+
EmptyCompactionSummaryError,
|
|
124
141
|
SUMMARY_BUFFER_TOKENS,
|
|
125
142
|
compactionThresholdTokens,
|
|
126
|
-
|
|
143
|
+
estimateCompleteModelInput,
|
|
144
|
+
estimateSerializedValueTokens,
|
|
145
|
+
hasModelGeneratedItem,
|
|
127
146
|
renderCompactionPromptInputForChat,
|
|
147
|
+
type CompleteModelInputFootprint,
|
|
148
|
+
type ProviderContextTokenSignal,
|
|
128
149
|
} from "./context-compaction";
|
|
129
150
|
import {
|
|
130
151
|
createSandboxClient,
|
|
131
152
|
desktopCapableBackend,
|
|
132
153
|
restoredSandboxSessionStateFromEntry,
|
|
133
154
|
setSelfhostedApplyDiff,
|
|
155
|
+
toolspaceTokenFileFromEnvironment,
|
|
156
|
+
withToolspaceTokenClient,
|
|
157
|
+
withToolspaceTokenSession,
|
|
158
|
+
withRunCredentialsClient,
|
|
159
|
+
withRunCredentialsSession,
|
|
160
|
+
type RunCredentialSessionReady,
|
|
134
161
|
} from "./sandbox";
|
|
135
162
|
import { runWithToolCallCorrelation } from "./sandbox/op-correlation";
|
|
163
|
+
import {
|
|
164
|
+
createTurnToolCancellationController,
|
|
165
|
+
TurnSandboxCommandCancelledError,
|
|
166
|
+
wrapCapabilityToolsForTurnCancellation,
|
|
167
|
+
type TurnSandboxCommandArgs,
|
|
168
|
+
type TurnSandboxCommandSession,
|
|
169
|
+
type TurnToolCancellationFence,
|
|
170
|
+
} from "./sandbox/turn-tool-cancellation";
|
|
136
171
|
import { computerUse, type ComputerToolMode } from "./sandbox-computer";
|
|
137
172
|
import type { RuntimeMetricsHooks } from "./metrics";
|
|
173
|
+
export {
|
|
174
|
+
getSkillLibraryEntry,
|
|
175
|
+
isSkillLibraryEntryId,
|
|
176
|
+
listSkillLibraryEntries,
|
|
177
|
+
loadSkillLibrarySkill,
|
|
178
|
+
type SkillLibraryEntry,
|
|
179
|
+
type SkillLibraryFile,
|
|
180
|
+
type SkillLibrarySkill,
|
|
181
|
+
} from "./skill-library";
|
|
138
182
|
|
|
139
183
|
export type { RuntimeMetricsHooks } from "./metrics";
|
|
184
|
+
export type { TurnToolCancellationFence } from "./sandbox/turn-tool-cancellation";
|
|
140
185
|
|
|
141
186
|
// P4.3 computer-use surface (the agent's :0 driver). Re-exported from the barrel
|
|
142
187
|
// so callers (the worker, live proofs) reach SandboxComputer/ComputerUseCapability
|
|
@@ -188,17 +233,25 @@ export { OpenAIChatCompletionsModel, OpenAIResponsesModel } from "@openai/agents
|
|
|
188
233
|
|
|
189
234
|
export {
|
|
190
235
|
CompactionNeededError,
|
|
236
|
+
CompactionProviderResponseError,
|
|
237
|
+
EmptyCompactionSummaryError,
|
|
191
238
|
buildCompactionPromptInput,
|
|
192
239
|
buildCompactionReplacementHistory,
|
|
240
|
+
compactionReplacementFingerprint,
|
|
193
241
|
compactionThresholdTokens,
|
|
194
242
|
clampCompactionThresholdRatio,
|
|
195
243
|
decideCompaction,
|
|
196
244
|
buildSummaryItem,
|
|
197
245
|
findCompactionNeededError,
|
|
198
246
|
isCompactionSummary,
|
|
247
|
+
latestCompactionReplacementFingerprint,
|
|
248
|
+
prepareCompactionPromptInput,
|
|
199
249
|
isUserMessage,
|
|
200
250
|
estimateTokens,
|
|
201
251
|
estimateItemTokens,
|
|
252
|
+
estimateCompleteModelInput,
|
|
253
|
+
estimateSerializedValueTokens,
|
|
254
|
+
hasModelGeneratedItem,
|
|
202
255
|
renderCompactionPromptInputForChat,
|
|
203
256
|
COMPACTION_SUMMARY_MARKER,
|
|
204
257
|
COMPACTION_PROMPT,
|
|
@@ -211,7 +264,11 @@ export {
|
|
|
211
264
|
isEphemeralInternalContext,
|
|
212
265
|
USER_MESSAGE_TRUNCATION_MARKER,
|
|
213
266
|
} from "./context-compaction";
|
|
214
|
-
export type {
|
|
267
|
+
export type {
|
|
268
|
+
CompactionDecision,
|
|
269
|
+
CompactionItem,
|
|
270
|
+
PreparedCompactionPromptInput,
|
|
271
|
+
} from "./context-compaction";
|
|
215
272
|
export { modelCallUsageTelemetry } from "./usage-telemetry";
|
|
216
273
|
export type { ModelCallUsageTelemetry } from "./usage-telemetry";
|
|
217
274
|
|
|
@@ -249,14 +306,21 @@ export type ResolveConnectionCredentialInput = {
|
|
|
249
306
|
};
|
|
250
307
|
|
|
251
308
|
export type ResolveConnectionCredentialResult =
|
|
252
|
-
| {
|
|
309
|
+
| {
|
|
310
|
+
status: "ok";
|
|
311
|
+
headers: Record<string, string>;
|
|
312
|
+
connectionId: string;
|
|
313
|
+
expiresAt?: Date | null;
|
|
314
|
+
}
|
|
253
315
|
| {
|
|
254
316
|
status: "auth_needed";
|
|
255
317
|
reason: ToolAuthNeededPayload["reason"];
|
|
256
318
|
providerDomain: string;
|
|
319
|
+
provider?: string;
|
|
257
320
|
connectionId?: string;
|
|
258
321
|
scopes?: string[];
|
|
259
322
|
resource?: string;
|
|
323
|
+
selectedResources?: McpServerConnectionRef["selectedResources"];
|
|
260
324
|
authorizationUrl?: string;
|
|
261
325
|
};
|
|
262
326
|
|
|
@@ -311,6 +375,11 @@ export type AgentSegmentInput =
|
|
|
311
375
|
approvalId: string;
|
|
312
376
|
decision: "approve" | "reject";
|
|
313
377
|
message?: string;
|
|
378
|
+
}
|
|
379
|
+
| {
|
|
380
|
+
kind: "human_input";
|
|
381
|
+
serializedRunState: string;
|
|
382
|
+
toolCallId: string;
|
|
314
383
|
};
|
|
315
384
|
|
|
316
385
|
export type PreparedAgentInput = {
|
|
@@ -318,6 +387,13 @@ export type PreparedAgentInput = {
|
|
|
318
387
|
sandboxSessionState?: SandboxSessionState;
|
|
319
388
|
};
|
|
320
389
|
|
|
390
|
+
export const HUMAN_INPUT_TOOL_NAME = "request_human_input";
|
|
391
|
+
|
|
392
|
+
export type SerializedHumanInputInterruption = {
|
|
393
|
+
toolCallId: string;
|
|
394
|
+
input: ReturnType<typeof RequestHumanInputToolInput.parse>;
|
|
395
|
+
};
|
|
396
|
+
|
|
321
397
|
export type SandboxFileDownload = {
|
|
322
398
|
fileId: string;
|
|
323
399
|
mountPath: string;
|
|
@@ -376,6 +452,7 @@ export type OpenGeniRuntime = {
|
|
|
376
452
|
options?: RunAgentStreamOptions,
|
|
377
453
|
) => Promise<Awaited<ReturnType<typeof runAgentStream>>>;
|
|
378
454
|
serializeApprovals: (interruptions: unknown[]) => unknown[];
|
|
455
|
+
serializeHumanInputRequests?: (interruptions: unknown[]) => SerializedHumanInputInterruption[];
|
|
379
456
|
};
|
|
380
457
|
|
|
381
458
|
export type ProductionRuntimeOverrides = {
|
|
@@ -411,6 +488,7 @@ export function createProductionAgentRuntime(
|
|
|
411
488
|
sandboxClient: overrides.sandboxClient,
|
|
412
489
|
}),
|
|
413
490
|
serializeApprovals,
|
|
491
|
+
serializeHumanInputRequests,
|
|
414
492
|
};
|
|
415
493
|
}
|
|
416
494
|
|
|
@@ -799,19 +877,28 @@ export async function summarizeForCompaction(
|
|
|
799
877
|
const maxTokens = options.maxOutputTokens ?? SUMMARY_BUFFER_TOKENS;
|
|
800
878
|
if (api === "chat") {
|
|
801
879
|
const transcript = renderCompactionPromptInputForChat(input);
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
880
|
+
let completion: unknown;
|
|
881
|
+
try {
|
|
882
|
+
completion = await client.chat.completions.create({
|
|
883
|
+
model,
|
|
884
|
+
max_tokens: maxTokens,
|
|
885
|
+
messages: [{ role: "user", content: transcript }],
|
|
886
|
+
...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
|
|
887
|
+
} as any);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(error), error);
|
|
890
|
+
}
|
|
808
891
|
const usage = modelResponseUsageFromResponse(completion);
|
|
809
892
|
if (usage) {
|
|
810
893
|
await options.onUsage?.(usage);
|
|
811
894
|
}
|
|
812
895
|
const text = (completion as { choices?: Array<{ message?: { content?: unknown } }> })
|
|
813
896
|
.choices?.[0]?.message?.content;
|
|
814
|
-
|
|
897
|
+
const summary = typeof text === "string" ? text.trim() : "";
|
|
898
|
+
if (!summary) {
|
|
899
|
+
throw new EmptyCompactionSummaryError(compactionResponseDiagnostics(completion, summary));
|
|
900
|
+
}
|
|
901
|
+
return summary;
|
|
815
902
|
}
|
|
816
903
|
// Use the same SDK Responses adapter as the real agent call. It converts the
|
|
817
904
|
// structured AgentInputItems (callId/providerData/etc.) to provider wire
|
|
@@ -834,12 +921,205 @@ export async function summarizeForCompaction(
|
|
|
834
921
|
handoffs: [],
|
|
835
922
|
tracing: false,
|
|
836
923
|
};
|
|
837
|
-
|
|
924
|
+
let response: unknown;
|
|
925
|
+
try {
|
|
926
|
+
response = await new CompactionResponsesModel(client, model).fetchResponse(request);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(error), error);
|
|
929
|
+
}
|
|
838
930
|
const usage = modelResponseUsageFromResponse(response);
|
|
839
931
|
if (usage) {
|
|
840
932
|
await options.onUsage?.(usage);
|
|
841
933
|
}
|
|
842
|
-
|
|
934
|
+
if (isFailedCompactionProviderResponse(response)) {
|
|
935
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(response));
|
|
936
|
+
}
|
|
937
|
+
const summary = extractResponseOutputText(response).trim();
|
|
938
|
+
if (!summary) {
|
|
939
|
+
throw new EmptyCompactionSummaryError(compactionResponseDiagnostics(response, summary));
|
|
940
|
+
}
|
|
941
|
+
return summary;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function isFailedCompactionProviderResponse(response: unknown): boolean {
|
|
945
|
+
if (!response || typeof response !== "object") return false;
|
|
946
|
+
const record = response as Record<string, unknown>;
|
|
947
|
+
return (
|
|
948
|
+
record.status === "failed" ||
|
|
949
|
+
record.status === "incomplete" ||
|
|
950
|
+
(record.error !== null && record.error !== undefined)
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** Bounded provider diagnostics: identifiers and classifications, never messages or model data. */
|
|
955
|
+
export function compactionProviderFailureDiagnostics(error: unknown): Record<string, unknown> {
|
|
956
|
+
let current = error;
|
|
957
|
+
let errorName: string | null = null;
|
|
958
|
+
let httpStatus: number | null = null;
|
|
959
|
+
let responseStatus: string | null = null;
|
|
960
|
+
let responseId: string | null = null;
|
|
961
|
+
let code: string | null = null;
|
|
962
|
+
let type: string | null = null;
|
|
963
|
+
let requestId: string | null = null;
|
|
964
|
+
let eventType: string | null = null;
|
|
965
|
+
const seen = new Set<object>();
|
|
966
|
+
for (let depth = 0; depth < 6 && current && typeof current === "object"; depth += 1) {
|
|
967
|
+
if (seen.has(current)) break;
|
|
968
|
+
seen.add(current);
|
|
969
|
+
const record = current as Record<string, unknown>;
|
|
970
|
+
if (!errorName && current instanceof Error) {
|
|
971
|
+
errorName = boundCompactionDiagnosticField(current.name);
|
|
972
|
+
}
|
|
973
|
+
if (
|
|
974
|
+
httpStatus === null &&
|
|
975
|
+
typeof record.status === "number" &&
|
|
976
|
+
Number.isFinite(record.status)
|
|
977
|
+
) {
|
|
978
|
+
httpStatus = record.status;
|
|
979
|
+
}
|
|
980
|
+
if (responseStatus === null && typeof record.status === "string") {
|
|
981
|
+
responseStatus = boundCompactionDiagnosticField(record.status);
|
|
982
|
+
}
|
|
983
|
+
const directResponseStatus = record.response_status ?? record.responseStatus;
|
|
984
|
+
if (responseStatus === null && typeof directResponseStatus === "string") {
|
|
985
|
+
responseStatus = boundCompactionDiagnosticField(directResponseStatus);
|
|
986
|
+
}
|
|
987
|
+
const directResponseId = record.response_id ?? record.responseId ?? record.id;
|
|
988
|
+
if (responseId === null && typeof directResponseId === "string") {
|
|
989
|
+
responseId = boundCompactionDiagnosticField(directResponseId);
|
|
990
|
+
}
|
|
991
|
+
if (code === null && typeof record.code === "string") {
|
|
992
|
+
code = boundCompactionDiagnosticField(record.code);
|
|
993
|
+
}
|
|
994
|
+
if (type === null && typeof record.type === "string") {
|
|
995
|
+
type = boundCompactionDiagnosticField(record.type);
|
|
996
|
+
}
|
|
997
|
+
const directEventType = record.event_type ?? record.eventType;
|
|
998
|
+
if (eventType === null && typeof directEventType === "string") {
|
|
999
|
+
eventType = boundCompactionDiagnosticField(directEventType);
|
|
1000
|
+
}
|
|
1001
|
+
if (requestId === null) {
|
|
1002
|
+
const directRequestId = record.request_id ?? record.requestId ?? record._request_id;
|
|
1003
|
+
if (typeof directRequestId === "string") {
|
|
1004
|
+
requestId = boundCompactionDiagnosticField(directRequestId);
|
|
1005
|
+
}
|
|
1006
|
+
const headers = record.headers;
|
|
1007
|
+
if (!requestId && headers && typeof headers === "object") {
|
|
1008
|
+
const get = (headers as { get?: unknown }).get;
|
|
1009
|
+
if (typeof get === "function") {
|
|
1010
|
+
const headerId = get.call(headers, "x-request-id");
|
|
1011
|
+
if (typeof headerId === "string") {
|
|
1012
|
+
requestId = boundCompactionDiagnosticField(headerId);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const nestedError = record.error;
|
|
1018
|
+
if (nestedError && typeof nestedError === "object" && !seen.has(nestedError)) {
|
|
1019
|
+
const nested = nestedError as Record<string, unknown>;
|
|
1020
|
+
if (code === null && typeof nested.code === "string") {
|
|
1021
|
+
code = boundCompactionDiagnosticField(nested.code);
|
|
1022
|
+
}
|
|
1023
|
+
if (type === null && typeof nested.type === "string") {
|
|
1024
|
+
type = boundCompactionDiagnosticField(nested.type);
|
|
1025
|
+
}
|
|
1026
|
+
const nestedResponseStatus = nested.response_status ?? nested.responseStatus;
|
|
1027
|
+
if (responseStatus === null && typeof nestedResponseStatus === "string") {
|
|
1028
|
+
responseStatus = boundCompactionDiagnosticField(nestedResponseStatus);
|
|
1029
|
+
}
|
|
1030
|
+
const nestedResponseId = nested.response_id ?? nested.responseId ?? nested.id;
|
|
1031
|
+
if (responseId === null && typeof nestedResponseId === "string") {
|
|
1032
|
+
responseId = boundCompactionDiagnosticField(nestedResponseId);
|
|
1033
|
+
}
|
|
1034
|
+
const nestedEventType = nested.event_type ?? nested.eventType;
|
|
1035
|
+
if (eventType === null && typeof nestedEventType === "string") {
|
|
1036
|
+
eventType = boundCompactionDiagnosticField(nestedEventType);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
current = record.cause;
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
errorName,
|
|
1043
|
+
httpStatus,
|
|
1044
|
+
responseStatus,
|
|
1045
|
+
responseId,
|
|
1046
|
+
code,
|
|
1047
|
+
type,
|
|
1048
|
+
requestId,
|
|
1049
|
+
...(eventType ? { eventType } : {}),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES = 256;
|
|
1054
|
+
const COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER = "…[truncated]";
|
|
1055
|
+
|
|
1056
|
+
function boundCompactionDiagnosticField(value: string): string {
|
|
1057
|
+
const bytes = Buffer.from(value, "utf8");
|
|
1058
|
+
if (bytes.length <= COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES) return value;
|
|
1059
|
+
const markerBytes = Buffer.byteLength(COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER, "utf8");
|
|
1060
|
+
let end = COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES - markerBytes;
|
|
1061
|
+
while (end > 0 && isUtf8ContinuationByte(bytes[end]!)) end -= 1;
|
|
1062
|
+
return `${bytes.subarray(0, end).toString("utf8")}${COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER}`;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function isUtf8ContinuationByte(value: number): boolean {
|
|
1066
|
+
return (value & 0xc0) === 0x80;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** Bounded, content-free diagnostics for a semantically empty checkpoint. */
|
|
1070
|
+
export function compactionResponseDiagnostics(
|
|
1071
|
+
response: unknown,
|
|
1072
|
+
extractedText = extractResponseOutputText(response).trim(),
|
|
1073
|
+
): Record<string, unknown> {
|
|
1074
|
+
if (!response || typeof response !== "object") {
|
|
1075
|
+
return { responseShape: typeof response, extractedTextLength: extractedText.length };
|
|
1076
|
+
}
|
|
1077
|
+
const record = response as Record<string, unknown>;
|
|
1078
|
+
const output = Array.isArray(record.output) ? record.output : [];
|
|
1079
|
+
const outputItems = output.slice(0, 50).map((item) => {
|
|
1080
|
+
if (!item || typeof item !== "object") return { type: typeof item };
|
|
1081
|
+
const value = item as Record<string, unknown>;
|
|
1082
|
+
const content = Array.isArray(value.content) ? value.content : [];
|
|
1083
|
+
return {
|
|
1084
|
+
type: typeof value.type === "string" ? value.type : null,
|
|
1085
|
+
role: typeof value.role === "string" ? value.role : null,
|
|
1086
|
+
status: typeof value.status === "string" ? value.status : null,
|
|
1087
|
+
contentPartTypes: content
|
|
1088
|
+
.slice(0, 50)
|
|
1089
|
+
.map((part) =>
|
|
1090
|
+
part && typeof part === "object" && typeof (part as { type?: unknown }).type === "string"
|
|
1091
|
+
? (part as { type: string }).type
|
|
1092
|
+
: typeof part,
|
|
1093
|
+
),
|
|
1094
|
+
contentPartCount: content.length,
|
|
1095
|
+
};
|
|
1096
|
+
});
|
|
1097
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
1098
|
+
const usage = modelResponseUsageFromResponse(response)?.usage;
|
|
1099
|
+
const incomplete =
|
|
1100
|
+
record.incomplete_details && typeof record.incomplete_details === "object"
|
|
1101
|
+
? (record.incomplete_details as Record<string, unknown>)
|
|
1102
|
+
: null;
|
|
1103
|
+
return {
|
|
1104
|
+
responseId: typeof record.id === "string" ? record.id : null,
|
|
1105
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
1106
|
+
outputItemCount: output.length,
|
|
1107
|
+
outputItems,
|
|
1108
|
+
choiceCount: choices.length,
|
|
1109
|
+
finishReasons: choices
|
|
1110
|
+
.slice(0, 20)
|
|
1111
|
+
.map((choice) =>
|
|
1112
|
+
choice && typeof choice === "object"
|
|
1113
|
+
? ((choice as Record<string, unknown>).finish_reason ?? null)
|
|
1114
|
+
: null,
|
|
1115
|
+
),
|
|
1116
|
+
incompleteReason:
|
|
1117
|
+
incomplete && typeof incomplete.reason === "string" ? incomplete.reason : null,
|
|
1118
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
1119
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
1120
|
+
totalTokens: usage?.totalTokens ?? null,
|
|
1121
|
+
extractedTextLength: extractedText.length,
|
|
1122
|
+
};
|
|
843
1123
|
}
|
|
844
1124
|
|
|
845
1125
|
/**
|
|
@@ -904,10 +1184,49 @@ class CompactionResponsesModel extends OpenAIResponsesModel {
|
|
|
904
1184
|
}
|
|
905
1185
|
|
|
906
1186
|
export type GitTokenSeeds = Partial<Record<GitCredentialProvider, string>>;
|
|
1187
|
+
export type GitCredentialBindingSeed = {
|
|
1188
|
+
credentialBindingId: string;
|
|
1189
|
+
provider: GitCredentialProvider;
|
|
1190
|
+
token: string;
|
|
1191
|
+
expiresAt?: string;
|
|
1192
|
+
/** Total active bindings for this provider, used to suppress unsafe aliases. */
|
|
1193
|
+
providerBindingCount?: number;
|
|
1194
|
+
};
|
|
907
1195
|
export type GitCredentialTokenWriterSession = SandboxSessionLike;
|
|
1196
|
+
export type ToolspaceTokenWriterSession = SandboxSessionLike;
|
|
1197
|
+
|
|
1198
|
+
export type EffectiveSkillSelection = Readonly<{
|
|
1199
|
+
id: string;
|
|
1200
|
+
name: string;
|
|
1201
|
+
source: "bundled" | "library" | "pack";
|
|
1202
|
+
version: string | null;
|
|
1203
|
+
contentSha256: string | null;
|
|
1204
|
+
reason: string;
|
|
1205
|
+
}>;
|
|
1206
|
+
|
|
1207
|
+
const agentSkillSelections = new WeakMap<object, readonly EffectiveSkillSelection[]>();
|
|
1208
|
+
const emptySkillSelections: readonly EffectiveSkillSelection[] = Object.freeze([]);
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Read-only, secret-free skill provenance for an already-built agent. This is
|
|
1212
|
+
* intentionally separate from the model instructions and sandbox manifest so
|
|
1213
|
+
* effective configuration inspection cannot accidentally expose credentials or
|
|
1214
|
+
* turn skill activation into an authorization change.
|
|
1215
|
+
*/
|
|
1216
|
+
export function effectiveSkillSelectionsForAgent(
|
|
1217
|
+
agent: object,
|
|
1218
|
+
): readonly EffectiveSkillSelection[] {
|
|
1219
|
+
return agentSkillSelections.get(agent) ?? emptySkillSelections;
|
|
1220
|
+
}
|
|
908
1221
|
|
|
909
1222
|
export type BuildAgentOptions = {
|
|
910
1223
|
model?: Model;
|
|
1224
|
+
/** Settled response for the one internal human-input interruption resumed by this run. */
|
|
1225
|
+
humanInputResponse?: {
|
|
1226
|
+
requestId: string;
|
|
1227
|
+
toolCallId: string;
|
|
1228
|
+
response: HumanInputResponse;
|
|
1229
|
+
};
|
|
911
1230
|
reasoningEffort?: ReasoningEffort;
|
|
912
1231
|
// Per-turn gating overrides for the multi-provider path. Each defaults to
|
|
913
1232
|
// today's settings-derived behaviour when omitted, so the legacy
|
|
@@ -1003,11 +1322,18 @@ export type BuildAgentOptions = {
|
|
|
1003
1322
|
// repository-clone hooks; runStream forwards them into the hook context, which
|
|
1004
1323
|
// seeds provider token FILES before the clone/setup runs.
|
|
1005
1324
|
gitTokenSeeds?: GitTokenSeeds;
|
|
1325
|
+
// Provider-neutral, independently mintable credentials. Binding ids remain
|
|
1326
|
+
// off-manifest and are hashed before they influence sandbox paths.
|
|
1327
|
+
gitCredentialBindings?: GitCredentialBindingSeed[];
|
|
1006
1328
|
// TOOLSPACE: the run-scoped delegated token to seed into
|
|
1007
1329
|
// $OPENGENI_TOOLSPACE_TOKEN_FILE. Like gitTokenSeed, this stays off the
|
|
1008
1330
|
// manifest/env delta and is written into the sandbox filesystem by a lifecycle
|
|
1009
1331
|
// hook before the agent starts.
|
|
1010
1332
|
toolspaceTokenSeed?: string;
|
|
1333
|
+
// Durable OpenGeni session identity used only to derive the off-manifest,
|
|
1334
|
+
// per-session token file. Required together with toolspaceTokenSeed so two
|
|
1335
|
+
// sessions sharing one box never overwrite the same pointer.
|
|
1336
|
+
toolspaceTokenSessionId?: string;
|
|
1011
1337
|
// Genesis turn only: inject a one-shot instruction into the FIRST model
|
|
1012
1338
|
// call telling it to title the session via opengeni__set_session_title.
|
|
1013
1339
|
// Keeping this out of the persistent Agent.instructions prevents every
|
|
@@ -1035,10 +1361,32 @@ export type BuildAgentOptions = {
|
|
|
1035
1361
|
// timeline message. Omitted ⇒ the composed instructions are byte-identical to
|
|
1036
1362
|
// a workspace-only persona.
|
|
1037
1363
|
sessionInstructions?: string;
|
|
1364
|
+
// Host context for this exact accepted turn. Composed system-level after the
|
|
1365
|
+
// durable session persona and omitted from all later turns.
|
|
1366
|
+
turnInstructions?: string;
|
|
1038
1367
|
// Skills delivered by enabled capability packs. They join the bundled
|
|
1039
1368
|
// skills in the sandbox skill index (mounted under .agents/) so
|
|
1040
1369
|
// skills/<name> references resolve like any other indexed skill.
|
|
1041
1370
|
packSkills?: PackSkill[];
|
|
1371
|
+
// Explicitly selected, immutable curated-library skill content. These are
|
|
1372
|
+
// separate from packSkills so repository-local/pack compatibility does not
|
|
1373
|
+
// turn a curated entry into a mutable override.
|
|
1374
|
+
skillLibrarySkills?: PackSkill[];
|
|
1375
|
+
// Secret-free provenance for effective-configuration inspection. The worker
|
|
1376
|
+
// resolves exact ids/versions/hashes before constructing the agent.
|
|
1377
|
+
skillLibrarySelections?: EffectiveSkillSelection[];
|
|
1378
|
+
/**
|
|
1379
|
+
* Internal per-attempt cancellation boundary. The worker supplies Temporal's
|
|
1380
|
+
* signal so an in-flight shell process is interrupted immediately instead of
|
|
1381
|
+
* holding Steer/Pause behind its requested yield or natural exit.
|
|
1382
|
+
*/
|
|
1383
|
+
turnCancellationSignal?: AbortSignal;
|
|
1384
|
+
/**
|
|
1385
|
+
* Receives the physical sandbox-tool fence at agent construction time. A
|
|
1386
|
+
* cancelled/fenced worker attempt must await it before publishing its durable
|
|
1387
|
+
* attempt-quiesced receipt.
|
|
1388
|
+
*/
|
|
1389
|
+
onToolCancellationFence?: (fence: TurnToolCancellationFence) => void;
|
|
1042
1390
|
};
|
|
1043
1391
|
|
|
1044
1392
|
export type PackSkillFile = {
|
|
@@ -1070,7 +1418,6 @@ export type WorkspaceEnvironmentContext = {
|
|
|
1070
1418
|
|
|
1071
1419
|
export type PersistentSessionSettings = {
|
|
1072
1420
|
titleIsSet: boolean;
|
|
1073
|
-
childNotificationsMode?: "digest" | "passive";
|
|
1074
1421
|
};
|
|
1075
1422
|
|
|
1076
1423
|
/**
|
|
@@ -1167,6 +1514,12 @@ export function appendSessionInstructions(composed: string, sessionInstructions?
|
|
|
1167
1514
|
return trimmed ? `${composed} ${trimmed}` : composed;
|
|
1168
1515
|
}
|
|
1169
1516
|
|
|
1517
|
+
/** Append system instructions that apply to this exact turn only. */
|
|
1518
|
+
export function appendTurnInstructions(composed: string, turnInstructions?: string): string {
|
|
1519
|
+
const trimmed = turnInstructions?.trim();
|
|
1520
|
+
return trimmed ? `${composed} ${trimmed}` : composed;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1170
1523
|
/**
|
|
1171
1524
|
* Append the durable session metadata the model otherwise cannot observe.
|
|
1172
1525
|
* This is deliberately declarative and excludes the title text: the title is
|
|
@@ -1181,13 +1534,7 @@ export function appendPersistentSessionSettings(
|
|
|
1181
1534
|
if (!settings) {
|
|
1182
1535
|
return composed;
|
|
1183
1536
|
}
|
|
1184
|
-
|
|
1185
|
-
? `; spawned-worker completion notifications are ${settings.childNotificationsMode}`
|
|
1186
|
-
: "";
|
|
1187
|
-
const persistentTools = settings.childNotificationsMode
|
|
1188
|
-
? "opengeni__set_session_title or opengeni__set_child_notifications_mode"
|
|
1189
|
-
: "opengeni__set_session_title";
|
|
1190
|
-
return `${composed} Persistent session settings already in effect: the display title is ${settings.titleIsSet ? "set" : "not set"}${childState}. These settings persist across turns, continuations, and interruptions. Do not call ${persistentTools} merely to reassert an unchanged value; call them only when the current value actually needs to change.`;
|
|
1537
|
+
return `${composed} Persistent session settings already in effect: the display title is ${settings.titleIsSet ? "set" : "not set"}. This setting persists across turns, continuations, and interruptions. Do not call opengeni__set_session_title merely to reassert an unchanged value; call it only when the current value actually needs to change.`;
|
|
1191
1538
|
}
|
|
1192
1539
|
|
|
1193
1540
|
/**
|
|
@@ -1241,7 +1588,9 @@ const agentRepositoryCloneHooks = new WeakMap<object, SandboxLifecycleHook[]>();
|
|
|
1241
1588
|
// provided-session env; runStream reads them to build the clone hook context.
|
|
1242
1589
|
// Absent when no brokered repo is attached / on the selfhosted path.
|
|
1243
1590
|
const agentGitTokenSeeds = new WeakMap<object, GitTokenSeeds>();
|
|
1591
|
+
const agentGitCredentialBindings = new WeakMap<object, GitCredentialBindingSeed[]>();
|
|
1244
1592
|
const agentToolspaceTokenSeed = new WeakMap<object, string>();
|
|
1593
|
+
const agentToolspaceTokenSessionId = new WeakMap<object, string>();
|
|
1245
1594
|
// A genesis directive is consumed by runAgentStream exactly once for the
|
|
1246
1595
|
// freshly-built agent. It must not remain in Agent.instructions: those
|
|
1247
1596
|
// instructions are presented again on every internal model/tool loop.
|
|
@@ -1310,6 +1659,9 @@ export function buildOpenGeniAgent(
|
|
|
1310
1659
|
resources: ResourceRef[],
|
|
1311
1660
|
options: BuildAgentOptions = {},
|
|
1312
1661
|
): Agent<any, any> {
|
|
1662
|
+
if (Boolean(options.toolspaceTokenSeed) !== Boolean(options.toolspaceTokenSessionId)) {
|
|
1663
|
+
throw new Error("toolspaceTokenSeed and toolspaceTokenSessionId must be supplied together");
|
|
1664
|
+
}
|
|
1313
1665
|
// Resolved per-turn gating. Each override defaults to today's settings-derived
|
|
1314
1666
|
// behaviour, so the legacy global-client callers (no resolved model) build the
|
|
1315
1667
|
// exact same agent as before; the multi-provider worker path passes the
|
|
@@ -1331,6 +1683,28 @@ export function buildOpenGeniAgent(
|
|
|
1331
1683
|
// [...agent.tools, ...capability.tools()]), so hosted web_search coexists with
|
|
1332
1684
|
// both rather than overriding them.
|
|
1333
1685
|
const hostedTools = hostedWebSearch ? [webSearchTool()] : [];
|
|
1686
|
+
const humanInputTool = agentTool({
|
|
1687
|
+
name: HUMAN_INPUT_TOOL_NAME,
|
|
1688
|
+
description:
|
|
1689
|
+
"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.",
|
|
1690
|
+
parameters: RequestHumanInputToolInput,
|
|
1691
|
+
needsApproval: true,
|
|
1692
|
+
// A missing/mismatched durable response is a protocol integrity failure,
|
|
1693
|
+
// not model-visible tool output the agent may reason past.
|
|
1694
|
+
errorFunction: null,
|
|
1695
|
+
execute: (_input, _context, details) => {
|
|
1696
|
+
const settled = options.humanInputResponse;
|
|
1697
|
+
if (!settled) {
|
|
1698
|
+
throw new Error("Human-input tool resumed without a durable response");
|
|
1699
|
+
}
|
|
1700
|
+
const resumedCallId = details?.toolCall?.callId;
|
|
1701
|
+
if (resumedCallId && resumedCallId !== settled.toolCallId) {
|
|
1702
|
+
throw new Error("Human-input response does not belong to the resumed tool call");
|
|
1703
|
+
}
|
|
1704
|
+
return JSON.stringify({ requestId: settled.requestId, ...settled.response });
|
|
1705
|
+
},
|
|
1706
|
+
});
|
|
1707
|
+
const agentTools = [...hostedTools, humanInputTool];
|
|
1334
1708
|
const baseConfig = {
|
|
1335
1709
|
name: "OpenGeni Agent",
|
|
1336
1710
|
model: options.model ?? settings.openaiModel,
|
|
@@ -1353,24 +1727,28 @@ export function buildOpenGeniAgent(
|
|
|
1353
1727
|
// and the worker resolved a nonblank block — appendWorkspaceMemory,
|
|
1354
1728
|
// 4. + the per-session persona instructions (session-specific, so it
|
|
1355
1729
|
// refines both the workspace persona and the substrate note),
|
|
1356
|
-
// 5. +
|
|
1730
|
+
// 5. + host context for this exact turn, when supplied,
|
|
1731
|
+
// 6. + durable session-setting state (title present + child notification
|
|
1357
1732
|
// mode), when supplied by the worker,
|
|
1358
1733
|
// The genesis title directive is deliberately NOT part of this persistent
|
|
1359
1734
|
// string. runAgentStream injects it into the first model call only.
|
|
1360
1735
|
instructions: appendPersistentSessionSettings(
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1736
|
+
appendTurnInstructions(
|
|
1737
|
+
appendSessionInstructions(
|
|
1738
|
+
appendWorkspaceMemory(
|
|
1739
|
+
appendToolspaceInstructions(
|
|
1740
|
+
composeAgentInstructions(
|
|
1741
|
+
options.instructionsTemplate ?? settings.agentInstructionsTemplate,
|
|
1742
|
+
options.workspaceEnvironment,
|
|
1743
|
+
options.rig,
|
|
1744
|
+
),
|
|
1745
|
+
settings.toolspaceEnabled && Boolean(options.toolspaceTokenSeed),
|
|
1368
1746
|
),
|
|
1369
|
-
|
|
1747
|
+
options.workspaceMemory,
|
|
1370
1748
|
),
|
|
1371
|
-
options.
|
|
1749
|
+
options.sessionInstructions,
|
|
1372
1750
|
),
|
|
1373
|
-
options.
|
|
1751
|
+
options.turnInstructions,
|
|
1374
1752
|
),
|
|
1375
1753
|
options.persistentSessionSettings,
|
|
1376
1754
|
),
|
|
@@ -1393,7 +1771,7 @@ export function buildOpenGeniAgent(
|
|
|
1393
1771
|
// `new Agent(baseConfig)` path (sandboxBackend === "none") and the
|
|
1394
1772
|
// `new SandboxAgent({ ...baseConfig, ... })` path via the shared baseConfig
|
|
1395
1773
|
// spread; the SDK concatenates these with MCP and sandbox capability tools.
|
|
1396
|
-
|
|
1774
|
+
tools: agentTools,
|
|
1397
1775
|
...(options.mcpServers?.length ? { mcpServers: options.mcpServers } : {}),
|
|
1398
1776
|
// Surface FAILED MCP tool calls as `{ isError: true }` tool output (see
|
|
1399
1777
|
// mcpToolErrorFunction / mcpToolErrorOutput) instead of the SDK's default
|
|
@@ -1425,6 +1803,9 @@ export function buildOpenGeniAgent(
|
|
|
1425
1803
|
),
|
|
1426
1804
|
...(runAs ? { runAs } : {}),
|
|
1427
1805
|
capabilities: buildAgentCapabilities(settings, options.packSkills ?? [], {
|
|
1806
|
+
...(options.skillLibrarySkills?.length
|
|
1807
|
+
? { skillLibrarySkills: options.skillLibrarySkills }
|
|
1808
|
+
: {}),
|
|
1428
1809
|
...(options.structuredToolTransport !== undefined
|
|
1429
1810
|
? { structuredToolTransport: options.structuredToolTransport }
|
|
1430
1811
|
: {}),
|
|
@@ -1432,8 +1813,24 @@ export function buildOpenGeniAgent(
|
|
|
1432
1813
|
? { computerToolMode: options.computerToolMode }
|
|
1433
1814
|
: {}),
|
|
1434
1815
|
...(options.onComputerUseReady ? { onComputerUseReady: options.onComputerUseReady } : {}),
|
|
1816
|
+
...(options.turnCancellationSignal
|
|
1817
|
+
? { turnCancellationSignal: options.turnCancellationSignal }
|
|
1818
|
+
: {}),
|
|
1819
|
+
...(options.onToolCancellationFence
|
|
1820
|
+
? { onToolCancellationFence: options.onToolCancellationFence }
|
|
1821
|
+
: {}),
|
|
1435
1822
|
}),
|
|
1436
1823
|
});
|
|
1824
|
+
agentSkillSelections.set(
|
|
1825
|
+
agent,
|
|
1826
|
+
Object.freeze(
|
|
1827
|
+
effectiveSkillSelections(
|
|
1828
|
+
options.skillLibrarySelections ?? [],
|
|
1829
|
+
options.skillLibrarySkills ?? [],
|
|
1830
|
+
options.packSkills ?? [],
|
|
1831
|
+
).map((selection) => Object.freeze(selection)),
|
|
1832
|
+
),
|
|
1833
|
+
);
|
|
1437
1834
|
if (options.genesisTitleHint) {
|
|
1438
1835
|
agentsNeedingGenesisTitleDirective.add(agent);
|
|
1439
1836
|
}
|
|
@@ -1465,8 +1862,12 @@ export function buildOpenGeniAgent(
|
|
|
1465
1862
|
if (Object.keys(gitTokenSeeds).length > 0) {
|
|
1466
1863
|
agentGitTokenSeeds.set(agent, gitTokenSeeds);
|
|
1467
1864
|
}
|
|
1865
|
+
if (options.gitCredentialBindings && options.gitCredentialBindings.length > 0) {
|
|
1866
|
+
agentGitCredentialBindings.set(agent, options.gitCredentialBindings);
|
|
1867
|
+
}
|
|
1468
1868
|
if (options.toolspaceTokenSeed) {
|
|
1469
1869
|
agentToolspaceTokenSeed.set(agent, options.toolspaceTokenSeed);
|
|
1870
|
+
agentToolspaceTokenSessionId.set(agent, options.toolspaceTokenSessionId!);
|
|
1470
1871
|
}
|
|
1471
1872
|
// M3: stash the rig setup descriptor + RESOLVE the rig credential hooks now.
|
|
1472
1873
|
// sandboxLifecycleHooksForIds throws on an unknown hook name, so a typo'd rig
|
|
@@ -1511,7 +1912,10 @@ function mcpToolRequiresApproval(policy: boolean | string[], unprefixedName: str
|
|
|
1511
1912
|
}
|
|
1512
1913
|
|
|
1513
1914
|
/** A per-server approval policy keyed by the server's `<id>__` tool prefix. */
|
|
1514
|
-
type McpApprovalPolicy = {
|
|
1915
|
+
type McpApprovalPolicy = {
|
|
1916
|
+
prefix: string;
|
|
1917
|
+
requireApproval: boolean | string[];
|
|
1918
|
+
};
|
|
1515
1919
|
|
|
1516
1920
|
/** The subset of the agent surface the approval wrap needs — including `clone`. */
|
|
1517
1921
|
type ApprovalCapableAgent = {
|
|
@@ -1720,6 +2124,7 @@ export function buildAgentCapabilities(
|
|
|
1720
2124
|
settings: Settings,
|
|
1721
2125
|
packSkills: PackSkill[],
|
|
1722
2126
|
options: {
|
|
2127
|
+
skillLibrarySkills?: PackSkill[];
|
|
1723
2128
|
structuredToolTransport?: boolean;
|
|
1724
2129
|
// EXPLICIT computer-use transport (see BuildAgentOptions.computerToolMode). When
|
|
1725
2130
|
// present, computerUse() is handed the mode directly and its tools() obeys it
|
|
@@ -1727,8 +2132,15 @@ export function buildAgentCapabilities(
|
|
|
1727
2132
|
// imageFunctionResults path (driven by structuredToolTransport) is unchanged.
|
|
1728
2133
|
computerToolMode?: ComputerToolMode;
|
|
1729
2134
|
onComputerUseReady?: (session: SandboxSessionLike) => Promise<void>;
|
|
2135
|
+
turnCancellationSignal?: AbortSignal;
|
|
2136
|
+
onToolCancellationFence?: (fence: TurnToolCancellationFence) => void;
|
|
1730
2137
|
} = {},
|
|
1731
2138
|
): ReturnType<typeof Capabilities.default> {
|
|
2139
|
+
const toolCancellation =
|
|
2140
|
+
options.turnCancellationSignal || options.onToolCancellationFence
|
|
2141
|
+
? createTurnToolCancellationController(options.turnCancellationSignal)
|
|
2142
|
+
: null;
|
|
2143
|
+
if (toolCancellation) options.onToolCancellationFence?.(toolCancellation);
|
|
1732
2144
|
// The `filesystem()` capability picks hosted-vs-function tool variants from the
|
|
1733
2145
|
// bound model instance (supportsApplyPatchTransport / structured tool output).
|
|
1734
2146
|
// When the caller declares the backend does NOT support that structured/hosted
|
|
@@ -1747,9 +2159,13 @@ export function buildAgentCapabilities(
|
|
|
1747
2159
|
}
|
|
1748
2160
|
const caps: ReturnType<typeof Capabilities.default> = [
|
|
1749
2161
|
filesystemCapability,
|
|
1750
|
-
shell({ configureTools: withExecOpCorrelation }),
|
|
2162
|
+
shell({ ...(toolCancellation ? {} : { configureTools: withExecOpCorrelation }) }),
|
|
1751
2163
|
];
|
|
1752
|
-
caps.push(
|
|
2164
|
+
caps.push(
|
|
2165
|
+
skills({
|
|
2166
|
+
lazyFrom: lazySkillSourceWithPackSkills(packSkills, options.skillLibrarySkills ?? []),
|
|
2167
|
+
}),
|
|
2168
|
+
);
|
|
1753
2169
|
// P4.3 computer-use: the agent drives the SAME :0 humans watch (xdotool/XTEST +
|
|
1754
2170
|
// scrot), but only when the desktop tier is ON, computer-use is enabled, and the
|
|
1755
2171
|
// backend is one whose image carries the X stack (descriptorgate — honest about
|
|
@@ -1801,6 +2217,14 @@ export function buildAgentCapabilities(
|
|
|
1801
2217
|
}
|
|
1802
2218
|
caps.push(computerCapability as unknown as ReturnType<typeof Capabilities.default>[number]);
|
|
1803
2219
|
}
|
|
2220
|
+
if (toolCancellation) {
|
|
2221
|
+
for (const capability of caps) {
|
|
2222
|
+
wrapCapabilityToolsForTurnCancellation(
|
|
2223
|
+
capability as unknown as { tools(): Tool<unknown>[] },
|
|
2224
|
+
toolCancellation,
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
1804
2228
|
return caps;
|
|
1805
2229
|
}
|
|
1806
2230
|
|
|
@@ -1829,6 +2253,9 @@ export type PrepareToolsOptions = {
|
|
|
1829
2253
|
// The calling turn's id, signed into the token so tools can classify the
|
|
1830
2254
|
// caller from its own identity instead of the session's live active pointer.
|
|
1831
2255
|
turnId?: string;
|
|
2256
|
+
// The exact executing attempt that owns the MCP call.
|
|
2257
|
+
attemptId?: string;
|
|
2258
|
+
executionGeneration?: number;
|
|
1832
2259
|
subjectId?: string;
|
|
1833
2260
|
subjectLabel?: string;
|
|
1834
2261
|
// Overrides the fixed first-party MCP permission set for this session's
|
|
@@ -2019,9 +2446,13 @@ function connectionBrokerFetch(
|
|
|
2019
2446
|
status: "auth_needed",
|
|
2020
2447
|
reason: "expired",
|
|
2021
2448
|
providerDomain: connectionRef.providerDomain,
|
|
2449
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2022
2450
|
connectionId: refreshed.connectionId,
|
|
2023
2451
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2024
2452
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2453
|
+
...(connectionRef.selectedResources
|
|
2454
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2455
|
+
: {}),
|
|
2025
2456
|
},
|
|
2026
2457
|
connectionRef,
|
|
2027
2458
|
);
|
|
@@ -2050,9 +2481,13 @@ async function resolveConnectionForRequest(
|
|
|
2050
2481
|
status: "auth_needed",
|
|
2051
2482
|
reason: "missing_connection",
|
|
2052
2483
|
providerDomain: connectionRef.providerDomain,
|
|
2484
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2053
2485
|
...(connectionRef.connectionId ? { connectionId: connectionRef.connectionId } : {}),
|
|
2054
2486
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2055
2487
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2488
|
+
...(connectionRef.selectedResources
|
|
2489
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2490
|
+
: {}),
|
|
2056
2491
|
};
|
|
2057
2492
|
}
|
|
2058
2493
|
const request: ResolveConnectionCredentialInput = {
|
|
@@ -2070,9 +2505,13 @@ async function resolveConnectionForRequest(
|
|
|
2070
2505
|
status: "auth_needed",
|
|
2071
2506
|
reason: "refresh_failed",
|
|
2072
2507
|
providerDomain: connectionRef.providerDomain,
|
|
2508
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2073
2509
|
...(connectionRef.connectionId ? { connectionId: connectionRef.connectionId } : {}),
|
|
2074
2510
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2075
2511
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2512
|
+
...(connectionRef.selectedResources
|
|
2513
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2514
|
+
: {}),
|
|
2076
2515
|
};
|
|
2077
2516
|
}
|
|
2078
2517
|
}
|
|
@@ -2090,6 +2529,7 @@ function insufficientScopeAuth(
|
|
|
2090
2529
|
status: "auth_needed",
|
|
2091
2530
|
reason: "insufficient_scope",
|
|
2092
2531
|
providerDomain: connectionRef.providerDomain,
|
|
2532
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2093
2533
|
connectionId,
|
|
2094
2534
|
...(challenge.scope?.length
|
|
2095
2535
|
? { scopes: challenge.scope }
|
|
@@ -2101,6 +2541,9 @@ function insufficientScopeAuth(
|
|
|
2101
2541
|
: connectionRef.resource
|
|
2102
2542
|
? { resource: connectionRef.resource }
|
|
2103
2543
|
: {}),
|
|
2544
|
+
...(connectionRef.selectedResources
|
|
2545
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2546
|
+
: {}),
|
|
2104
2547
|
};
|
|
2105
2548
|
}
|
|
2106
2549
|
|
|
@@ -2116,6 +2559,11 @@ async function authNeededFetchResponse(
|
|
|
2116
2559
|
serverId,
|
|
2117
2560
|
toolName: request.toolName ?? null,
|
|
2118
2561
|
providerDomain: auth.providerDomain,
|
|
2562
|
+
...(auth.provider
|
|
2563
|
+
? { provider: auth.provider }
|
|
2564
|
+
: connectionRef.provider
|
|
2565
|
+
? { provider: connectionRef.provider }
|
|
2566
|
+
: {}),
|
|
2119
2567
|
reason: auth.reason,
|
|
2120
2568
|
...(connectionId ? { connectionId } : {}),
|
|
2121
2569
|
...(auth.scopes
|
|
@@ -2128,13 +2576,20 @@ async function authNeededFetchResponse(
|
|
|
2128
2576
|
: connectionRef.resource
|
|
2129
2577
|
? { resource: connectionRef.resource }
|
|
2130
2578
|
: {}),
|
|
2579
|
+
...(auth.selectedResources
|
|
2580
|
+
? { selectedResources: auth.selectedResources }
|
|
2581
|
+
: connectionRef.selectedResources
|
|
2582
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2583
|
+
: {}),
|
|
2131
2584
|
...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
|
|
2132
2585
|
...(options.subjectId ? { subjectId: options.subjectId } : {}),
|
|
2133
2586
|
});
|
|
2134
2587
|
if (request.method === "tools/call") {
|
|
2135
2588
|
return mcpToolAuthNeededResponse(request.id);
|
|
2136
2589
|
}
|
|
2137
|
-
return new Response("Authentication required for MCP server connection", {
|
|
2590
|
+
return new Response("Authentication required for MCP server connection", {
|
|
2591
|
+
status: 401,
|
|
2592
|
+
});
|
|
2138
2593
|
}
|
|
2139
2594
|
|
|
2140
2595
|
async function publishAuthNeeded(
|
|
@@ -2295,7 +2750,10 @@ function mcpToolUnavailableMessage(reason: string): string {
|
|
|
2295
2750
|
// transport error carries the raw response BODY in its `.message` (a broker
|
|
2296
2751
|
// 401/403 body can echo request detail), so `.message` is never included; the
|
|
2297
2752
|
// numeric `.code`/`.status` (e.g. 401) is safe and useful.
|
|
2298
|
-
function safeMcpErrorFields(error: unknown): {
|
|
2753
|
+
function safeMcpErrorFields(error: unknown): {
|
|
2754
|
+
errorClass: string;
|
|
2755
|
+
status?: number;
|
|
2756
|
+
} {
|
|
2299
2757
|
const errorClass = error instanceof Error ? error.constructor.name : typeof error;
|
|
2300
2758
|
const raw = (error as { code?: unknown; status?: unknown } | null)?.code;
|
|
2301
2759
|
const status = typeof raw === "number" ? raw : undefined;
|
|
@@ -2383,6 +2841,8 @@ async function signFirstPartyDelegatedBearer(
|
|
|
2383
2841
|
permissions: options.firstPartyPermissions ?? firstPartyMcpPermissions,
|
|
2384
2842
|
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
2385
2843
|
...(options.turnId ? { turnId: options.turnId } : {}),
|
|
2844
|
+
...(options.attemptId ? { attemptId: options.attemptId } : {}),
|
|
2845
|
+
...(options.executionGeneration ? { executionGeneration: options.executionGeneration } : {}),
|
|
2386
2846
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
2387
2847
|
});
|
|
2388
2848
|
}
|
|
@@ -2577,6 +3037,7 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2577
3037
|
// doesn't spam the log when the SDK re-lists across model turns.
|
|
2578
3038
|
private readonly bestEffort: boolean;
|
|
2579
3039
|
private loggedListToolsFailure = false;
|
|
3040
|
+
private listedToolSchemaTokens = 0;
|
|
2580
3041
|
|
|
2581
3042
|
constructor(
|
|
2582
3043
|
private readonly inner: MCPServer,
|
|
@@ -2633,9 +3094,19 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2633
3094
|
}
|
|
2634
3095
|
return [];
|
|
2635
3096
|
}
|
|
2636
|
-
|
|
3097
|
+
const exposed = tools
|
|
2637
3098
|
.filter((tool) => this.isAllowed(tool.name))
|
|
2638
|
-
.map((tool) => ({
|
|
3099
|
+
.map((tool) => ({
|
|
3100
|
+
...tool,
|
|
3101
|
+
name: prefixedMcpToolName(this.name, tool.name),
|
|
3102
|
+
}));
|
|
3103
|
+
this.listedToolSchemaTokens = estimateSerializedValueTokens(exposed);
|
|
3104
|
+
return exposed;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/** Latest exact tools/list projection used to build the model request. */
|
|
3108
|
+
modelToolSchemaTokens(): number {
|
|
3109
|
+
return this.listedToolSchemaTokens;
|
|
2639
3110
|
}
|
|
2640
3111
|
|
|
2641
3112
|
async callTool(
|
|
@@ -2657,7 +3128,10 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2657
3128
|
// This applies to ANY server — an auth-needed is recoverable once the user
|
|
2658
3129
|
// re-links, so even a required tool degrades gracefully here.
|
|
2659
3130
|
if (isAuthNeededMcpError(error)) {
|
|
2660
|
-
return {
|
|
3131
|
+
return {
|
|
3132
|
+
isError: true,
|
|
3133
|
+
content: [{ type: "text", text: MCP_AUTH_NEEDED_MESSAGE }],
|
|
3134
|
+
};
|
|
2661
3135
|
}
|
|
2662
3136
|
// Best-effort INVOCATION isolation (sibling to the listTools guard). When
|
|
2663
3137
|
// the model calls a best-effort server's tool and the call throws for ANY
|
|
@@ -2679,7 +3153,12 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2679
3153
|
);
|
|
2680
3154
|
return {
|
|
2681
3155
|
isError: true,
|
|
2682
|
-
content: [
|
|
3156
|
+
content: [
|
|
3157
|
+
{
|
|
3158
|
+
type: "text",
|
|
3159
|
+
text: mcpToolUnavailableMessage(mcpErrorReason(fields)),
|
|
3160
|
+
},
|
|
3161
|
+
],
|
|
2683
3162
|
};
|
|
2684
3163
|
}
|
|
2685
3164
|
throw error;
|
|
@@ -2791,22 +3270,25 @@ export async function prepareRunInput(
|
|
|
2791
3270
|
...(sandboxSessionState ? { sandboxSessionState } : {}),
|
|
2792
3271
|
};
|
|
2793
3272
|
}
|
|
2794
|
-
// An
|
|
3273
|
+
// An interrupted tool can only be resumed against a real saved run state. If the
|
|
2795
3274
|
// latest blob is the cleared sentinel the awaiting turn was wiped (the API
|
|
2796
3275
|
// refuses clear in requires_action, so this is a defensive guard) — fail with
|
|
2797
3276
|
// an honest message instead of the cryptic SDK "missing schema version".
|
|
2798
3277
|
if (isClearedRunStateBlob(input.serializedRunState)) {
|
|
2799
3278
|
throw new Error(
|
|
2800
|
-
"Cannot resume an
|
|
3279
|
+
"Cannot resume an interrupted tool: the session context was cleared, so the awaiting run state no longer exists.",
|
|
2801
3280
|
);
|
|
2802
3281
|
}
|
|
2803
3282
|
const state = await RunState.fromString(agent, input.serializedRunState);
|
|
2804
3283
|
const interruptions = state.getInterruptions();
|
|
2805
|
-
const
|
|
3284
|
+
const interruptionId = input.kind === "human_input" ? input.toolCallId : input.approvalId;
|
|
3285
|
+
const target = interruptions.find((item: any) => approvalIdentifier(item) === interruptionId);
|
|
2806
3286
|
if (!target) {
|
|
2807
|
-
throw new Error(`
|
|
3287
|
+
throw new Error(`Interrupted tool not found in saved run state: ${interruptionId}`);
|
|
2808
3288
|
}
|
|
2809
|
-
if (input.
|
|
3289
|
+
if (input.kind === "human_input") {
|
|
3290
|
+
state.approve(target as any);
|
|
3291
|
+
} else if (input.decision === "approve") {
|
|
2810
3292
|
state.approve(target as any);
|
|
2811
3293
|
} else {
|
|
2812
3294
|
state.reject(target as any, input.message ? { message: input.message } : undefined);
|
|
@@ -2815,16 +3297,26 @@ export async function prepareRunInput(
|
|
|
2815
3297
|
}
|
|
2816
3298
|
|
|
2817
3299
|
export type RunAgentStreamOptions = {
|
|
3300
|
+
/** Abort the provider/tool loop when the owning activity is cancelled. */
|
|
3301
|
+
signal?: AbortSignal;
|
|
2818
3302
|
sandboxClient?: unknown;
|
|
2819
3303
|
sandboxEnvironment?: Record<string, string>;
|
|
2820
3304
|
onRuntimeEvent?: (event: NormalizedRuntimeEvent) => Promise<void> | void;
|
|
2821
|
-
|
|
3305
|
+
contextCompactionSignal?: () => ProviderContextTokenSignal | null | undefined;
|
|
2822
3306
|
contextCompactionRequested?: () => boolean | Promise<boolean>;
|
|
2823
3307
|
// Host-managed git credential renewal registration. Called only after the
|
|
2824
3308
|
// initial token-file seed completed on a real provisioned box. The worker
|
|
2825
3309
|
// owns the multi-day timer and uses this pinned, un-proxied session to
|
|
2826
3310
|
// atomically replace token files; runtime never mints credentials itself.
|
|
2827
3311
|
onGitCredentialSessionReady?: (session: GitCredentialTokenWriterSession) => Promise<void> | void;
|
|
3312
|
+
// OpenGeni-minted Toolspace token renewal registration. Called only after the
|
|
3313
|
+
// initial token file reached the real sandbox session.
|
|
3314
|
+
onToolspaceTokenSessionReady?: (session: ToolspaceTokenWriterSession) => Promise<void> | void;
|
|
3315
|
+
// Host-owned run material is seeded off-manifest before setup and every
|
|
3316
|
+
// agent-created process sources the active immutable generation. The worker
|
|
3317
|
+
// owns resolution/renewal/fencing; runtime owns sandbox transport.
|
|
3318
|
+
runCredentialSessionId?: string;
|
|
3319
|
+
onRunCredentialSessionReady?: RunCredentialSessionReady;
|
|
2828
3320
|
// OWNERSHIP INVERSION (P1.2): an externally-owned, already-live sandbox
|
|
2829
3321
|
// session resolved by the per-turn resume-by-id path. When present,
|
|
2830
3322
|
// runAgentStream does NOT build (or resume, or discard) a client — it threads
|
|
@@ -2855,6 +3347,13 @@ export type RunAgentStreamOptions = {
|
|
|
2855
3347
|
// after establish, so runAgentStream must not run it eagerly here.
|
|
2856
3348
|
deferredSetup?: boolean;
|
|
2857
3349
|
};
|
|
3350
|
+
/**
|
|
3351
|
+
* The attempt's authoritative physical sandbox-operation fence. Platform
|
|
3352
|
+
* lifecycle commands use its command runner too, so Steer/Pause can interrupt
|
|
3353
|
+
* repository clone, rig setup, file materialization, and credential seeding
|
|
3354
|
+
* before the attempt-quiesced receipt opens queue admission.
|
|
3355
|
+
*/
|
|
3356
|
+
turnToolCancellationFence?: TurnToolCancellationFence;
|
|
2858
3357
|
// A per-turn model-input filter chained AFTER the provider-item-id strip.
|
|
2859
3358
|
// Used by the genesis-title injection to prepend a hidden, NON-PERSISTED
|
|
2860
3359
|
// directive: a callModelInputFilter mutates only `modelData.input` for each
|
|
@@ -2864,7 +3363,7 @@ export type RunAgentStreamOptions = {
|
|
|
2864
3363
|
};
|
|
2865
3364
|
|
|
2866
3365
|
export type ContextRobustnessFilterOptions = {
|
|
2867
|
-
|
|
3366
|
+
contextCompactionSignal?: () => ProviderContextTokenSignal | null | undefined;
|
|
2868
3367
|
contextCompactionRequested?: () => boolean | Promise<boolean>;
|
|
2869
3368
|
throwOnCompactionNeeded?: boolean;
|
|
2870
3369
|
};
|
|
@@ -2910,9 +3409,10 @@ function takeGenesisTitleInputFilter(agent: Agent<any, any>): CallModelInputFilt
|
|
|
2910
3409
|
// Generic substrate prompting for programmatic tool calling (toolspace). Same
|
|
2911
3410
|
// text for every host; gated per-turn by appendToolspaceInstructions on the
|
|
2912
3411
|
// presence of a minted toolspace token, so it only appears when the sandbox
|
|
2913
|
-
//
|
|
3412
|
+
// exposes $OPENGENI_TOOLSPACE_URL/_TOKEN_FILE. Stock images carry ogtool;
|
|
3413
|
+
// custom environments get only an exact deployment-pinned bootstrap hint.
|
|
2914
3414
|
export const TOOLSPACE_PROGRAMMATIC_DIRECTIVE =
|
|
2915
|
-
"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.
|
|
3415
|
+
"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.";
|
|
2916
3416
|
|
|
2917
3417
|
/**
|
|
2918
3418
|
* callModelInputFilter that removes provider-assigned item ids (rs_/msg_/fc_…)
|
|
@@ -2971,24 +3471,89 @@ export const elideSupersededViewImagesFilter: CallModelInputFilter = ({ modelDat
|
|
|
2971
3471
|
) as unknown as AgentInputItem[],
|
|
2972
3472
|
});
|
|
2973
3473
|
|
|
3474
|
+
/**
|
|
3475
|
+
* Canonical Codex-style tool-result bound at the final model-input seam. The
|
|
3476
|
+
* identical pure normalizer also runs before conversation rows are persisted,
|
|
3477
|
+
* so this is a live-turn defense rather than a request-only alternate history.
|
|
3478
|
+
*/
|
|
3479
|
+
export function boundModelToolOutputsFilterForSettings(settings: Settings): CallModelInputFilter {
|
|
3480
|
+
return ({ modelData }) => ({
|
|
3481
|
+
...modelData,
|
|
3482
|
+
input: boundModelToolOutputItems(
|
|
3483
|
+
modelData.input as unknown as Array<Record<string, unknown>>,
|
|
3484
|
+
settings.modelToolOutputTruncationTokens,
|
|
3485
|
+
) as unknown as AgentInputItem[],
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
function estimateAgentToolSchemaTokens(agent: Agent<any, any>): number {
|
|
3490
|
+
const localTools = Array.isArray((agent as { tools?: unknown }).tools)
|
|
3491
|
+
? ((agent as { tools: unknown[] }).tools ?? [])
|
|
3492
|
+
: [];
|
|
3493
|
+
const localDescriptors = localTools.map((candidate) => {
|
|
3494
|
+
if (!candidate || typeof candidate !== "object") return candidate;
|
|
3495
|
+
const tool = candidate as Record<string, unknown>;
|
|
3496
|
+
return {
|
|
3497
|
+
type: tool.type,
|
|
3498
|
+
name: tool.name,
|
|
3499
|
+
description: tool.description,
|
|
3500
|
+
parameters: tool.parameters,
|
|
3501
|
+
inputSchema: tool.inputSchema,
|
|
3502
|
+
strict: tool.strict,
|
|
3503
|
+
providerData: tool.providerData,
|
|
3504
|
+
};
|
|
3505
|
+
});
|
|
3506
|
+
const mcpServers = Array.isArray((agent as { mcpServers?: unknown }).mcpServers)
|
|
3507
|
+
? ((agent as { mcpServers: unknown[] }).mcpServers ?? [])
|
|
3508
|
+
: [];
|
|
3509
|
+
const mcpTokens = mcpServers.reduce<number>((total, server) => {
|
|
3510
|
+
const getter = (server as { modelToolSchemaTokens?: () => number } | null)
|
|
3511
|
+
?.modelToolSchemaTokens;
|
|
3512
|
+
return total + (typeof getter === "function" ? getter.call(server) : 0);
|
|
3513
|
+
}, 0);
|
|
3514
|
+
return estimateSerializedValueTokens(localDescriptors) + mcpTokens;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
2974
3517
|
export function contextRobustnessFilterForSettings(
|
|
2975
3518
|
settings: Settings,
|
|
2976
3519
|
options: ContextRobustnessFilterOptions = {},
|
|
2977
3520
|
): CallModelInputFilter {
|
|
2978
3521
|
const thresholdTokens = compactionThresholdTokens(settings);
|
|
2979
|
-
|
|
3522
|
+
let previousRequest: { revision: number; footprint: CompleteModelInputFootprint } | null = null;
|
|
3523
|
+
let requestRevision = 0;
|
|
3524
|
+
return async ({ modelData, agent }) => {
|
|
2980
3525
|
const input = modelData.input;
|
|
2981
3526
|
if (options.throwOnCompactionNeeded) {
|
|
2982
|
-
const reported = options.
|
|
2983
|
-
const
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
:
|
|
3527
|
+
const reported = options.contextCompactionSignal?.();
|
|
3528
|
+
const current: CompleteModelInputFootprint = {
|
|
3529
|
+
input: input as unknown as Array<Record<string, unknown>>,
|
|
3530
|
+
instructionsTokens: estimateSerializedValueTokens(modelData.instructions ?? ""),
|
|
3531
|
+
toolSchemaTokens: estimateAgentToolSchemaTokens(agent),
|
|
3532
|
+
};
|
|
3533
|
+
// Stream consumption can lag the SDK's background model loop. A provider
|
|
3534
|
+
// usage signal is safe only when its response revision belongs to the
|
|
3535
|
+
// immediately preceding request. Never attach a delayed response count to
|
|
3536
|
+
// a newer footprint: doing so can hide all model output produced between
|
|
3537
|
+
// them and recreate an under-counting compaction loop.
|
|
3538
|
+
const boundProvider =
|
|
3539
|
+
reported &&
|
|
3540
|
+
previousRequest &&
|
|
3541
|
+
reported.revision === previousRequest.revision &&
|
|
3542
|
+
hasModelGeneratedItem(current.input)
|
|
3543
|
+
? reported
|
|
3544
|
+
: null;
|
|
3545
|
+
const estimate = estimateCompleteModelInput({
|
|
3546
|
+
current,
|
|
3547
|
+
provider: boundProvider,
|
|
3548
|
+
providerRequestFootprint: boundProvider ? (previousRequest?.footprint ?? null) : null,
|
|
3549
|
+
});
|
|
3550
|
+
const signalTokens = estimate.tokens;
|
|
3551
|
+
previousRequest = { revision: ++requestRevision, footprint: current };
|
|
2987
3552
|
if (await options.contextCompactionRequested?.()) {
|
|
2988
3553
|
throw new CompactionNeededError({
|
|
2989
3554
|
signalTokens,
|
|
2990
3555
|
thresholdTokens,
|
|
2991
|
-
signalSource:
|
|
3556
|
+
signalSource: boundProvider ? "provider" : "estimate",
|
|
2992
3557
|
trigger: "operator",
|
|
2993
3558
|
});
|
|
2994
3559
|
}
|
|
@@ -2996,7 +3561,7 @@ export function contextRobustnessFilterForSettings(
|
|
|
2996
3561
|
throw new CompactionNeededError({
|
|
2997
3562
|
signalTokens,
|
|
2998
3563
|
thresholdTokens,
|
|
2999
|
-
signalSource:
|
|
3564
|
+
signalSource: boundProvider ? "provider" : "estimate",
|
|
3000
3565
|
});
|
|
3001
3566
|
}
|
|
3002
3567
|
}
|
|
@@ -3033,6 +3598,7 @@ export function callModelInputFilterForSettings(
|
|
|
3033
3598
|
const filters: CallModelInputFilter[] = [
|
|
3034
3599
|
normalizeComputerCallsFilter,
|
|
3035
3600
|
elideSupersededViewImagesFilter,
|
|
3601
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
3036
3602
|
];
|
|
3037
3603
|
if (settings.openaiProviderItemIds === "strip") {
|
|
3038
3604
|
filters.push(stripProviderItemIdsFilter);
|
|
@@ -3050,7 +3616,11 @@ export async function runAgentStream(
|
|
|
3050
3616
|
const prepared: PreparedAgentInput =
|
|
3051
3617
|
typeof input === "string" || input instanceof RunState ? { input } : input;
|
|
3052
3618
|
const environment = overrides.sandboxEnvironment ?? collectSandboxEnvironment(settings);
|
|
3619
|
+
const toolspaceTokenFile = toolspaceTokenFileForAgent(agent, environment);
|
|
3053
3620
|
const genesisTitleInputFilter = takeGenesisTitleInputFilter(agent);
|
|
3621
|
+
if (overrides.onRunCredentialSessionReady && !overrides.runCredentialSessionId) {
|
|
3622
|
+
throw new Error("runCredentialSessionId is required when run credential setup is enabled");
|
|
3623
|
+
}
|
|
3054
3624
|
|
|
3055
3625
|
// OWNED PATH (P1.2 ownership inversion): the per-turn resume path injected a
|
|
3056
3626
|
// live, externally-owned box. We thread the live `session` straight into
|
|
@@ -3069,11 +3639,24 @@ export async function runAgentStream(
|
|
|
3069
3639
|
// whose per-op pointer re-read could land these execs on a machine swapped in
|
|
3070
3640
|
// mid-turn.
|
|
3071
3641
|
const setupSession = (overrides.ownedSandbox.setupSession ?? session) as SandboxSessionLike;
|
|
3642
|
+
const credentialAgentSession = overrides.runCredentialSessionId
|
|
3643
|
+
? withRunCredentialsSession(session as SandboxSessionLike, overrides.runCredentialSessionId)
|
|
3644
|
+
: (session as SandboxSessionLike);
|
|
3645
|
+
const agentSession = toolspaceTokenFile
|
|
3646
|
+
? withToolspaceTokenSession(credentialAgentSession, toolspaceTokenFile)
|
|
3647
|
+
: credentialAgentSession;
|
|
3648
|
+
const credentialSetupSession = overrides.runCredentialSessionId
|
|
3649
|
+
? withRunCredentialsSession(setupSession, overrides.runCredentialSessionId)
|
|
3650
|
+
: setupSession;
|
|
3651
|
+
const decoratedSetupSession = toolspaceTokenFile
|
|
3652
|
+
? withToolspaceTokenSession(credentialSetupSession, toolspaceTokenFile)
|
|
3653
|
+
: credentialSetupSession;
|
|
3072
3654
|
// Platform setup (manifest-env pin + beforeAgentStart hooks + file downloads)
|
|
3073
3655
|
// against the UN-proxied established box — the ONE-TRUTH helper shared with the
|
|
3074
3656
|
// lazy provisioner. Eager path: runs here, before the run starts (unchanged).
|
|
3075
3657
|
if (!overrides.ownedSandbox.deferredSetup) {
|
|
3076
|
-
await
|
|
3658
|
+
await overrides.onRunCredentialSessionReady?.(session as SandboxSessionLike);
|
|
3659
|
+
await runOwnedSandboxSetup(agent, session as SandboxSessionLike, decoratedSetupSession, {
|
|
3077
3660
|
settings,
|
|
3078
3661
|
environment,
|
|
3079
3662
|
preparedInput: prepared,
|
|
@@ -3081,7 +3664,17 @@ export async function runAgentStream(
|
|
|
3081
3664
|
? { fileDownloadsMaterialized: true }
|
|
3082
3665
|
: {}),
|
|
3083
3666
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3667
|
+
...(overrides.turnToolCancellationFence
|
|
3668
|
+
? {
|
|
3669
|
+
commandRunner: overrides.turnToolCancellationFence.runSandboxCommand.bind(
|
|
3670
|
+
overrides.turnToolCancellationFence,
|
|
3671
|
+
),
|
|
3672
|
+
}
|
|
3673
|
+
: {}),
|
|
3084
3674
|
});
|
|
3675
|
+
if (toolspaceTokenSeedForAgent(agent)) {
|
|
3676
|
+
await overrides.onToolspaceTokenSessionReady?.(agentSession);
|
|
3677
|
+
}
|
|
3085
3678
|
await overrides.onGitCredentialSessionReady?.(setupSession);
|
|
3086
3679
|
}
|
|
3087
3680
|
const runAs = sandboxRunAs(settings);
|
|
@@ -3091,11 +3684,19 @@ export async function runAgentStream(
|
|
|
3091
3684
|
? withSandboxFileDownloads(ownedClient as SandboxClient, fileDownloads, {
|
|
3092
3685
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3093
3686
|
...(runAs ? { runAs } : {}),
|
|
3687
|
+
...(overrides.turnToolCancellationFence
|
|
3688
|
+
? {
|
|
3689
|
+
commandRunner: overrides.turnToolCancellationFence.runSandboxCommand.bind(
|
|
3690
|
+
overrides.turnToolCancellationFence,
|
|
3691
|
+
),
|
|
3692
|
+
}
|
|
3693
|
+
: {}),
|
|
3094
3694
|
})
|
|
3095
3695
|
: (ownedClient as SandboxClient);
|
|
3096
3696
|
// TOKEN-BROKER (B1): the per-turn git token seed, forwarded OFF-MANIFEST so the
|
|
3097
3697
|
// repository-clone hook seeds it to the box's token file before the clone.
|
|
3098
3698
|
const ownedGitTokenSeeds = gitTokenSeedsForAgent(agent);
|
|
3699
|
+
const ownedGitCredentialBindings = gitCredentialBindingsForAgent(agent);
|
|
3099
3700
|
const ownedToolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
|
|
3100
3701
|
const ownedRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3101
3702
|
const ownedHooks = [
|
|
@@ -3108,6 +3709,9 @@ export async function runAgentStream(
|
|
|
3108
3709
|
rigCredentialHooksForAgent(agent),
|
|
3109
3710
|
),
|
|
3110
3711
|
...sandboxToolspaceTokenHooksForAgent(agent),
|
|
3712
|
+
...toolspaceTokenSessionRegistrationHooks(
|
|
3713
|
+
ownedToolspaceTokenSeed ? overrides.onToolspaceTokenSessionReady : undefined,
|
|
3714
|
+
),
|
|
3111
3715
|
...sandboxRepositoryCloneHooksForAgent(agent),
|
|
3112
3716
|
...gitCredentialSessionRegistrationHooks(overrides.onGitCredentialSessionReady),
|
|
3113
3717
|
];
|
|
@@ -3116,37 +3720,60 @@ export async function runAgentStream(
|
|
|
3116
3720
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3117
3721
|
...(runAs ? { runAs } : {}),
|
|
3118
3722
|
...(ownedGitTokenSeeds ? { gitTokenSeeds: ownedGitTokenSeeds } : {}),
|
|
3723
|
+
...(ownedGitCredentialBindings ? { gitCredentialBindings: ownedGitCredentialBindings } : {}),
|
|
3119
3724
|
...(ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {}),
|
|
3725
|
+
...(toolspaceTokenFile ? { toolspaceTokenFile } : {}),
|
|
3120
3726
|
...(ownedRigSetup ? { rigSetup: ownedRigSetup } : {}),
|
|
3121
3727
|
};
|
|
3122
|
-
// Keep
|
|
3123
|
-
//
|
|
3124
|
-
|
|
3728
|
+
// Keep both credential seeding and lifecycle decoration as a safety net for
|
|
3729
|
+
// any session the SDK does create/resume during this run. They are inert for
|
|
3730
|
+
// the provided session, which remains the normal ownership-inverted path.
|
|
3731
|
+
const credentialResourceClient = overrides.runCredentialSessionId
|
|
3732
|
+
? withRunCredentialsClient(
|
|
3733
|
+
resourceClient,
|
|
3734
|
+
overrides.runCredentialSessionId,
|
|
3735
|
+
overrides.onRunCredentialSessionReady,
|
|
3736
|
+
)
|
|
3737
|
+
: resourceClient;
|
|
3738
|
+
const toolspaceResourceClient = toolspaceTokenFile
|
|
3739
|
+
? withToolspaceTokenClient(credentialResourceClient, toolspaceTokenFile)
|
|
3740
|
+
: credentialResourceClient;
|
|
3741
|
+
const decoratedClient = withSandboxLifecycleHooks(
|
|
3742
|
+
toolspaceResourceClient,
|
|
3743
|
+
ownedHooks,
|
|
3744
|
+
ownedHookContext,
|
|
3745
|
+
);
|
|
3125
3746
|
const ownedFilter = composeCallModelInputFilters(
|
|
3126
3747
|
[
|
|
3127
|
-
callModelInputFilterForSettings(settings,
|
|
3748
|
+
callModelInputFilterForSettings(settings),
|
|
3749
|
+
genesisTitleInputFilter,
|
|
3750
|
+
overrides.callModelInputFilter,
|
|
3751
|
+
// A caller filter may synthesize model input. Re-apply the idempotent
|
|
3752
|
+
// canonical bound at the literal final seam before accounting/provider
|
|
3753
|
+
// serialization so no extension can bypass the policy.
|
|
3754
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
3755
|
+
contextRobustnessFilterForSettings(settings, {
|
|
3128
3756
|
throwOnCompactionNeeded: Boolean(
|
|
3129
|
-
overrides.
|
|
3757
|
+
overrides.contextCompactionSignal || overrides.contextCompactionRequested,
|
|
3130
3758
|
),
|
|
3131
|
-
...(overrides.
|
|
3132
|
-
? {
|
|
3759
|
+
...(overrides.contextCompactionSignal
|
|
3760
|
+
? { contextCompactionSignal: overrides.contextCompactionSignal }
|
|
3133
3761
|
: {}),
|
|
3134
3762
|
...(overrides.contextCompactionRequested
|
|
3135
3763
|
? { contextCompactionRequested: overrides.contextCompactionRequested }
|
|
3136
3764
|
: {}),
|
|
3137
3765
|
}),
|
|
3138
|
-
genesisTitleInputFilter,
|
|
3139
|
-
overrides.callModelInputFilter,
|
|
3140
3766
|
].filter((f): f is CallModelInputFilter => Boolean(f)),
|
|
3141
3767
|
);
|
|
3142
3768
|
const ownedRunOptions: Parameters<typeof run>[2] = {
|
|
3143
3769
|
stream: true,
|
|
3144
3770
|
maxTurns: settings.agentMaxModelCallsPerTurn,
|
|
3145
3771
|
callModelInputFilter: ownedFilter,
|
|
3772
|
+
...(overrides.signal ? { signal: overrides.signal } : {}),
|
|
3146
3773
|
};
|
|
3147
3774
|
ownedRunOptions.sandbox = {
|
|
3148
3775
|
client: decoratedClient,
|
|
3149
|
-
session,
|
|
3776
|
+
session: agentSession,
|
|
3150
3777
|
...(sessionState ? { sessionState } : {}),
|
|
3151
3778
|
} as SandboxRunConfig;
|
|
3152
3779
|
return await runScopedRunner(settings).run(agent, prepared.input, ownedRunOptions);
|
|
@@ -3171,14 +3798,27 @@ export async function runAgentStream(
|
|
|
3171
3798
|
...(runAs ? { runAs } : {}),
|
|
3172
3799
|
})
|
|
3173
3800
|
: refreshedClient;
|
|
3801
|
+
const credentialClient =
|
|
3802
|
+
resourceClient && overrides.runCredentialSessionId
|
|
3803
|
+
? withRunCredentialsClient(
|
|
3804
|
+
resourceClient,
|
|
3805
|
+
overrides.runCredentialSessionId,
|
|
3806
|
+
overrides.onRunCredentialSessionReady,
|
|
3807
|
+
)
|
|
3808
|
+
: resourceClient;
|
|
3809
|
+
const toolspaceClient =
|
|
3810
|
+
credentialClient && toolspaceTokenFile
|
|
3811
|
+
? withToolspaceTokenClient(credentialClient, toolspaceTokenFile)
|
|
3812
|
+
: credentialClient;
|
|
3174
3813
|
// TOKEN-BROKER (B1): the per-turn git token seed, forwarded OFF-MANIFEST so the
|
|
3175
3814
|
// repository-clone hook seeds it to the box's token file before the clone.
|
|
3176
3815
|
const gitTokenSeeds = gitTokenSeedsForAgent(agent);
|
|
3816
|
+
const gitCredentialBindings = gitCredentialBindingsForAgent(agent);
|
|
3177
3817
|
const toolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
|
|
3178
3818
|
const legacyRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3179
|
-
const client =
|
|
3819
|
+
const client = toolspaceClient
|
|
3180
3820
|
? withSandboxLifecycleHooks(
|
|
3181
|
-
|
|
3821
|
+
toolspaceClient,
|
|
3182
3822
|
[
|
|
3183
3823
|
// M3: same rig-setup-first ordering + credential-hook union as the owned
|
|
3184
3824
|
// path (this legacy create/resume decoration path is byte-for-byte today
|
|
@@ -3189,6 +3829,9 @@ export async function runAgentStream(
|
|
|
3189
3829
|
rigCredentialHooksForAgent(agent),
|
|
3190
3830
|
),
|
|
3191
3831
|
...sandboxToolspaceTokenHooksForAgent(agent),
|
|
3832
|
+
...toolspaceTokenSessionRegistrationHooks(
|
|
3833
|
+
toolspaceTokenSeed ? overrides.onToolspaceTokenSessionReady : undefined,
|
|
3834
|
+
),
|
|
3192
3835
|
...sandboxRepositoryCloneHooksForAgent(agent),
|
|
3193
3836
|
...gitCredentialSessionRegistrationHooks(overrides.onGitCredentialSessionReady),
|
|
3194
3837
|
],
|
|
@@ -3197,7 +3840,9 @@ export async function runAgentStream(
|
|
|
3197
3840
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3198
3841
|
...(runAs ? { runAs } : {}),
|
|
3199
3842
|
...(gitTokenSeeds ? { gitTokenSeeds } : {}),
|
|
3843
|
+
...(gitCredentialBindings ? { gitCredentialBindings } : {}),
|
|
3200
3844
|
...(toolspaceTokenSeed ? { toolspaceTokenSeed } : {}),
|
|
3845
|
+
...(toolspaceTokenFile ? { toolspaceTokenFile } : {}),
|
|
3201
3846
|
...(legacyRigSetup ? { rigSetup: legacyRigSetup } : {}),
|
|
3202
3847
|
},
|
|
3203
3848
|
)
|
|
@@ -3210,19 +3855,21 @@ export async function runAgentStream(
|
|
|
3210
3855
|
// OpenGeni's durable conversation truth is still reconciled explicitly below.
|
|
3211
3856
|
const callModelInputFilter = composeCallModelInputFilters(
|
|
3212
3857
|
[
|
|
3213
|
-
callModelInputFilterForSettings(settings,
|
|
3858
|
+
callModelInputFilterForSettings(settings),
|
|
3859
|
+
genesisTitleInputFilter,
|
|
3860
|
+
overrides.callModelInputFilter,
|
|
3861
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
3862
|
+
contextRobustnessFilterForSettings(settings, {
|
|
3214
3863
|
throwOnCompactionNeeded: Boolean(
|
|
3215
|
-
overrides.
|
|
3864
|
+
overrides.contextCompactionSignal || overrides.contextCompactionRequested,
|
|
3216
3865
|
),
|
|
3217
|
-
...(overrides.
|
|
3218
|
-
? {
|
|
3866
|
+
...(overrides.contextCompactionSignal
|
|
3867
|
+
? { contextCompactionSignal: overrides.contextCompactionSignal }
|
|
3219
3868
|
: {}),
|
|
3220
3869
|
...(overrides.contextCompactionRequested
|
|
3221
3870
|
? { contextCompactionRequested: overrides.contextCompactionRequested }
|
|
3222
3871
|
: {}),
|
|
3223
3872
|
}),
|
|
3224
|
-
genesisTitleInputFilter,
|
|
3225
|
-
overrides.callModelInputFilter,
|
|
3226
3873
|
].filter((f): f is CallModelInputFilter => Boolean(f)),
|
|
3227
3874
|
);
|
|
3228
3875
|
const runOptions: Parameters<typeof run>[2] = {
|
|
@@ -3233,6 +3880,7 @@ export async function runAgentStream(
|
|
|
3233
3880
|
// raise the proactive compaction signal. This runs for turn-start replay AND
|
|
3234
3881
|
// every mid-turn follow-up.
|
|
3235
3882
|
callModelInputFilter,
|
|
3883
|
+
...(overrides.signal ? { signal: overrides.signal } : {}),
|
|
3236
3884
|
};
|
|
3237
3885
|
if (client) {
|
|
3238
3886
|
runOptions.sandbox = {
|
|
@@ -3282,7 +3930,9 @@ function appendSandboxFileDownloadFailureNote(
|
|
|
3282
3930
|
* default runner. setDefaultModelProvider remains only as a boot-time fallback.
|
|
3283
3931
|
*/
|
|
3284
3932
|
function runScopedRunner(settings: Settings): Runner {
|
|
3285
|
-
return new Runner({
|
|
3933
|
+
return new Runner({
|
|
3934
|
+
modelProvider: new MultiProviderModelProvider(settings),
|
|
3935
|
+
});
|
|
3286
3936
|
}
|
|
3287
3937
|
|
|
3288
3938
|
export { MaxTurnsExceededError } from "@openai/agents";
|
|
@@ -3338,7 +3988,9 @@ export function withManifestRefreshOnResume(
|
|
|
3338
3988
|
? { supportsDefaultOptions: client.supportsDefaultOptions }
|
|
3339
3989
|
: {}),
|
|
3340
3990
|
...(client.create
|
|
3341
|
-
? {
|
|
3991
|
+
? {
|
|
3992
|
+
create: async (...args: any[]) => await (client.create as any)(...args),
|
|
3993
|
+
}
|
|
3342
3994
|
: {}),
|
|
3343
3995
|
resume: async (state: SandboxSessionState) => {
|
|
3344
3996
|
const session = await client.resume!(state);
|
|
@@ -3346,7 +3998,9 @@ export function withManifestRefreshOnResume(
|
|
|
3346
3998
|
return session;
|
|
3347
3999
|
},
|
|
3348
4000
|
...(client.delete
|
|
3349
|
-
? {
|
|
4001
|
+
? {
|
|
4002
|
+
delete: async (state: SandboxSessionState) => await client.delete!(state),
|
|
4003
|
+
}
|
|
3350
4004
|
: {}),
|
|
3351
4005
|
...(client.serializeSessionState
|
|
3352
4006
|
? {
|
|
@@ -3395,7 +4049,11 @@ export async function applyMissingManifestEntries(
|
|
|
3395
4049
|
state?: {
|
|
3396
4050
|
manifest?:
|
|
3397
4051
|
| Manifest
|
|
3398
|
-
| {
|
|
4052
|
+
| {
|
|
4053
|
+
root?: string;
|
|
4054
|
+
entries?: Record<string, any>;
|
|
4055
|
+
environment?: Record<string, any>;
|
|
4056
|
+
};
|
|
3399
4057
|
};
|
|
3400
4058
|
}
|
|
3401
4059
|
).state?.manifest;
|
|
@@ -3505,7 +4163,11 @@ export function manifestEnvironmentDrift(
|
|
|
3505
4163
|
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
3506
4164
|
return null;
|
|
3507
4165
|
}
|
|
3508
|
-
return {
|
|
4166
|
+
return {
|
|
4167
|
+
added: added.sort(),
|
|
4168
|
+
removed: removed.sort(),
|
|
4169
|
+
changed: changed.sort(),
|
|
4170
|
+
};
|
|
3509
4171
|
}
|
|
3510
4172
|
|
|
3511
4173
|
async function reportManifestEnvironmentDrift(
|
|
@@ -3520,7 +4182,10 @@ async function reportManifestEnvironmentDrift(
|
|
|
3520
4182
|
// Reporting must never break a resume: the drift itself is benign under the
|
|
3521
4183
|
// env pin (the box keeps running on its baked env); only the SIGNAL matters.
|
|
3522
4184
|
try {
|
|
3523
|
-
await context.onRuntimeEvent?.({
|
|
4185
|
+
await context.onRuntimeEvent?.({
|
|
4186
|
+
type: "sandbox.env.drift",
|
|
4187
|
+
payload: drift,
|
|
4188
|
+
});
|
|
3524
4189
|
} catch {
|
|
3525
4190
|
// Swallow: a failed emit must not fail the turn.
|
|
3526
4191
|
}
|
|
@@ -3549,7 +4214,11 @@ export async function pinProvidedSessionManifestEnvironment(
|
|
|
3549
4214
|
state?: {
|
|
3550
4215
|
manifest?:
|
|
3551
4216
|
| Manifest
|
|
3552
|
-
| {
|
|
4217
|
+
| {
|
|
4218
|
+
root?: string;
|
|
4219
|
+
entries?: Record<string, any>;
|
|
4220
|
+
environment?: Record<string, any>;
|
|
4221
|
+
};
|
|
3553
4222
|
};
|
|
3554
4223
|
}
|
|
3555
4224
|
).state?.manifest;
|
|
@@ -3603,6 +4272,9 @@ export async function runOwnedSandboxSetup(
|
|
|
3603
4272
|
onRuntimeEvent?: SandboxLifecycleHookContext["onRuntimeEvent"];
|
|
3604
4273
|
gitTokenSeedsOverride?: GitTokenSeeds;
|
|
3605
4274
|
gitTokenSeedOverride?: string;
|
|
4275
|
+
gitCredentialBindingsOverride?: GitCredentialBindingSeed[];
|
|
4276
|
+
toolspaceTokenSeedOverride?: string;
|
|
4277
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
3606
4278
|
},
|
|
3607
4279
|
): Promise<void> {
|
|
3608
4280
|
const { settings, environment } = opts;
|
|
@@ -3624,7 +4296,11 @@ export async function runOwnedSandboxSetup(
|
|
|
3624
4296
|
...(opts.gitTokenSeedOverride ? { github: opts.gitTokenSeedOverride } : {}),
|
|
3625
4297
|
...(opts.gitTokenSeedsOverride ?? {}),
|
|
3626
4298
|
} satisfies GitTokenSeeds;
|
|
3627
|
-
const
|
|
4299
|
+
const ownedGitCredentialBindings =
|
|
4300
|
+
opts.gitCredentialBindingsOverride ?? gitCredentialBindingsForAgent(agent);
|
|
4301
|
+
const ownedToolspaceTokenSeed =
|
|
4302
|
+
opts.toolspaceTokenSeedOverride ?? toolspaceTokenSeedForAgent(agent);
|
|
4303
|
+
const ownedToolspaceTokenFile = toolspaceTokenFileForAgent(agent, environment);
|
|
3628
4304
|
const ownedRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3629
4305
|
const ownedHooks = [
|
|
3630
4306
|
// M3: rig setup runs FIRST so any tooling it installs is present for the
|
|
@@ -3646,8 +4322,11 @@ export async function runOwnedSandboxSetup(
|
|
|
3646
4322
|
...(opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {}),
|
|
3647
4323
|
...(runAs ? { runAs } : {}),
|
|
3648
4324
|
...(Object.keys(ownedGitTokenSeeds).length > 0 ? { gitTokenSeeds: ownedGitTokenSeeds } : {}),
|
|
4325
|
+
...(ownedGitCredentialBindings ? { gitCredentialBindings: ownedGitCredentialBindings } : {}),
|
|
3649
4326
|
...(ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {}),
|
|
4327
|
+
...(ownedToolspaceTokenFile ? { toolspaceTokenFile: ownedToolspaceTokenFile } : {}),
|
|
3650
4328
|
...(ownedRigSetup ? { rigSetup: ownedRigSetup } : {}),
|
|
4329
|
+
...(opts.commandRunner ? { commandRunner: opts.commandRunner } : {}),
|
|
3651
4330
|
};
|
|
3652
4331
|
// OWNED-PATH HOOKS: run the beforeAgentStart hooks directly against the provided
|
|
3653
4332
|
// box, once per turn, BEFORE the run starts (repository-clone hook seeds the B1
|
|
@@ -3667,6 +4346,7 @@ export async function runOwnedSandboxSetup(
|
|
|
3667
4346
|
const materialized = await materializeSandboxFileDownloads(setupSession, fileDownloads, {
|
|
3668
4347
|
...(opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {}),
|
|
3669
4348
|
...(runAs ? { runAs } : {}),
|
|
4349
|
+
...(opts.commandRunner ? { commandRunner: opts.commandRunner } : {}),
|
|
3670
4350
|
});
|
|
3671
4351
|
if (opts.preparedInput) {
|
|
3672
4352
|
appendSandboxFileDownloadFailureNote(opts.preparedInput, materialized.failures);
|
|
@@ -3700,7 +4380,7 @@ function mergePathGrants(
|
|
|
3700
4380
|
export function withSandboxFileDownloads(
|
|
3701
4381
|
client: SandboxClient,
|
|
3702
4382
|
downloads: SandboxFileDownload[],
|
|
3703
|
-
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs"> = {},
|
|
4383
|
+
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs" | "commandRunner"> = {},
|
|
3704
4384
|
): SandboxClient {
|
|
3705
4385
|
const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
|
|
3706
4386
|
if (normalizedDownloads.length === 0) {
|
|
@@ -3732,7 +4412,9 @@ export function withSandboxFileDownloads(
|
|
|
3732
4412
|
}
|
|
3733
4413
|
: {}),
|
|
3734
4414
|
...(client.delete
|
|
3735
|
-
? {
|
|
4415
|
+
? {
|
|
4416
|
+
delete: async (state: SandboxSessionState) => await client.delete!(state),
|
|
4417
|
+
}
|
|
3736
4418
|
: {}),
|
|
3737
4419
|
...(client.serializeSessionState
|
|
3738
4420
|
? {
|
|
@@ -3764,7 +4446,7 @@ export function withSandboxFileDownloads(
|
|
|
3764
4446
|
export async function materializeSandboxFileDownloads(
|
|
3765
4447
|
session: SandboxSessionLike,
|
|
3766
4448
|
downloads: SandboxFileDownload[],
|
|
3767
|
-
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs"> = {},
|
|
4449
|
+
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs" | "commandRunner"> = {},
|
|
3768
4450
|
): Promise<SandboxFileDownloadMaterializationResult> {
|
|
3769
4451
|
const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
|
|
3770
4452
|
if (normalizedDownloads.length === 0) {
|
|
@@ -3802,27 +4484,24 @@ export async function materializeSandboxFileDownloads(
|
|
|
3802
4484
|
}
|
|
3803
4485
|
let result: unknown;
|
|
3804
4486
|
try {
|
|
3805
|
-
result =
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
3817
|
-
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
3818
|
-
maxOutputTokens: 20_000,
|
|
3819
|
-
});
|
|
4487
|
+
result = await runSandboxLifecycleCommand(
|
|
4488
|
+
session,
|
|
4489
|
+
{
|
|
4490
|
+
cmd: sandboxFileDownloadCommand(download, targetPath),
|
|
4491
|
+
workdir: "/workspace",
|
|
4492
|
+
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
4493
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
4494
|
+
maxOutputTokens: 20_000,
|
|
4495
|
+
},
|
|
4496
|
+
context.commandRunner,
|
|
4497
|
+
);
|
|
3820
4498
|
assertSandboxCommandSucceeded(result, `Sandbox file resource download ${download.fileId}`);
|
|
3821
4499
|
await context.onRuntimeEvent?.({
|
|
3822
4500
|
type: "sandbox.operation.completed",
|
|
3823
4501
|
payload: { name: "file-resource-download", ...payload },
|
|
3824
4502
|
});
|
|
3825
4503
|
} catch (error) {
|
|
4504
|
+
if (error instanceof TurnSandboxCommandCancelledError) throw error;
|
|
3826
4505
|
const failure = sandboxFileDownloadFailure(download, targetPath, error, result);
|
|
3827
4506
|
failures.push(failure);
|
|
3828
4507
|
await context.onRuntimeEvent?.({
|
|
@@ -3899,82 +4578,87 @@ function ensureManifest(
|
|
|
3899
4578
|
});
|
|
3900
4579
|
}
|
|
3901
4580
|
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
4581
|
+
function base64DecodedByteLength(value: string): number {
|
|
4582
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
4583
|
+
return Math.max(0, Math.floor((value.length * 3) / 4) - padding);
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
/** Determine image byte length without allocating another binary/base64 copy. */
|
|
4587
|
+
function imageDataByteLength(data: unknown): number | null {
|
|
4588
|
+
if (ArrayBuffer.isView(data)) return data.byteLength;
|
|
4589
|
+
if (data instanceof ArrayBuffer) return data.byteLength;
|
|
3910
4590
|
if (Array.isArray(data)) {
|
|
3911
|
-
return data.every((
|
|
4591
|
+
return data.every((value) => typeof value === "number") ? data.length : null;
|
|
3912
4592
|
}
|
|
3913
|
-
if (data
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
}
|
|
4593
|
+
if (!data || typeof data !== "object") return null;
|
|
4594
|
+
const record = data as Record<string, unknown>;
|
|
4595
|
+
if (record.type === "Buffer" && Array.isArray(record.data)) {
|
|
4596
|
+
return record.data.every((value) => typeof value === "number") ? record.data.length : null;
|
|
3918
4597
|
}
|
|
3919
|
-
|
|
4598
|
+
const keys = Object.keys(record);
|
|
4599
|
+
return keys.length > 0 &&
|
|
4600
|
+
keys.every((key) => /^\d+$/.test(key) && typeof record[key] === "number")
|
|
4601
|
+
? keys.length
|
|
4602
|
+
: null;
|
|
3920
4603
|
}
|
|
3921
4604
|
|
|
3922
|
-
/**
|
|
3923
|
-
*
|
|
3924
|
-
*
|
|
3925
|
-
*
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
if (
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
4605
|
+
/**
|
|
4606
|
+
* Convert one image-shaped tool result into a content-free audit fact. This is
|
|
4607
|
+
* intentionally different from model history: the model keeps its structured
|
|
4608
|
+
* image item, while `session_events` never becomes an implicit image blob store.
|
|
4609
|
+
*/
|
|
4610
|
+
function toolOutputMediaPreview(value: unknown): SessionEventMediaPreview | null {
|
|
4611
|
+
if (typeof value === "string") {
|
|
4612
|
+
return sessionEventMediaPreviewFromDataUrl(value);
|
|
4613
|
+
}
|
|
4614
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4615
|
+
const record = value as Record<string, unknown>;
|
|
4616
|
+
if (record.type === "input_image") {
|
|
4617
|
+
const source = record.image ?? record.image_url ?? record.imageUrl;
|
|
4618
|
+
const url =
|
|
4619
|
+
typeof source === "string"
|
|
4620
|
+
? source
|
|
4621
|
+
: source && typeof source === "object"
|
|
4622
|
+
? (source as Record<string, unknown>).url
|
|
4623
|
+
: null;
|
|
4624
|
+
if (typeof url !== "string" || url.length === 0) return null;
|
|
4625
|
+
return sessionEventMediaPreviewFromDataUrl(url) ?? sessionEventMediaPreview("image/*", null);
|
|
4626
|
+
}
|
|
4627
|
+
if (record.type !== "image" || !record.image || typeof record.image !== "object") return null;
|
|
4628
|
+
const image = record.image as Record<string, unknown>;
|
|
3942
4629
|
const mediaType =
|
|
3943
4630
|
typeof image.mediaType === "string" && image.mediaType.length > 0
|
|
3944
4631
|
? image.mediaType
|
|
3945
4632
|
: "image/png";
|
|
4633
|
+
if (typeof image.url === "string" && image.url.length > 0) {
|
|
4634
|
+
return (
|
|
4635
|
+
sessionEventMediaPreviewFromDataUrl(image.url) ?? sessionEventMediaPreview(mediaType, null)
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
3946
4638
|
if (typeof image.data === "string") {
|
|
3947
|
-
return
|
|
4639
|
+
return (
|
|
4640
|
+
sessionEventMediaPreviewFromDataUrl(image.data) ??
|
|
4641
|
+
sessionEventMediaPreview(mediaType, base64DecodedByteLength(image.data))
|
|
4642
|
+
);
|
|
3948
4643
|
}
|
|
3949
|
-
const
|
|
3950
|
-
return
|
|
4644
|
+
const byteLength = imageDataByteLength(image.data);
|
|
4645
|
+
return byteLength === null ? null : sessionEventMediaPreview(mediaType, byteLength);
|
|
3951
4646
|
}
|
|
3952
4647
|
|
|
3953
4648
|
/**
|
|
3954
|
-
*
|
|
3955
|
-
*
|
|
3956
|
-
*
|
|
3957
|
-
*
|
|
3958
|
-
* per screenshot in session_events — ~10x the base64 form). This mirrors the desktop
|
|
3959
|
-
* screenshot to the SAME compact `data:<mediaType>;base64,…` STRING the HOSTED
|
|
3960
|
-
* `computer_call` event already carries (agents-core sets its output to that data-URL),
|
|
3961
|
-
* so both computer-use transports emit one representation. The full data-URL is kept
|
|
3962
|
-
* (not truncated) because the web timeline RENDERS the screenshot from this event
|
|
3963
|
-
* payload — packages/react/src/timeline/tool-renderers.tsx ComputerCallRenderer
|
|
3964
|
-
* (`out.startsWith("data:image")` → <ScreenshotFigure src={out}/>) and ViewImageRenderer.
|
|
3965
|
-
* Non-image outputs (text strings, MCP `{isError,content}` objects, hosted computer_call
|
|
3966
|
-
* data-URL strings) pass through unchanged.
|
|
4649
|
+
* Normalize a tool-call output for the lossy `agent.toolCall.output` audit event.
|
|
4650
|
+
* Inline image bytes/data URLs become a compact `media_preview` with exact byte
|
|
4651
|
+
* length where knowable and `fullOutputAvailable:false`. The model-facing output
|
|
4652
|
+
* is not changed here, and mixed arrays retain their non-image text/error facts.
|
|
3967
4653
|
*/
|
|
3968
4654
|
export function normalizeToolOutputForEvent(output: unknown): unknown {
|
|
3969
|
-
const single =
|
|
4655
|
+
const single = toolOutputMediaPreview(output);
|
|
3970
4656
|
if (single !== null) {
|
|
3971
4657
|
return single;
|
|
3972
4658
|
}
|
|
3973
4659
|
if (Array.isArray(output)) {
|
|
3974
|
-
const normalized = output.map((el) =>
|
|
3975
|
-
|
|
3976
|
-
// image renderers expect; a mixed/multi array keeps its (now-compact) shape.
|
|
3977
|
-
if (normalized.length === 1 && typeof normalized[0] === "string") {
|
|
4660
|
+
const normalized = output.map((el) => toolOutputMediaPreview(el) ?? el);
|
|
4661
|
+
if (normalized.length === 1 && normalized[0]?.type === "media_preview") {
|
|
3978
4662
|
return normalized[0];
|
|
3979
4663
|
}
|
|
3980
4664
|
return normalized;
|
|
@@ -4002,7 +4686,10 @@ export function normalizeSdkEvent(event: RunStreamEvent): NormalizedRuntimeEvent
|
|
|
4002
4686
|
return out;
|
|
4003
4687
|
}
|
|
4004
4688
|
if (event.type === "agent_updated_stream_event") {
|
|
4005
|
-
out.push({
|
|
4689
|
+
out.push({
|
|
4690
|
+
type: "agent.updated",
|
|
4691
|
+
payload: { agent: (event as any).agent?.name ?? null },
|
|
4692
|
+
});
|
|
4006
4693
|
return out;
|
|
4007
4694
|
}
|
|
4008
4695
|
if (event.type !== "run_item_stream_event") {
|
|
@@ -4028,8 +4715,8 @@ export function normalizeSdkEvent(event: RunStreamEvent): NormalizedRuntimeEvent
|
|
|
4028
4715
|
type: "agent.toolCall.output",
|
|
4029
4716
|
payload: {
|
|
4030
4717
|
id: item.rawItem?.callId ?? item.id ?? null,
|
|
4031
|
-
//
|
|
4032
|
-
//
|
|
4718
|
+
// Inline media becomes a content-free audit fact. Model history keeps
|
|
4719
|
+
// the provider's real structured image output on its separate path.
|
|
4033
4720
|
output: normalizeToolOutputForEvent(item.output),
|
|
4034
4721
|
},
|
|
4035
4722
|
});
|
|
@@ -4164,7 +4851,9 @@ function inputTokenDetailsProp(raw: Record<string, unknown>): Partial<ModelRespo
|
|
|
4164
4851
|
if (!details || typeof details !== "object") {
|
|
4165
4852
|
return {};
|
|
4166
4853
|
}
|
|
4167
|
-
return {
|
|
4854
|
+
return {
|
|
4855
|
+
inputTokensDetails: details as Record<string, number> | Array<Record<string, number>>,
|
|
4856
|
+
};
|
|
4168
4857
|
}
|
|
4169
4858
|
|
|
4170
4859
|
function outputTokenDetailsProp(
|
|
@@ -4181,17 +4870,45 @@ function outputTokenDetailsProp(
|
|
|
4181
4870
|
}
|
|
4182
4871
|
|
|
4183
4872
|
export function serializeApprovals(interruptions: unknown[]): unknown[] {
|
|
4184
|
-
return interruptions
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4873
|
+
return interruptions
|
|
4874
|
+
.filter((item) => interruptionToolName(item) !== HUMAN_INPUT_TOOL_NAME)
|
|
4875
|
+
.map((item: any) => {
|
|
4876
|
+
if (typeof item?.toJSON === "function") {
|
|
4877
|
+
return item.toJSON();
|
|
4878
|
+
}
|
|
4879
|
+
return {
|
|
4880
|
+
id: approvalIdentifier(item) ?? "approval",
|
|
4881
|
+
name: item?.name ?? item?.rawItem?.name ?? "tool",
|
|
4882
|
+
arguments: item?.arguments ?? item?.rawItem?.arguments ?? null,
|
|
4883
|
+
raw: item,
|
|
4884
|
+
};
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
export function serializeHumanInputRequests(
|
|
4889
|
+
interruptions: unknown[],
|
|
4890
|
+
): SerializedHumanInputInterruption[] {
|
|
4891
|
+
return interruptions
|
|
4892
|
+
.filter((item) => interruptionToolName(item) === HUMAN_INPUT_TOOL_NAME)
|
|
4893
|
+
.map((item: any) => {
|
|
4894
|
+
const rawArguments = item?.arguments ?? item?.rawItem?.arguments;
|
|
4895
|
+
let parsedArguments: unknown = rawArguments;
|
|
4896
|
+
if (typeof rawArguments === "string") {
|
|
4897
|
+
try {
|
|
4898
|
+
parsedArguments = JSON.parse(rawArguments);
|
|
4899
|
+
} catch {
|
|
4900
|
+
throw new Error("Human-input interruption contains invalid JSON arguments");
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
const toolCallId = approvalIdentifier(item);
|
|
4904
|
+
if (!toolCallId) {
|
|
4905
|
+
throw new Error("Human-input interruption is missing a stable tool-call identity");
|
|
4906
|
+
}
|
|
4907
|
+
return {
|
|
4908
|
+
toolCallId,
|
|
4909
|
+
input: RequestHumanInputToolInput.parse(parsedArguments),
|
|
4910
|
+
};
|
|
4911
|
+
});
|
|
4195
4912
|
}
|
|
4196
4913
|
|
|
4197
4914
|
export function buildManifest(
|
|
@@ -4200,6 +4917,7 @@ export function buildManifest(
|
|
|
4200
4917
|
environment = collectSandboxEnvironment(settings),
|
|
4201
4918
|
fileResourceDownloads: SandboxFileDownload[] = [],
|
|
4202
4919
|
): Manifest {
|
|
4920
|
+
assertUniqueResourceMountPaths(resources);
|
|
4203
4921
|
const entries: Record<string, any> = {};
|
|
4204
4922
|
const downloadsByFileId = new Map(
|
|
4205
4923
|
normalizeSandboxFileDownloads(fileResourceDownloads).map((download) => [
|
|
@@ -4210,9 +4928,9 @@ export function buildManifest(
|
|
|
4210
4928
|
for (const resource of resources) {
|
|
4211
4929
|
if (resource.kind === "repository") {
|
|
4212
4930
|
const url = new URL(resource.uri);
|
|
4213
|
-
const host = url.
|
|
4931
|
+
const host = url.host.toLowerCase();
|
|
4214
4932
|
const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
4215
|
-
const mountPath =
|
|
4933
|
+
const mountPath = resourceMountPath(resource);
|
|
4216
4934
|
if (repositoryUsesSandboxClone(settings, resource)) {
|
|
4217
4935
|
entries[mountPath] = dir();
|
|
4218
4936
|
continue;
|
|
@@ -4221,12 +4939,12 @@ export function buildManifest(
|
|
|
4221
4939
|
host,
|
|
4222
4940
|
repo,
|
|
4223
4941
|
ref: resource.ref,
|
|
4224
|
-
...(resource.subpath ? { subpath:
|
|
4942
|
+
...(resource.subpath ? { subpath: normalizeRepositorySubpath(resource.subpath) } : {}),
|
|
4225
4943
|
});
|
|
4226
4944
|
continue;
|
|
4227
4945
|
}
|
|
4228
4946
|
if (resource.kind === "file") {
|
|
4229
|
-
const mountPath =
|
|
4947
|
+
const mountPath = resourceMountPath(resource);
|
|
4230
4948
|
const download = downloadsByFileId.get(resource.fileId);
|
|
4231
4949
|
entries[mountPath] = download
|
|
4232
4950
|
? sandboxDownloadDirectory(download, mountPath)
|
|
@@ -4282,7 +5000,9 @@ function objectStorageFileMount(settings: Settings, prefix: string): any {
|
|
|
4282
5000
|
accountKey: config.accountKey,
|
|
4283
5001
|
endpointUrl: config.endpointUrl,
|
|
4284
5002
|
readOnly: true,
|
|
4285
|
-
mountStrategy: inContainerMountStrategy({
|
|
5003
|
+
mountStrategy: inContainerMountStrategy({
|
|
5004
|
+
pattern: { type: "rclone", mode: "fuse" },
|
|
5005
|
+
}),
|
|
4286
5006
|
});
|
|
4287
5007
|
}
|
|
4288
5008
|
if (settings.objectStorageBackend === "aws-s3" || settings.objectStorageBackend === "gcs") {
|
|
@@ -4384,11 +5104,7 @@ function parseAzureConnectionString(value: string): Record<string, string> {
|
|
|
4384
5104
|
}
|
|
4385
5105
|
|
|
4386
5106
|
function normalizeManifestPath(path: string): string {
|
|
4387
|
-
|
|
4388
|
-
if (!normalized || normalized.includes("..")) {
|
|
4389
|
-
throw new Error(`Invalid sandbox resource path: ${path}`);
|
|
4390
|
-
}
|
|
4391
|
-
return normalized;
|
|
5107
|
+
return normalizeResourceMountPath(path);
|
|
4392
5108
|
}
|
|
4393
5109
|
|
|
4394
5110
|
function normalizeSandboxFileDownloads(downloads: SandboxFileDownload[]): SandboxFileDownload[] {
|
|
@@ -4451,6 +5167,11 @@ function shellQuote(value: string): string {
|
|
|
4451
5167
|
|
|
4452
5168
|
export type SandboxLifecycleHookPhase = "beforeAgentStart";
|
|
4453
5169
|
|
|
5170
|
+
export type SandboxLifecycleCommandRunner = (
|
|
5171
|
+
session: TurnSandboxCommandSession,
|
|
5172
|
+
args: TurnSandboxCommandArgs,
|
|
5173
|
+
) => Promise<unknown>;
|
|
5174
|
+
|
|
4454
5175
|
export type SandboxLifecycleHookContext = {
|
|
4455
5176
|
environment: Record<string, string>;
|
|
4456
5177
|
onRuntimeEvent?: (event: NormalizedRuntimeEvent) => Promise<void> | void;
|
|
@@ -4462,12 +5183,28 @@ export type SandboxLifecycleHookContext = {
|
|
|
4462
5183
|
// NEVER the box/agent manifest env (validateNoEnvironmentDelta must never see
|
|
4463
5184
|
// rotating values).
|
|
4464
5185
|
gitTokenSeeds?: GitTokenSeeds;
|
|
5186
|
+
gitCredentialBindings?: GitCredentialBindingSeed[];
|
|
4465
5187
|
toolspaceTokenSeed?: string;
|
|
5188
|
+
toolspaceTokenFile?: string;
|
|
4466
5189
|
// M3: the rig setup descriptor for the rig-setup hook (the script + marker
|
|
4467
5190
|
// version id + the rig's own timeout). Present only on a rig-bound turn.
|
|
4468
5191
|
rigSetup?: RigSetupDescriptor;
|
|
5192
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
4469
5193
|
};
|
|
4470
5194
|
|
|
5195
|
+
async function runSandboxLifecycleCommand(
|
|
5196
|
+
session: SandboxSessionLike,
|
|
5197
|
+
args: TurnSandboxCommandArgs,
|
|
5198
|
+
commandRunner?: SandboxLifecycleCommandRunner,
|
|
5199
|
+
): Promise<unknown> {
|
|
5200
|
+
if (commandRunner) {
|
|
5201
|
+
return await commandRunner(session as TurnSandboxCommandSession, args);
|
|
5202
|
+
}
|
|
5203
|
+
if (session.exec) return await session.exec(args);
|
|
5204
|
+
if (session.execCommand) return await session.execCommand(args);
|
|
5205
|
+
throw new Error("Sandbox session does not support command execution");
|
|
5206
|
+
}
|
|
5207
|
+
|
|
4471
5208
|
// M3: everything the rig-setup hook needs to run the frozen rig version's setup
|
|
4472
5209
|
// script exactly once per box. `versionId` keys the idempotence marker
|
|
4473
5210
|
// (/var/opengeni/rig-setup-<versionId>.done); `timeoutMs` is the rig-specific
|
|
@@ -4574,7 +5311,9 @@ export function withSandboxLifecycleHooks(
|
|
|
4574
5311
|
}
|
|
4575
5312
|
: {}),
|
|
4576
5313
|
...(client.delete
|
|
4577
|
-
? {
|
|
5314
|
+
? {
|
|
5315
|
+
delete: async (state: SandboxSessionState) => await client.delete!(state),
|
|
5316
|
+
}
|
|
4578
5317
|
: {}),
|
|
4579
5318
|
...(client.serializeSessionState
|
|
4580
5319
|
? {
|
|
@@ -4615,10 +5354,32 @@ function gitTokenSeedsForAgent(agent: Agent<any, any>): GitTokenSeeds | undefine
|
|
|
4615
5354
|
return agentGitTokenSeeds.get(agent);
|
|
4616
5355
|
}
|
|
4617
5356
|
|
|
5357
|
+
function gitCredentialBindingsForAgent(
|
|
5358
|
+
agent: Agent<any, any>,
|
|
5359
|
+
): GitCredentialBindingSeed[] | undefined {
|
|
5360
|
+
return agentGitCredentialBindings.get(agent);
|
|
5361
|
+
}
|
|
5362
|
+
|
|
4618
5363
|
function toolspaceTokenSeedForAgent(agent: Agent<any, any>): string | undefined {
|
|
4619
5364
|
return agentToolspaceTokenSeed.get(agent);
|
|
4620
5365
|
}
|
|
4621
5366
|
|
|
5367
|
+
function toolspaceTokenSessionIdForAgent(agent: Agent<any, any>): string | undefined {
|
|
5368
|
+
return agentToolspaceTokenSessionId.get(agent);
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
function toolspaceTokenFileForAgent(
|
|
5372
|
+
agent: Agent<any, any>,
|
|
5373
|
+
environment: Readonly<Record<string, string>>,
|
|
5374
|
+
): string | undefined {
|
|
5375
|
+
if (!toolspaceTokenSeedForAgent(agent)) return undefined;
|
|
5376
|
+
const sessionId = toolspaceTokenSessionIdForAgent(agent);
|
|
5377
|
+
if (!sessionId) {
|
|
5378
|
+
throw new Error("Toolspace token seed is missing its session identity");
|
|
5379
|
+
}
|
|
5380
|
+
return toolspaceTokenFileFromEnvironment(environment, sessionId);
|
|
5381
|
+
}
|
|
5382
|
+
|
|
4622
5383
|
function sandboxToolspaceTokenHooksForAgent(agent: Agent<any, any>): SandboxLifecycleHook[] {
|
|
4623
5384
|
return toolspaceTokenSeedForAgent(agent)
|
|
4624
5385
|
? [
|
|
@@ -4688,6 +5449,22 @@ function gitCredentialSessionRegistrationHooks(
|
|
|
4688
5449
|
: [];
|
|
4689
5450
|
}
|
|
4690
5451
|
|
|
5452
|
+
function toolspaceTokenSessionRegistrationHooks(
|
|
5453
|
+
callback: RunAgentStreamOptions["onToolspaceTokenSessionReady"],
|
|
5454
|
+
): SandboxLifecycleHook[] {
|
|
5455
|
+
return callback
|
|
5456
|
+
? [
|
|
5457
|
+
{
|
|
5458
|
+
id: "toolspace-token-renewal-registration",
|
|
5459
|
+
phase: "beforeAgentStart",
|
|
5460
|
+
run: async (session) => {
|
|
5461
|
+
await callback(session);
|
|
5462
|
+
},
|
|
5463
|
+
},
|
|
5464
|
+
]
|
|
5465
|
+
: [];
|
|
5466
|
+
}
|
|
5467
|
+
|
|
4691
5468
|
function sandboxRepositoryCloneHooks(
|
|
4692
5469
|
settings: Settings,
|
|
4693
5470
|
resources: ResourceRef[],
|
|
@@ -4755,6 +5532,20 @@ function gitProviderSeedEnv(provider: GitCredentialProvider): string {
|
|
|
4755
5532
|
return `OPENGENI_GIT_${provider.toUpperCase()}_TOKEN_SEED`;
|
|
4756
5533
|
}
|
|
4757
5534
|
|
|
5535
|
+
export function gitCredentialBindingHash(credentialBindingId: string): string {
|
|
5536
|
+
return createHash("sha256").update(credentialBindingId, "utf8").digest("hex").slice(0, 32);
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
function gitBindingSeedEnv(binding: GitCredentialBindingSeed): string {
|
|
5540
|
+
return `OPENGENI_GIT_BINDING_${gitCredentialBindingHash(binding.credentialBindingId).toUpperCase()}_TOKEN_SEED`;
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
function gitCredentialBindingSeedExportPrefix(bindings: GitCredentialBindingSeed[]): string {
|
|
5544
|
+
return bindings
|
|
5545
|
+
.map((binding) => `export ${gitBindingSeedEnv(binding)}=${shellQuote(binding.token)}`)
|
|
5546
|
+
.join("\n");
|
|
5547
|
+
}
|
|
5548
|
+
|
|
4758
5549
|
function gitTokenSeedExportPrefix(seeds: GitTokenSeeds): string {
|
|
4759
5550
|
const lines: string[] = [];
|
|
4760
5551
|
for (const provider of GIT_CREDENTIAL_PROVIDERS) {
|
|
@@ -4770,37 +5561,116 @@ function gitTokenSeedExportPrefix(seeds: GitTokenSeeds): string {
|
|
|
4770
5561
|
return lines.join("\n");
|
|
4771
5562
|
}
|
|
4772
5563
|
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
5564
|
+
type RuntimeGitBindingDescriptor = {
|
|
5565
|
+
provider: GitCredentialProvider;
|
|
5566
|
+
credentialBindingId: string;
|
|
5567
|
+
bindingHash: string;
|
|
5568
|
+
protocol: string;
|
|
5569
|
+
host: string;
|
|
5570
|
+
path: string;
|
|
5571
|
+
uri: string;
|
|
5572
|
+
};
|
|
5573
|
+
|
|
5574
|
+
function runtimeGitBindingDescriptors(
|
|
5575
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5576
|
+
): RuntimeGitBindingDescriptor[] {
|
|
5577
|
+
const remoteBindings = new Map<string, string>();
|
|
5578
|
+
const bindingProviders = new Map<string, GitCredentialProvider>();
|
|
5579
|
+
return resources.map((resource) => {
|
|
5580
|
+
const url = new URL(resource.uri);
|
|
5581
|
+
const credentialProvider = gitCredentialProviderForRepository(resource);
|
|
5582
|
+
// Provider-less public/legacy resources retain the historical GitHub
|
|
5583
|
+
// askpass fallback, but credential-bound resources derive through the
|
|
5584
|
+
// shared contracts helper used by the worker and core.
|
|
5585
|
+
const provider = credentialProvider ?? "github";
|
|
5586
|
+
const credentialBindingId =
|
|
5587
|
+
gitCredentialBindingIdForRepository(resource, credentialProvider) ?? provider;
|
|
5588
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "");
|
|
5589
|
+
const remote = `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}/${path.replace(/\.git$/, "")}`;
|
|
5590
|
+
const bindingKey = `${provider}\u0000${credentialBindingId}`;
|
|
5591
|
+
const boundProvider = bindingProviders.get(credentialBindingId);
|
|
5592
|
+
if (boundProvider && boundProvider !== provider) {
|
|
5593
|
+
throw new Error(
|
|
5594
|
+
`credential binding ${credentialBindingId} is assigned to multiple Git providers`,
|
|
5595
|
+
);
|
|
5596
|
+
}
|
|
5597
|
+
bindingProviders.set(credentialBindingId, provider);
|
|
5598
|
+
const claimed = remoteBindings.get(remote);
|
|
5599
|
+
if (claimed && claimed !== bindingKey) {
|
|
5600
|
+
throw new Error(
|
|
5601
|
+
`repository remote ${resource.uri} is claimed by multiple credential bindings`,
|
|
5602
|
+
);
|
|
5603
|
+
}
|
|
5604
|
+
remoteBindings.set(remote, bindingKey);
|
|
5605
|
+
return {
|
|
5606
|
+
provider,
|
|
5607
|
+
credentialBindingId,
|
|
5608
|
+
bindingHash: gitCredentialBindingHash(credentialBindingId),
|
|
5609
|
+
protocol: url.protocol.replace(/:$/, "").toLowerCase(),
|
|
5610
|
+
host: url.host.toLowerCase(),
|
|
5611
|
+
path,
|
|
5612
|
+
uri: resource.uri,
|
|
5613
|
+
};
|
|
5614
|
+
});
|
|
5615
|
+
}
|
|
5616
|
+
|
|
5617
|
+
function gitUsernameForProvider(provider: GitCredentialProvider): string {
|
|
5618
|
+
if (provider === "github") return "x-access-token";
|
|
5619
|
+
if (provider === "gitlab") return "oauth2";
|
|
5620
|
+
return "opengeni";
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
function gitCredentialHelperBindingCaseLines(
|
|
5624
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5625
|
+
): string[] {
|
|
5626
|
+
return runtimeGitBindingDescriptors(resources).flatMap((descriptor) => {
|
|
5627
|
+
const paths = new Set([
|
|
5628
|
+
descriptor.path,
|
|
5629
|
+
descriptor.path.replace(/\.git$/, ""),
|
|
5630
|
+
`${descriptor.path.replace(/\.git$/, "")}.git`,
|
|
5631
|
+
]);
|
|
5632
|
+
return [...paths].map(
|
|
5633
|
+
(path) =>
|
|
5634
|
+
` ${shellQuote(`${descriptor.protocol}|${descriptor.host}|${path}`)}) username=${shellQuote(gitUsernameForProvider(descriptor.provider))}; token_file="$credential_dir/${descriptor.bindingHash}-token" ;;`,
|
|
5635
|
+
);
|
|
5636
|
+
});
|
|
4777
5637
|
}
|
|
4778
5638
|
|
|
4779
5639
|
function gitAskpassHostProviderCaseLines(
|
|
4780
5640
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
4781
5641
|
): string[] {
|
|
4782
|
-
const hosts = new Map<string, GitCredentialProvider>();
|
|
4783
|
-
for (const
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
} catch {
|
|
4791
|
-
// Resource validation catches invalid URIs before normal runtime use. Keep
|
|
4792
|
-
// helper generation tolerant so tests for clone failure can still build.
|
|
4793
|
-
}
|
|
5642
|
+
const hosts = new Map<string, { provider: GitCredentialProvider; bindings: Set<string> }>();
|
|
5643
|
+
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
5644
|
+
const entry = hosts.get(descriptor.host) ?? {
|
|
5645
|
+
provider: descriptor.provider,
|
|
5646
|
+
bindings: new Set<string>(),
|
|
5647
|
+
};
|
|
5648
|
+
entry.bindings.add(`${descriptor.provider}\u0000${descriptor.credentialBindingId}`);
|
|
5649
|
+
hosts.set(descriptor.host, entry);
|
|
4794
5650
|
}
|
|
4795
5651
|
return [...hosts.entries()]
|
|
5652
|
+
.filter(([, entry]) => entry.bindings.size === 1)
|
|
4796
5653
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
4797
5654
|
.map(
|
|
4798
|
-
([hostname,
|
|
4799
|
-
` ${shellQuote(hostname)}) printf '%s\\n' ${provider}; return 0 ;;`,
|
|
5655
|
+
([hostname, entry]) =>
|
|
5656
|
+
` ${shellQuote(hostname)}) printf '%s\\n' ${entry.provider}; return 0 ;;`,
|
|
4800
5657
|
);
|
|
4801
5658
|
}
|
|
4802
5659
|
|
|
4803
|
-
function gitCredentialTokenWriterCommandLines(): string[] {
|
|
5660
|
+
function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed[] = []): string[] {
|
|
5661
|
+
const bindingWrites = bindings.flatMap((binding) => {
|
|
5662
|
+
const hash = gitCredentialBindingHash(binding.credentialBindingId);
|
|
5663
|
+
const seedEnv = gitBindingSeedEnv(binding);
|
|
5664
|
+
const count = Math.max(1, binding.providerBindingCount ?? 1);
|
|
5665
|
+
const provider = binding.provider;
|
|
5666
|
+
const lines = [`write_git_binding_token ${shellQuote(hash)} "\${${seedEnv}:-}"`];
|
|
5667
|
+
if (count === 1) {
|
|
5668
|
+
lines.push(`write_git_provider_token ${shellQuote(provider)} "\${${seedEnv}:-}"`);
|
|
5669
|
+
} else {
|
|
5670
|
+
lines.push(`remove_git_provider_token ${shellQuote(provider)}`);
|
|
5671
|
+
}
|
|
5672
|
+
return lines;
|
|
5673
|
+
});
|
|
4804
5674
|
return [
|
|
4805
5675
|
// TOKEN-BROKER (B1/B2): seed run-scoped provider tokens into stable files and
|
|
4806
5676
|
// atomically replace each provider file. Token VALUES are supplied only by
|
|
@@ -4814,6 +5684,23 @@ function gitCredentialTokenWriterCommandLines(): string[] {
|
|
|
4814
5684
|
" *) printf '%s\\n' \"${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$provider-token\" ;;",
|
|
4815
5685
|
" esac",
|
|
4816
5686
|
"}",
|
|
5687
|
+
"write_git_binding_token() {",
|
|
5688
|
+
' binding_hash="$1"',
|
|
5689
|
+
' token="$2"',
|
|
5690
|
+
' [ -n "$token" ] || return 0',
|
|
5691
|
+
' credential_dir="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}"',
|
|
5692
|
+
' mkdir -p "$credential_dir"',
|
|
5693
|
+
' token_file="$credential_dir/$binding_hash-token"',
|
|
5694
|
+
' printf \'%s\' "$token" > "$token_file.tmp.$$"',
|
|
5695
|
+
' mv -f "$token_file.tmp.$$" "$token_file"',
|
|
5696
|
+
"}",
|
|
5697
|
+
"remove_git_provider_token() {",
|
|
5698
|
+
' provider="$1"',
|
|
5699
|
+
' rm -f "$(git_provider_token_file "$provider")"',
|
|
5700
|
+
' if [ "$provider" = github ]; then',
|
|
5701
|
+
' rm -f "${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/github-token"',
|
|
5702
|
+
" fi",
|
|
5703
|
+
"}",
|
|
4817
5704
|
"write_git_provider_token() {",
|
|
4818
5705
|
' provider="$1"',
|
|
4819
5706
|
' token="$2"',
|
|
@@ -4834,16 +5721,53 @@ function gitCredentialTokenWriterCommandLines(): string[] {
|
|
|
4834
5721
|
'write_git_provider_token github "${OPENGENI_GIT_GITHUB_TOKEN_SEED:-${OPENGENI_GIT_TOKEN_SEED:-}}"',
|
|
4835
5722
|
'write_git_provider_token gitlab "${OPENGENI_GIT_GITLAB_TOKEN_SEED:-}"',
|
|
4836
5723
|
'write_git_provider_token azure_devops "${OPENGENI_GIT_AZURE_DEVOPS_TOKEN_SEED:-}"',
|
|
5724
|
+
...bindingWrites,
|
|
4837
5725
|
'umask "$seed_umask"',
|
|
4838
5726
|
];
|
|
4839
5727
|
}
|
|
4840
5728
|
|
|
4841
5729
|
function gitCredentialHelperCommandLines(
|
|
4842
5730
|
resources: Extract<ResourceRef, { kind: "repository" }>[] = [],
|
|
5731
|
+
bindings: GitCredentialBindingSeed[] = [],
|
|
4843
5732
|
): string[] {
|
|
4844
5733
|
const hostProviderCases = gitAskpassHostProviderCaseLines(resources);
|
|
5734
|
+
const bindingCases = gitCredentialHelperBindingCaseLines(resources);
|
|
5735
|
+
const descriptors = runtimeGitBindingDescriptors(resources);
|
|
5736
|
+
const wrapperDescriptors = bindings.length > 0 ? descriptors : [];
|
|
5737
|
+
const bindingProviders = new Map<GitCredentialProvider, Set<string>>();
|
|
5738
|
+
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
5739
|
+
const ids = bindingProviders.get(descriptor.provider) ?? new Set<string>();
|
|
5740
|
+
ids.add(descriptor.credentialBindingId);
|
|
5741
|
+
bindingProviders.set(descriptor.provider, ids);
|
|
5742
|
+
}
|
|
5743
|
+
const strictAskpass = [...bindingProviders.values()].some((ids) => ids.size > 1);
|
|
5744
|
+
const allowedWrapperHashes = [
|
|
5745
|
+
...new Set(wrapperDescriptors.map((item) => `${item.provider}|${item.bindingHash}`)),
|
|
5746
|
+
].map((key) => ` ${shellQuote(key)}) return 0 ;;`);
|
|
5747
|
+
const originWrapperHashes = wrapperDescriptors.flatMap((item) => {
|
|
5748
|
+
const base = item.uri.replace(/\.git$/, "");
|
|
5749
|
+
return [...new Set([item.uri, base, `${base}.git`])].map(
|
|
5750
|
+
(uri) =>
|
|
5751
|
+
` ${shellQuote(`${item.provider}|${uri}`)}) printf '%s\\n' ${shellQuote(item.bindingHash)}; return 0 ;;`,
|
|
5752
|
+
);
|
|
5753
|
+
});
|
|
5754
|
+
const soleWrapperHashes = [...bindingProviders.entries()].flatMap(([provider, ids]) => {
|
|
5755
|
+
if (ids.size !== 1) return [];
|
|
5756
|
+
const descriptor = wrapperDescriptors.find((item) => item.provider === provider);
|
|
5757
|
+
return descriptor
|
|
5758
|
+
? [
|
|
5759
|
+
` ${shellQuote(provider)}) printf '%s\\n' ${shellQuote(descriptor.bindingHash)}; return 0 ;;`,
|
|
5760
|
+
]
|
|
5761
|
+
: [];
|
|
5762
|
+
});
|
|
5763
|
+
const multiWrapperProviders =
|
|
5764
|
+
bindings.length > 0
|
|
5765
|
+
? [...bindingProviders.entries()]
|
|
5766
|
+
.filter(([, ids]) => ids.size > 1)
|
|
5767
|
+
.map(([provider]) => provider)
|
|
5768
|
+
: [];
|
|
4845
5769
|
return [
|
|
4846
|
-
...gitCredentialTokenWriterCommandLines(),
|
|
5770
|
+
...gitCredentialTokenWriterCommandLines(bindings),
|
|
4847
5771
|
// Provision git/provider-CLI helpers at SETUP (runtime) before any clone
|
|
4848
5772
|
// runs. Renewal updates only token files and deliberately leaves these
|
|
4849
5773
|
// repository-specific host mappings intact.
|
|
@@ -4860,8 +5784,8 @@ function gitCredentialHelperCommandLines(
|
|
|
4860
5784
|
' rest="${prompt_lower#*://}"',
|
|
4861
5785
|
' rest="${rest#*@}"',
|
|
4862
5786
|
' host="${rest%%/*}"',
|
|
4863
|
-
' host="${host%%:*}"',
|
|
4864
5787
|
' host="$(printf \'%s\\n\' "$host" | tr -d "\'")"',
|
|
5788
|
+
' host="${host%:}"',
|
|
4865
5789
|
" printf '%s\\n' \"$host\"",
|
|
4866
5790
|
"}",
|
|
4867
5791
|
"provider_for_prompt() {",
|
|
@@ -4873,7 +5797,7 @@ function gitCredentialHelperCommandLines(
|
|
|
4873
5797
|
" *github.com*|*githubusercontent.com*) printf '%s\\n' github ;;",
|
|
4874
5798
|
" *gitlab*) printf '%s\\n' gitlab ;;",
|
|
4875
5799
|
" *dev.azure.com*|*.visualstudio.com*) printf '%s\\n' azure_devops ;;",
|
|
4876
|
-
" *) printf '%s\\n' github ;;",
|
|
5800
|
+
strictAskpass ? " *) printf '\\n' ;;" : " *) printf '%s\\n' github ;;",
|
|
4877
5801
|
" esac",
|
|
4878
5802
|
"}",
|
|
4879
5803
|
"token_file_for_provider() {",
|
|
@@ -4899,6 +5823,40 @@ function gitCredentialHelperCommandLines(
|
|
|
4899
5823
|
"ASKPASS_EOF",
|
|
4900
5824
|
'chmod 0755 "$git_askpass.tmp.$$"',
|
|
4901
5825
|
'mv -f "$git_askpass.tmp.$$" "$git_askpass"',
|
|
5826
|
+
'git_credential_helper="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/helper"',
|
|
5827
|
+
'mkdir -p "$(dirname "$git_credential_helper")"',
|
|
5828
|
+
"cat > \"$git_credential_helper.tmp.$$\" <<'GIT_CREDENTIAL_HELPER_EOF'",
|
|
5829
|
+
"#!/usr/bin/env sh",
|
|
5830
|
+
"set -eu",
|
|
5831
|
+
'[ "${1:-get}" = get ] || exit 0',
|
|
5832
|
+
"protocol= host= path=",
|
|
5833
|
+
"while IFS='=' read -r key value; do",
|
|
5834
|
+
' case "$key" in',
|
|
5835
|
+
" protocol) protocol=\"$(printf '%s' \"$value\" | tr '[:upper:]' '[:lower:]')\" ;;",
|
|
5836
|
+
" host) host=\"$(printf '%s' \"$value\" | tr '[:upper:]' '[:lower:]')\" ;;",
|
|
5837
|
+
' path) path="${value#/}" ;;',
|
|
5838
|
+
" esac",
|
|
5839
|
+
"done",
|
|
5840
|
+
'credential_dir="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}"',
|
|
5841
|
+
"username= token_file=",
|
|
5842
|
+
'case "$protocol|$host|$path" in',
|
|
5843
|
+
...bindingCases,
|
|
5844
|
+
" *) exit 0 ;;",
|
|
5845
|
+
"esac",
|
|
5846
|
+
'[ -r "$token_file" ] || exit 0',
|
|
5847
|
+
'password="$(cat "$token_file" 2>/dev/null || true)"',
|
|
5848
|
+
'[ -n "$password" ] || exit 0',
|
|
5849
|
+
'printf \'username=%s\\npassword=%s\\n\' "$username" "$password"',
|
|
5850
|
+
"GIT_CREDENTIAL_HELPER_EOF",
|
|
5851
|
+
'chmod 0755 "$git_credential_helper.tmp.$$"',
|
|
5852
|
+
'mv -f "$git_credential_helper.tmp.$$" "$git_credential_helper"',
|
|
5853
|
+
// Empty helper resets lower-priority/system helpers; our exact path-aware
|
|
5854
|
+
// helper returns no credential for an unbound remote, so multi-binding
|
|
5855
|
+
// sessions fail closed instead of falling through to ambient credentials.
|
|
5856
|
+
"git config --global --unset-all credential.helper >/dev/null 2>&1 || true",
|
|
5857
|
+
"git config --global --add credential.helper ''",
|
|
5858
|
+
'git config --global --add credential.helper "$git_credential_helper"',
|
|
5859
|
+
"git config --global credential.useHttpPath true",
|
|
4902
5860
|
'wrapper_dir="${OPENGENI_GIT_CLI_WRAPPER_DIR:-$HOME/.opengeni/bin}"',
|
|
4903
5861
|
'mkdir -p "$wrapper_dir"',
|
|
4904
5862
|
"for opengeni_git_cli_tool in gh glab az; do",
|
|
@@ -4913,11 +5871,53 @@ function gitCredentialHelperCommandLines(
|
|
|
4913
5871
|
" az) provider=azure_devops; token_env=AZURE_DEVOPS_EXT_PAT ;;",
|
|
4914
5872
|
" *) provider=; token_env= ;;",
|
|
4915
5873
|
"esac",
|
|
4916
|
-
|
|
5874
|
+
"hash_binding_id() {",
|
|
5875
|
+
" if command -v sha256sum >/dev/null 2>&1; then printf '%s' \"$1\" | sha256sum | cut -c1-32; return; fi",
|
|
5876
|
+
" if command -v shasum >/dev/null 2>&1; then printf '%s' \"$1\" | shasum -a 256 | cut -c1-32; return; fi",
|
|
5877
|
+
" if command -v openssl >/dev/null 2>&1; then printf '%s' \"$1\" | openssl dgst -sha256 | sed 's/^.*= //' | cut -c1-32; return; fi",
|
|
5878
|
+
" printf '%s\\n' 'No SHA-256 utility is available to select OPENGENI_GIT_BINDING' >&2",
|
|
5879
|
+
" return 127",
|
|
5880
|
+
"}",
|
|
5881
|
+
"binding_hash_allowed() {",
|
|
5882
|
+
' case "$provider|$1" in',
|
|
5883
|
+
...allowedWrapperHashes,
|
|
5884
|
+
" *) return 1 ;;",
|
|
5885
|
+
" esac",
|
|
5886
|
+
"}",
|
|
5887
|
+
"binding_hash_for_origin() {",
|
|
5888
|
+
' case "$provider|$1" in',
|
|
5889
|
+
...originWrapperHashes,
|
|
5890
|
+
" *) return 1 ;;",
|
|
5891
|
+
" esac",
|
|
5892
|
+
"}",
|
|
5893
|
+
"sole_binding_hash() {",
|
|
4917
5894
|
' case "$provider" in',
|
|
4918
|
-
|
|
4919
|
-
|
|
5895
|
+
...soleWrapperHashes,
|
|
5896
|
+
" *) return 1 ;;",
|
|
4920
5897
|
" esac",
|
|
5898
|
+
"}",
|
|
5899
|
+
`multi_binding_providers=${shellQuote(multiWrapperProviders.join(" "))}`,
|
|
5900
|
+
'if [ -n "$provider" ]; then',
|
|
5901
|
+
" binding_hash=",
|
|
5902
|
+
' if [ -n "${OPENGENI_GIT_BINDING:-}" ]; then',
|
|
5903
|
+
' binding_hash="$(hash_binding_id "$OPENGENI_GIT_BINDING")"',
|
|
5904
|
+
' binding_hash_allowed "$binding_hash" || { printf \'%s\\n\' "OPENGENI_GIT_BINDING does not select a $provider credential attached to this session" >&2; exit 2; }',
|
|
5905
|
+
" elif command -v git >/dev/null 2>&1; then",
|
|
5906
|
+
' origin="$(git config --get remote.origin.url 2>/dev/null || true)"',
|
|
5907
|
+
' [ -z "$origin" ] || binding_hash="$(binding_hash_for_origin "$origin" 2>/dev/null || true)"',
|
|
5908
|
+
" fi",
|
|
5909
|
+
' [ -n "$binding_hash" ] || binding_hash="$(sole_binding_hash 2>/dev/null || true)"',
|
|
5910
|
+
' if [ -n "$binding_hash" ]; then',
|
|
5911
|
+
' token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$binding_hash-token"',
|
|
5912
|
+
" else",
|
|
5913
|
+
' case " $multi_binding_providers " in',
|
|
5914
|
+
' *" $provider "*) printf \'%s\\n\' "Unable to select one of multiple $provider credentials; run inside an attached repository or set OPENGENI_GIT_BINDING" >&2; exit 2 ;;',
|
|
5915
|
+
" esac",
|
|
5916
|
+
' case "$provider" in',
|
|
5917
|
+
' github) token_file="${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" ;;',
|
|
5918
|
+
' *) token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$provider-token" ;;',
|
|
5919
|
+
" esac",
|
|
5920
|
+
" fi",
|
|
4921
5921
|
' if [ -f "$token_file" ]; then',
|
|
4922
5922
|
' token="$(cat "$token_file" 2>/dev/null || true)"',
|
|
4923
5923
|
' if [ -n "$token" ]; then',
|
|
@@ -4972,10 +5972,23 @@ export function gitProviderTokenRefreshCommand(seeds: GitTokenSeeds): string {
|
|
|
4972
5972
|
].join("\n");
|
|
4973
5973
|
}
|
|
4974
5974
|
|
|
5975
|
+
export function gitCredentialBindingTokenRefreshCommand(
|
|
5976
|
+
bindings: GitCredentialBindingSeed[],
|
|
5977
|
+
): string {
|
|
5978
|
+
const seedPrefix = gitCredentialBindingSeedExportPrefix(bindings);
|
|
5979
|
+
if (!seedPrefix) return "";
|
|
5980
|
+
return [
|
|
5981
|
+
seedPrefix,
|
|
5982
|
+
"set -eu",
|
|
5983
|
+
'export HOME="${HOME:-/workspace}"',
|
|
5984
|
+
...gitCredentialTokenWriterCommandLines(bindings),
|
|
5985
|
+
].join("\n");
|
|
5986
|
+
}
|
|
5987
|
+
|
|
4975
5988
|
export async function refreshGitProviderTokenFiles(
|
|
4976
5989
|
session: GitCredentialTokenWriterSession,
|
|
4977
5990
|
seeds: GitTokenSeeds,
|
|
4978
|
-
options: { runAs?: string } = {},
|
|
5991
|
+
options: { runAs?: string; commandRunner?: SandboxLifecycleCommandRunner } = {},
|
|
4979
5992
|
): Promise<void> {
|
|
4980
5993
|
const command = gitProviderTokenRefreshCommand(seeds);
|
|
4981
5994
|
if (!command) {
|
|
@@ -4988,23 +6001,41 @@ export async function refreshGitProviderTokenFiles(
|
|
|
4988
6001
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
4989
6002
|
maxOutputTokens: 4_000,
|
|
4990
6003
|
};
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
6004
|
+
assertSandboxCommandSucceeded(
|
|
6005
|
+
await runSandboxLifecycleCommand(session, args, options.commandRunner),
|
|
6006
|
+
"Git credential refresh",
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
export async function refreshGitCredentialBindingTokenFiles(
|
|
6011
|
+
session: GitCredentialTokenWriterSession,
|
|
6012
|
+
bindings: GitCredentialBindingSeed[],
|
|
6013
|
+
options: { runAs?: string; commandRunner?: SandboxLifecycleCommandRunner } = {},
|
|
6014
|
+
): Promise<void> {
|
|
6015
|
+
const command = gitCredentialBindingTokenRefreshCommand(bindings);
|
|
6016
|
+
if (!command) return;
|
|
6017
|
+
const args = {
|
|
6018
|
+
cmd: command,
|
|
6019
|
+
workdir: "/workspace",
|
|
6020
|
+
...(options.runAs ? { runAs: options.runAs } : {}),
|
|
6021
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
6022
|
+
maxOutputTokens: 4_000,
|
|
6023
|
+
};
|
|
6024
|
+
assertSandboxCommandSucceeded(
|
|
6025
|
+
await runSandboxLifecycleCommand(session, args, options.commandRunner),
|
|
6026
|
+
"Git credential binding refresh",
|
|
6027
|
+
);
|
|
4998
6028
|
}
|
|
4999
6029
|
|
|
5000
6030
|
export function repositoryCloneCommand(
|
|
5001
6031
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
6032
|
+
bindings: GitCredentialBindingSeed[] = [],
|
|
5002
6033
|
): string {
|
|
6034
|
+
assertUniqueResourceMountPaths(resources);
|
|
5003
6035
|
const commands = [
|
|
5004
6036
|
"set -eu",
|
|
5005
6037
|
'export HOME="${HOME:-/workspace}"',
|
|
5006
6038
|
'export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"',
|
|
5007
|
-
...gitCredentialHelperCommandLines(resources),
|
|
5008
6039
|
"ensure_git() {",
|
|
5009
6040
|
" if command -v git >/dev/null 2>&1; then",
|
|
5010
6041
|
" return 0",
|
|
@@ -5020,6 +6051,7 @@ export function repositoryCloneCommand(
|
|
|
5020
6051
|
" exit 127",
|
|
5021
6052
|
"}",
|
|
5022
6053
|
"ensure_git",
|
|
6054
|
+
...gitCredentialHelperCommandLines(resources, bindings),
|
|
5023
6055
|
"clone_repository() {",
|
|
5024
6056
|
' target="$1"',
|
|
5025
6057
|
' uri="$2"',
|
|
@@ -5092,33 +6124,41 @@ export function repositoryCloneCommand(
|
|
|
5092
6124
|
"}",
|
|
5093
6125
|
];
|
|
5094
6126
|
for (const resource of resources) {
|
|
5095
|
-
const
|
|
5096
|
-
const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
5097
|
-
const mountPath = normalizeManifestPath(resource.mountPath ?? `repos/${repo}`);
|
|
6127
|
+
const mountPath = resourceMountPath(resource);
|
|
5098
6128
|
commands.push(
|
|
5099
6129
|
[
|
|
5100
6130
|
"clone_repository",
|
|
5101
6131
|
shellQuote(posixPath.join("/workspace", mountPath)),
|
|
5102
6132
|
shellQuote(resource.uri),
|
|
5103
6133
|
shellQuote(resource.ref),
|
|
5104
|
-
shellQuote(resource.subpath ?
|
|
6134
|
+
shellQuote(resource.subpath ? normalizeRepositorySubpath(resource.subpath) : ""),
|
|
5105
6135
|
].join(" "),
|
|
5106
6136
|
);
|
|
5107
6137
|
}
|
|
5108
6138
|
return commands.join("\n");
|
|
5109
6139
|
}
|
|
5110
6140
|
|
|
5111
|
-
export function toolspaceTokenSeedCommand(
|
|
6141
|
+
export function toolspaceTokenSeedCommand(
|
|
6142
|
+
options: { tokenFile?: string; legacyTokenFile?: string } = {},
|
|
6143
|
+
): string {
|
|
5112
6144
|
return [
|
|
5113
6145
|
"set -eu",
|
|
5114
6146
|
'export HOME="${HOME:-/workspace}"',
|
|
5115
6147
|
'if [ -n "${OPENGENI_TOOLSPACE_TOKEN_SEED:-}" ]; then',
|
|
5116
6148
|
' seed_umask="$(umask)"',
|
|
5117
6149
|
" umask 077",
|
|
5118
|
-
|
|
6150
|
+
options.tokenFile
|
|
6151
|
+
? ` token_file=${shellQuote(options.tokenFile)}`
|
|
6152
|
+
: ' token_file="${OPENGENI_TOOLSPACE_TOKEN_FILE:-$HOME/.opengeni/toolspace-token}"',
|
|
6153
|
+
options.legacyTokenFile
|
|
6154
|
+
? ` legacy_token_file=${shellQuote(options.legacyTokenFile)}`
|
|
6155
|
+
: ' legacy_token_file=""',
|
|
5119
6156
|
' mkdir -p "$(dirname "$token_file")"',
|
|
5120
6157
|
' printf \'%s\' "$OPENGENI_TOOLSPACE_TOKEN_SEED" > "$token_file.tmp.$$"',
|
|
5121
6158
|
' mv -f "$token_file.tmp.$$" "$token_file"',
|
|
6159
|
+
' if [ -n "$legacy_token_file" ] && [ "$legacy_token_file" != "$token_file" ]; then',
|
|
6160
|
+
' rm -f -- "$legacy_token_file"',
|
|
6161
|
+
" fi",
|
|
5122
6162
|
' umask "$seed_umask"',
|
|
5123
6163
|
"fi",
|
|
5124
6164
|
].join("\n");
|
|
@@ -5131,28 +6171,56 @@ export async function runToolspaceTokenSeedHook(
|
|
|
5131
6171
|
if (!context.toolspaceTokenSeed) {
|
|
5132
6172
|
return;
|
|
5133
6173
|
}
|
|
5134
|
-
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(context.toolspaceTokenSeed)}\n${toolspaceTokenSeedCommand(
|
|
5135
|
-
|
|
5136
|
-
|
|
6174
|
+
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(context.toolspaceTokenSeed)}\n${toolspaceTokenSeedCommand(
|
|
6175
|
+
{
|
|
6176
|
+
...(context.toolspaceTokenFile ? { tokenFile: context.toolspaceTokenFile } : {}),
|
|
6177
|
+
...(context.toolspaceTokenFile && context.environment.OPENGENI_TOOLSPACE_TOKEN_FILE
|
|
6178
|
+
? { legacyTokenFile: context.environment.OPENGENI_TOOLSPACE_TOKEN_FILE }
|
|
6179
|
+
: {}),
|
|
6180
|
+
},
|
|
6181
|
+
)}`;
|
|
6182
|
+
const result = await runSandboxLifecycleCommand(
|
|
6183
|
+
session,
|
|
6184
|
+
{
|
|
5137
6185
|
cmd: command,
|
|
5138
6186
|
workdir: "/workspace",
|
|
5139
6187
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5140
6188
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5141
6189
|
maxOutputTokens: 4_000,
|
|
5142
|
-
}
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
6190
|
+
},
|
|
6191
|
+
context.commandRunner,
|
|
6192
|
+
);
|
|
6193
|
+
assertSandboxCommandSucceeded(result, "Toolspace token seed hook");
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
export async function refreshToolspaceTokenFile(
|
|
6197
|
+
session: ToolspaceTokenWriterSession,
|
|
6198
|
+
token: string,
|
|
6199
|
+
options: {
|
|
6200
|
+
runAs?: string;
|
|
6201
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
6202
|
+
tokenFile?: string;
|
|
6203
|
+
legacyTokenFile?: string;
|
|
6204
|
+
} = {},
|
|
6205
|
+
): Promise<void> {
|
|
6206
|
+
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(token)}\n${toolspaceTokenSeedCommand(
|
|
6207
|
+
{
|
|
6208
|
+
...(options.tokenFile ? { tokenFile: options.tokenFile } : {}),
|
|
6209
|
+
...(options.legacyTokenFile ? { legacyTokenFile: options.legacyTokenFile } : {}),
|
|
6210
|
+
},
|
|
6211
|
+
)}`;
|
|
6212
|
+
const result = await runSandboxLifecycleCommand(
|
|
6213
|
+
session,
|
|
6214
|
+
{
|
|
5146
6215
|
cmd: command,
|
|
5147
6216
|
workdir: "/workspace",
|
|
5148
|
-
...(
|
|
6217
|
+
...(options.runAs ? { runAs: options.runAs } : {}),
|
|
5149
6218
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5150
6219
|
maxOutputTokens: 4_000,
|
|
5151
|
-
}
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
}
|
|
6220
|
+
},
|
|
6221
|
+
options.commandRunner,
|
|
6222
|
+
);
|
|
6223
|
+
assertSandboxCommandSucceeded(result, "Toolspace token refresh");
|
|
5156
6224
|
}
|
|
5157
6225
|
|
|
5158
6226
|
// Bounds the setup output tail carried on a rig.setup failure event/error so a
|
|
@@ -5261,18 +6329,15 @@ export async function runRigSetupHook(
|
|
|
5261
6329
|
};
|
|
5262
6330
|
let result: unknown;
|
|
5263
6331
|
try {
|
|
5264
|
-
|
|
5265
|
-
result = await session.exec(execArgs);
|
|
5266
|
-
} else if (session.execCommand) {
|
|
5267
|
-
result = await session.execCommand(execArgs);
|
|
5268
|
-
} else {
|
|
5269
|
-
throw new Error("Sandbox session does not support command execution");
|
|
5270
|
-
}
|
|
6332
|
+
result = await runSandboxLifecycleCommand(session, execArgs, context.commandRunner);
|
|
5271
6333
|
} catch (error) {
|
|
5272
6334
|
const message = error instanceof Error ? error.message : String(error);
|
|
5273
6335
|
await context.onRuntimeEvent?.({
|
|
5274
6336
|
type: "rig.setup.failed",
|
|
5275
|
-
payload: {
|
|
6337
|
+
payload: {
|
|
6338
|
+
...payload,
|
|
6339
|
+
error: message.slice(-RIG_SETUP_OUTPUT_TAIL_LIMIT),
|
|
6340
|
+
},
|
|
5276
6341
|
});
|
|
5277
6342
|
throw new Error(
|
|
5278
6343
|
`Rig setup failed for rig "${rigSetup.rigName}" (version ${rigSetup.versionId}): ${message}`,
|
|
@@ -5304,7 +6369,10 @@ export async function runRigSetupHook(
|
|
|
5304
6369
|
);
|
|
5305
6370
|
await context.onRuntimeEvent?.({
|
|
5306
6371
|
type: "rig.setup.failed",
|
|
5307
|
-
payload: {
|
|
6372
|
+
payload: {
|
|
6373
|
+
...payload,
|
|
6374
|
+
error: failure.message.slice(-RIG_SETUP_OUTPUT_TAIL_LIMIT),
|
|
6375
|
+
},
|
|
5308
6376
|
});
|
|
5309
6377
|
throw failure;
|
|
5310
6378
|
}
|
|
@@ -5319,8 +6387,14 @@ export async function runRepositoryCloneHook(
|
|
|
5319
6387
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5320
6388
|
context: SandboxLifecycleHookContext = { environment: {} },
|
|
5321
6389
|
): Promise<void> {
|
|
5322
|
-
const payload = {
|
|
5323
|
-
|
|
6390
|
+
const payload = {
|
|
6391
|
+
name: "repository-clone",
|
|
6392
|
+
repositoryCount: resources.length,
|
|
6393
|
+
};
|
|
6394
|
+
await context.onRuntimeEvent?.({
|
|
6395
|
+
type: "sandbox.operation.started",
|
|
6396
|
+
payload,
|
|
6397
|
+
});
|
|
5324
6398
|
try {
|
|
5325
6399
|
// TOKEN-BROKER (B1): thread run-scoped provider tokens PER-EXEC, never on
|
|
5326
6400
|
// the manifest. The SDK's ExecCommandArgs has no `environment` field (exec
|
|
@@ -5334,32 +6408,31 @@ export async function runRepositoryCloneHook(
|
|
|
5334
6408
|
...(context.gitTokenSeeds ?? {}),
|
|
5335
6409
|
...(context.gitTokenSeed ? { github: context.gitTokenSeed } : {}),
|
|
5336
6410
|
} satisfies GitTokenSeeds;
|
|
5337
|
-
const
|
|
5338
|
-
const
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
assertSandboxCommandSucceeded(result, "Repository clone hook");
|
|
5350
|
-
} else if (session.execCommand) {
|
|
5351
|
-
const result = await session.execCommand({
|
|
6411
|
+
const gitCredentialBindings = context.gitCredentialBindings ?? [];
|
|
6412
|
+
const seedPrefix = [
|
|
6413
|
+
gitTokenSeedExportPrefix(gitTokenSeeds),
|
|
6414
|
+
gitCredentialBindingSeedExportPrefix(gitCredentialBindings),
|
|
6415
|
+
]
|
|
6416
|
+
.filter(Boolean)
|
|
6417
|
+
.join("\n");
|
|
6418
|
+
const cloneCommand = repositoryCloneCommand(resources, gitCredentialBindings);
|
|
6419
|
+
const command = seedPrefix ? `${seedPrefix}\n${cloneCommand}` : cloneCommand;
|
|
6420
|
+
const result = await runSandboxLifecycleCommand(
|
|
6421
|
+
session,
|
|
6422
|
+
{
|
|
5352
6423
|
cmd: command,
|
|
5353
6424
|
workdir: "/workspace",
|
|
5354
6425
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5355
6426
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5356
6427
|
maxOutputTokens: 20_000,
|
|
5357
|
-
}
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
6428
|
+
},
|
|
6429
|
+
context.commandRunner,
|
|
6430
|
+
);
|
|
6431
|
+
assertSandboxCommandSucceeded(result, "Repository clone hook");
|
|
6432
|
+
await context.onRuntimeEvent?.({
|
|
6433
|
+
type: "sandbox.operation.completed",
|
|
6434
|
+
payload,
|
|
6435
|
+
});
|
|
5363
6436
|
} catch (error) {
|
|
5364
6437
|
await context.onRuntimeEvent?.({
|
|
5365
6438
|
type: "sandbox.operation.failed",
|
|
@@ -5472,31 +6545,31 @@ export async function runAzureCliLoginHook(
|
|
|
5472
6545
|
session: SandboxSessionLike,
|
|
5473
6546
|
context: SandboxLifecycleHookContext = { environment: {} },
|
|
5474
6547
|
): Promise<void> {
|
|
5475
|
-
const payload = {
|
|
5476
|
-
|
|
6548
|
+
const payload = {
|
|
6549
|
+
name: "azure-cli-login",
|
|
6550
|
+
command: "az login --service-principal",
|
|
6551
|
+
};
|
|
6552
|
+
await context.onRuntimeEvent?.({
|
|
6553
|
+
type: "sandbox.operation.started",
|
|
6554
|
+
payload,
|
|
6555
|
+
});
|
|
5477
6556
|
try {
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
workdir: "/workspace",
|
|
5482
|
-
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5483
|
-
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5484
|
-
maxOutputTokens: 20_000,
|
|
5485
|
-
});
|
|
5486
|
-
assertSandboxCommandSucceeded(result, "Azure CLI login hook");
|
|
5487
|
-
} else if (session.execCommand) {
|
|
5488
|
-
const result = await session.execCommand({
|
|
6557
|
+
const result = await runSandboxLifecycleCommand(
|
|
6558
|
+
session,
|
|
6559
|
+
{
|
|
5489
6560
|
cmd: azureCliLoginCommand(),
|
|
5490
6561
|
workdir: "/workspace",
|
|
5491
6562
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5492
6563
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5493
6564
|
maxOutputTokens: 20_000,
|
|
5494
|
-
}
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
6565
|
+
},
|
|
6566
|
+
context.commandRunner,
|
|
6567
|
+
);
|
|
6568
|
+
assertSandboxCommandSucceeded(result, "Azure CLI login hook");
|
|
6569
|
+
await context.onRuntimeEvent?.({
|
|
6570
|
+
type: "sandbox.operation.completed",
|
|
6571
|
+
payload,
|
|
6572
|
+
});
|
|
5500
6573
|
} catch (error) {
|
|
5501
6574
|
await context.onRuntimeEvent?.({
|
|
5502
6575
|
type: "sandbox.operation.failed",
|
|
@@ -5582,26 +6655,46 @@ function isPathWithin(root: string, candidate: string): boolean {
|
|
|
5582
6655
|
}
|
|
5583
6656
|
|
|
5584
6657
|
/**
|
|
5585
|
-
* The skill source fed to the SDK Skills capability. Without pack
|
|
5586
|
-
* is the plain bundled local-dir source, byte-for-byte the
|
|
5587
|
-
* With
|
|
5588
|
-
* skill directories (as local_dir
|
|
5589
|
-
*
|
|
5590
|
-
* index, one `## Skills` instruction section, lazy
|
|
5591
|
-
* them. A pack skill shadows a bundled
|
|
6658
|
+
* The skill source fed to the SDK Skills capability. Without pack or curated
|
|
6659
|
+
* skills this is the plain bundled local-dir source, byte-for-byte the
|
|
6660
|
+
* pre-pack behavior. With either selected source it becomes a single
|
|
6661
|
+
* in-memory dir source combining bundled skill directories (as local_dir
|
|
6662
|
+
* entries the SDK materializes lazily) with selected in-memory skill
|
|
6663
|
+
* directories — one skill index, one `## Skills` instruction section, lazy
|
|
6664
|
+
* `load_skill` for all of them. A pack skill shadows a bundled or curated
|
|
6665
|
+
* skill with the same directory name, case-insensitively.
|
|
5592
6666
|
*/
|
|
5593
|
-
export function lazySkillSourceWithPackSkills(
|
|
6667
|
+
export function lazySkillSourceWithPackSkills(
|
|
6668
|
+
packSkills: PackSkill[],
|
|
6669
|
+
skillLibrarySkills: PackSkill[] = [],
|
|
6670
|
+
): LocalDirLazySkillSource {
|
|
5594
6671
|
const bundledDir = bundledSkillsDir();
|
|
5595
6672
|
const bundled = localDirLazySkillSource({ src: bundledDir });
|
|
5596
|
-
if (packSkills.length === 0) {
|
|
6673
|
+
if (packSkills.length === 0 && skillLibrarySkills.length === 0) {
|
|
5597
6674
|
return bundled;
|
|
5598
6675
|
}
|
|
5599
6676
|
const children: Record<string, Entry> = {};
|
|
5600
6677
|
for (const name of bundledSkillDirNames(bundledDir)) {
|
|
5601
6678
|
children[name] = localDir({ src: join(bundledDir, name) });
|
|
5602
6679
|
}
|
|
6680
|
+
const libraryIndex: SkillIndexEntry[] = [];
|
|
6681
|
+
const libraryNameKeys = new Set<string>();
|
|
6682
|
+
for (const skill of skillLibrarySkills) {
|
|
6683
|
+
assertSafePackSkillName(skill.name);
|
|
6684
|
+
const key = skill.name.toLowerCase();
|
|
6685
|
+
if (libraryNameKeys.has(key)) {
|
|
6686
|
+
throw new Error(`Duplicate curated skill name: ${skill.name}`);
|
|
6687
|
+
}
|
|
6688
|
+
libraryNameKeys.add(key);
|
|
6689
|
+
removeSkillChildByNameKey(children, key);
|
|
6690
|
+
children[skill.name] = packSkillDirEntry(skill);
|
|
6691
|
+
libraryIndex.push({
|
|
6692
|
+
name: skill.name,
|
|
6693
|
+
description: packSkillDescription(skill),
|
|
6694
|
+
path: skill.name,
|
|
6695
|
+
});
|
|
6696
|
+
}
|
|
5603
6697
|
const packIndex: SkillIndexEntry[] = [];
|
|
5604
|
-
const packNames = new Set<string>();
|
|
5605
6698
|
const packNameKeys = new Set<string>();
|
|
5606
6699
|
for (const skill of packSkills) {
|
|
5607
6700
|
assertSafePackSkillName(skill.name);
|
|
@@ -5609,7 +6702,7 @@ export function lazySkillSourceWithPackSkills(packSkills: PackSkill[]): LocalDir
|
|
|
5609
6702
|
throw new Error(`Duplicate pack skill name: ${skill.name}`);
|
|
5610
6703
|
}
|
|
5611
6704
|
packNameKeys.add(skill.name.toLowerCase());
|
|
5612
|
-
|
|
6705
|
+
removeSkillChildByNameKey(children, skill.name.toLowerCase());
|
|
5613
6706
|
children[skill.name] = packSkillDirEntry(skill);
|
|
5614
6707
|
packIndex.push({
|
|
5615
6708
|
name: skill.name,
|
|
@@ -5621,13 +6714,62 @@ export function lazySkillSourceWithPackSkills(packSkills: PackSkill[]): LocalDir
|
|
|
5621
6714
|
source: dir({ children }),
|
|
5622
6715
|
getIndex: (manifest, skillsPath) => [
|
|
5623
6716
|
...(bundled.getIndex?.(manifest, skillsPath) ?? []).filter(
|
|
5624
|
-
(entry) =>
|
|
6717
|
+
(entry) =>
|
|
6718
|
+
!packNameKeys.has((entry.path ?? entry.name).toLowerCase()) &&
|
|
6719
|
+
!libraryNameKeys.has((entry.path ?? entry.name).toLowerCase()),
|
|
6720
|
+
),
|
|
6721
|
+
...libraryIndex.filter(
|
|
6722
|
+
(entry) => !packNameKeys.has((entry.path ?? entry.name).toLowerCase()),
|
|
5625
6723
|
),
|
|
5626
6724
|
...packIndex,
|
|
5627
6725
|
],
|
|
5628
6726
|
};
|
|
5629
6727
|
}
|
|
5630
6728
|
|
|
6729
|
+
function effectiveSkillSelections(
|
|
6730
|
+
librarySelections: readonly EffectiveSkillSelection[],
|
|
6731
|
+
librarySkills: readonly PackSkill[],
|
|
6732
|
+
packSkills: readonly PackSkill[],
|
|
6733
|
+
): readonly EffectiveSkillSelection[] {
|
|
6734
|
+
const defaultSelections = bundledSkillDirNames(bundledSkillsDir()).map((name) => ({
|
|
6735
|
+
id: `bundled:${name}`,
|
|
6736
|
+
name,
|
|
6737
|
+
source: "bundled" as const,
|
|
6738
|
+
version: null,
|
|
6739
|
+
contentSha256: null,
|
|
6740
|
+
reason: "deployment default skill bundle",
|
|
6741
|
+
}));
|
|
6742
|
+
const libraryNameKeys = new Set(librarySkills.map((skill) => skill.name.toLowerCase()));
|
|
6743
|
+
const packNameKeys = new Set(packSkills.map((skill) => skill.name.toLowerCase()));
|
|
6744
|
+
const selected = librarySelections.filter((selection) =>
|
|
6745
|
+
libraryNameKeys.has(selection.name.toLowerCase()),
|
|
6746
|
+
);
|
|
6747
|
+
return [
|
|
6748
|
+
...defaultSelections.filter(
|
|
6749
|
+
(selection) =>
|
|
6750
|
+
!libraryNameKeys.has(selection.name.toLowerCase()) &&
|
|
6751
|
+
!packNameKeys.has(selection.name.toLowerCase()),
|
|
6752
|
+
),
|
|
6753
|
+
...selected.filter((selection) => !packNameKeys.has(selection.name.toLowerCase())),
|
|
6754
|
+
...packSkills.map((skill) => ({
|
|
6755
|
+
id: `pack:${skill.name}`,
|
|
6756
|
+
name: skill.name,
|
|
6757
|
+
source: "pack" as const,
|
|
6758
|
+
version: null,
|
|
6759
|
+
contentSha256: null,
|
|
6760
|
+
reason: "enabled capability pack",
|
|
6761
|
+
})),
|
|
6762
|
+
];
|
|
6763
|
+
}
|
|
6764
|
+
|
|
6765
|
+
function removeSkillChildByNameKey(children: Record<string, Entry>, nameKey: string): void {
|
|
6766
|
+
for (const name of Object.keys(children)) {
|
|
6767
|
+
if (name.toLowerCase() === nameKey) {
|
|
6768
|
+
delete children[name];
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
|
|
5631
6773
|
function bundledSkillDirNames(root: string): string[] {
|
|
5632
6774
|
return readdirSync(root, { withFileTypes: true })
|
|
5633
6775
|
.filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "SKILL.md")))
|
|
@@ -5771,7 +6913,12 @@ function sortJson(value: unknown): unknown {
|
|
|
5771
6913
|
}
|
|
5772
6914
|
return value;
|
|
5773
6915
|
}
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
6916
|
+
function interruptionToolName(item: unknown): string {
|
|
6917
|
+
const candidate = item as {
|
|
6918
|
+
toolName?: unknown;
|
|
6919
|
+
name?: unknown;
|
|
6920
|
+
rawItem?: { name?: unknown };
|
|
6921
|
+
};
|
|
6922
|
+
const name = candidate?.toolName ?? candidate?.name ?? candidate?.rawItem?.name;
|
|
6923
|
+
return typeof name === "string" ? name : "";
|
|
5777
6924
|
}
|