@opengeni/runtime 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2PO56VAL.js +3478 -0
- package/dist/chunk-2PO56VAL.js.map +1 -0
- package/dist/index.d.ts +912 -0
- package/dist/index.js +3663 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox/index.d.ts +1738 -0
- package/dist/sandbox/index.js +187 -0
- package/dist/sandbox/index.js.map +1 -0
- package/package.json +49 -0
- package/src/bundled_hashicorp_terraform_skills/LICENSE +373 -0
- package/src/bundled_hashicorp_terraform_skills/README.md +18 -0
- package/src/bundled_hashicorp_terraform_skills/UPSTREAM_GIT_SHA +1 -0
- package/src/bundled_hashicorp_terraform_skills/azure-verified-modules/SKILL.md +613 -0
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +43 -0
- package/src/bundled_hashicorp_terraform_skills/refactor-module/SKILL.md +538 -0
- package/src/bundled_hashicorp_terraform_skills/social-media-marketing/SKILL.md +35 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/SKILL.md +372 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/references/MANUAL-IMPORT.md +113 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/scripts/list_resources.sh +38 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/SKILL.md +480 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/api-monitoring.md +543 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/component-blocks.md +476 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/deployment-blocks.md +391 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/examples.md +1529 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/linked-stacks.md +187 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/troubleshooting.md +671 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-style-guide/SKILL.md +353 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/SKILL.md +451 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/CI_CD.md +80 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/EXAMPLES.md +314 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/MOCK_PROVIDERS.md +171 -0
- package/src/codex-tool-search.ts +267 -0
- package/src/context-compaction.ts +538 -0
- package/src/history-sanitizer.ts +719 -0
- package/src/index.ts +3299 -0
- package/src/sandbox/capabilities.ts +69 -0
- package/src/sandbox/channel-a.ts +1031 -0
- package/src/sandbox/display-stack.ts +231 -0
- package/src/sandbox/errors.ts +34 -0
- package/src/sandbox/index.ts +832 -0
- package/src/sandbox/providers/blaxel.ts +35 -0
- package/src/sandbox/providers/cloudflare.ts +24 -0
- package/src/sandbox/providers/daytona.ts +34 -0
- package/src/sandbox/providers/docker.ts +17 -0
- package/src/sandbox/providers/e2b.ts +36 -0
- package/src/sandbox/providers/index.ts +107 -0
- package/src/sandbox/providers/local.ts +13 -0
- package/src/sandbox/providers/modal.ts +55 -0
- package/src/sandbox/providers/none.ts +13 -0
- package/src/sandbox/providers/runloop.ts +32 -0
- package/src/sandbox/providers/selfhosted.ts +96 -0
- package/src/sandbox/providers/types.ts +38 -0
- package/src/sandbox/providers/vercel.ts +29 -0
- package/src/sandbox/recording.ts +286 -0
- package/src/sandbox/routing/backend-resolver.ts +189 -0
- package/src/sandbox/routing/routing-session.ts +455 -0
- package/src/sandbox/select.ts +371 -0
- package/src/sandbox/selfhosted/capabilities.ts +255 -0
- package/src/sandbox/selfhosted/control-rpc.ts +351 -0
- package/src/sandbox/selfhosted/session.ts +930 -0
- package/src/sandbox/selfhosted/testing.ts +230 -0
- package/src/sandbox/stream-port.ts +185 -0
- package/src/sandbox/stream-token.ts +90 -0
- package/src/sandbox/terminal-server.ts +203 -0
- package/src/sandbox-computer.ts +835 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3663 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActiveBackendUnresolvableError,
|
|
3
|
+
CAPABILITY_DESCRIPTORS,
|
|
4
|
+
ChannelAConflictError,
|
|
5
|
+
ChannelANotFoundError,
|
|
6
|
+
ChannelAUnsupportedError,
|
|
7
|
+
ChannelAValidationError,
|
|
8
|
+
DEFAULT_DESKTOP_GEOMETRY,
|
|
9
|
+
DESKTOP_STREAM_PORT,
|
|
10
|
+
DISPLAY_STACK_TIMEOUT_MS,
|
|
11
|
+
DisplayStackError,
|
|
12
|
+
DisplayStackUnsupportedError,
|
|
13
|
+
MockAgentResponder,
|
|
14
|
+
NatsControlRpc,
|
|
15
|
+
PROVIDER_REGISTRY,
|
|
16
|
+
RecordingError,
|
|
17
|
+
RecordingUnavailableError,
|
|
18
|
+
RoutingSandboxSession,
|
|
19
|
+
RoutingUnsupportedError,
|
|
20
|
+
SELFHOSTED_DEFAULT_TIMEOUT_MS,
|
|
21
|
+
SELFHOSTED_RECONNECT_WINDOW_MS,
|
|
22
|
+
SELFHOSTED_RELAY_STREAM_PATH,
|
|
23
|
+
STREAM_PORT,
|
|
24
|
+
STREAM_TOKEN_DEFAULT_TTL_SECONDS,
|
|
25
|
+
SandboxChannelAService,
|
|
26
|
+
SandboxConfigError,
|
|
27
|
+
SandboxProviderUnavailableError,
|
|
28
|
+
SelfhostedControlError,
|
|
29
|
+
SelfhostedSandboxClient,
|
|
30
|
+
SelfhostedSession,
|
|
31
|
+
StreamPortUnavailableError,
|
|
32
|
+
StreamTokenPayload,
|
|
33
|
+
TERMINAL_SERVER_TIMEOUT_MS,
|
|
34
|
+
TERMINAL_STREAM_PORT,
|
|
35
|
+
TerminalServerError,
|
|
36
|
+
TerminalServerUnsupportedError,
|
|
37
|
+
agentErrorToControlError,
|
|
38
|
+
assertDescriptorRegistryInvariants,
|
|
39
|
+
assertProviderRegistryInvariants,
|
|
40
|
+
assertSafeRelPath,
|
|
41
|
+
backendSupportsOs,
|
|
42
|
+
buildDisplayStackScript,
|
|
43
|
+
buildSelfhostedBackendSession,
|
|
44
|
+
buildStreamUrl,
|
|
45
|
+
buildTerminalServerScript,
|
|
46
|
+
collectSandboxEnvironment,
|
|
47
|
+
contentTypeForCodec,
|
|
48
|
+
createSandboxClient,
|
|
49
|
+
createSandboxClientForBackend,
|
|
50
|
+
decodeModalSnapshotId,
|
|
51
|
+
deletePriorPersistedSnapshot,
|
|
52
|
+
deleteRecordingArtifacts,
|
|
53
|
+
deserializeSandboxSessionStateEnvelope,
|
|
54
|
+
desktopCapableBackend,
|
|
55
|
+
ensureDisplayStack,
|
|
56
|
+
ensureTerminalServer,
|
|
57
|
+
establishSandboxSessionFromEnvelope,
|
|
58
|
+
exposeStreamPort,
|
|
59
|
+
extForCodec,
|
|
60
|
+
isExecSessionLostBanner,
|
|
61
|
+
isProviderSandboxNotFoundError,
|
|
62
|
+
isSelfhostedProviderNotFoundError,
|
|
63
|
+
isWorkspaceEscapeError,
|
|
64
|
+
makeActiveBackendResolver,
|
|
65
|
+
mintStreamToken,
|
|
66
|
+
negotiateCapabilities,
|
|
67
|
+
negotiateSelfhostedCapabilities,
|
|
68
|
+
offlineAgentError,
|
|
69
|
+
offlineControlResponse,
|
|
70
|
+
parseExecBannerSessionId,
|
|
71
|
+
parseExposedPorts,
|
|
72
|
+
parseNumstatZ,
|
|
73
|
+
parsePorcelainV2,
|
|
74
|
+
parseUnifiedPatch,
|
|
75
|
+
readRecordingBytes,
|
|
76
|
+
readWorkspaceArchiveFromEnvelopeSessionState,
|
|
77
|
+
recordingStorageKey,
|
|
78
|
+
restoredSandboxSessionStateFromEntry,
|
|
79
|
+
sandboxStateEntryFromRunState,
|
|
80
|
+
selectBackend,
|
|
81
|
+
selfhostedLiveness,
|
|
82
|
+
serializeEstablishedSandboxEnvelope,
|
|
83
|
+
setSelfhostedApplyDiff,
|
|
84
|
+
startRecording,
|
|
85
|
+
stopRecording,
|
|
86
|
+
stripExecBanner,
|
|
87
|
+
subjectFor,
|
|
88
|
+
tearDownDisplayStack,
|
|
89
|
+
tearDownTerminalServer,
|
|
90
|
+
timeoutAgentError,
|
|
91
|
+
timeoutControlResponse,
|
|
92
|
+
verifyStreamToken
|
|
93
|
+
} from "./chunk-2PO56VAL.js";
|
|
94
|
+
|
|
95
|
+
// src/index.ts
|
|
96
|
+
import { AGENT_INSTRUCTIONS_CORE_PLACEHOLDER, collectSandboxEnvironment as collectSandboxEnvironment2, contextServerCompactThreshold, firstPartyMcpBaseUrl, resolveContextCompactionMode, resolveModelProvider, sandboxLifecycleHookIds } from "@opengeni/config";
|
|
97
|
+
import { CAPABILITY_DESCRIPTORS as CAPABILITY_DESCRIPTORS2, isClearedRunStateBlob, signDelegatedAccessToken } from "@opengeni/contracts";
|
|
98
|
+
import {
|
|
99
|
+
Agent,
|
|
100
|
+
AgentsError,
|
|
101
|
+
connectMcpServers,
|
|
102
|
+
OpenAIProvider,
|
|
103
|
+
setDefaultModelProvider,
|
|
104
|
+
MaxTurnsExceededError,
|
|
105
|
+
MCPServerStreamableHttp,
|
|
106
|
+
OpenAIChatCompletionsModel,
|
|
107
|
+
OpenAIResponsesModel,
|
|
108
|
+
RunState,
|
|
109
|
+
isOpenAIResponsesRawModelStreamEvent,
|
|
110
|
+
Runner,
|
|
111
|
+
setDefaultOpenAIClient,
|
|
112
|
+
setDefaultOpenAIKey,
|
|
113
|
+
setOpenAIResponsesTransport,
|
|
114
|
+
webSearchTool,
|
|
115
|
+
applyDiff
|
|
116
|
+
} from "@openai/agents";
|
|
117
|
+
import {
|
|
118
|
+
localDirLazySkillSource
|
|
119
|
+
} from "@openai/agents/sandbox/local";
|
|
120
|
+
import {
|
|
121
|
+
Manifest,
|
|
122
|
+
SandboxAgent,
|
|
123
|
+
StaticCompactionPolicy,
|
|
124
|
+
azureBlobMount,
|
|
125
|
+
compaction,
|
|
126
|
+
dir,
|
|
127
|
+
file,
|
|
128
|
+
filesystem,
|
|
129
|
+
gitRepo,
|
|
130
|
+
inContainerMountStrategy,
|
|
131
|
+
localDir,
|
|
132
|
+
s3Mount,
|
|
133
|
+
shell,
|
|
134
|
+
skills
|
|
135
|
+
} from "@openai/agents/sandbox";
|
|
136
|
+
import { ModalCloudBucketMountStrategy } from "@openai/agents-extensions/sandbox/modal";
|
|
137
|
+
import OpenAI from "openai";
|
|
138
|
+
import { CODEX_APPS_MCP_SERVER_ID, CODEX_MODEL_ID_PREFIX, CODEX_ORIGINATOR, codexAppsSanitizingFetch, codexRequestStorage, codexSubscriptionFetch } from "@opengeni/codex";
|
|
139
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "fs";
|
|
140
|
+
import { dirname, isAbsolute, join, posix as posixPath, relative } from "path";
|
|
141
|
+
import { fileURLToPath } from "url";
|
|
142
|
+
|
|
143
|
+
// src/history-sanitizer.ts
|
|
144
|
+
var RESULT_TYPE_BY_CALL_TYPE = {
|
|
145
|
+
function_call: "function_call_result",
|
|
146
|
+
computer_call: "computer_call_result",
|
|
147
|
+
shell_call: "shell_call_output",
|
|
148
|
+
apply_patch_call: "apply_patch_call_output",
|
|
149
|
+
// Progressive connector disclosure (codex tool_search): a replayed
|
|
150
|
+
// `tool_search_call` must be settled by its `tool_search_output` exactly like a
|
|
151
|
+
// function call — an unpaired one 400s the store:false replay. The SDK pairs
|
|
152
|
+
// these OUTSIDE its own TOOL_CALL_RESULT_TYPE_BY_CALL_TYPE (sessionPersistence's
|
|
153
|
+
// hasToolSearchCallId), so we mirror the semantics here; the correlation id can
|
|
154
|
+
// additionally ride providerData (see callIdOf).
|
|
155
|
+
tool_search_call: "tool_search_output"
|
|
156
|
+
};
|
|
157
|
+
var RESULT_TYPES = new Set(Object.values(RESULT_TYPE_BY_CALL_TYPE));
|
|
158
|
+
function itemType(item) {
|
|
159
|
+
if (!item || typeof item !== "object") {
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
const type = item.type;
|
|
163
|
+
return typeof type === "string" ? type : void 0;
|
|
164
|
+
}
|
|
165
|
+
function callIdOf(item) {
|
|
166
|
+
if (!item || typeof item !== "object") {
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
const record = item;
|
|
170
|
+
if (typeof record.callId === "string" && record.callId.length > 0) {
|
|
171
|
+
return record.callId;
|
|
172
|
+
}
|
|
173
|
+
if (typeof record.call_id === "string" && record.call_id.length > 0) {
|
|
174
|
+
return record.call_id;
|
|
175
|
+
}
|
|
176
|
+
const provider = record.providerData;
|
|
177
|
+
if (provider && typeof provider === "object") {
|
|
178
|
+
if (typeof provider.call_id === "string" && provider.call_id.length > 0) {
|
|
179
|
+
return provider.call_id;
|
|
180
|
+
}
|
|
181
|
+
if (typeof provider.callId === "string" && provider.callId.length > 0) {
|
|
182
|
+
return provider.callId;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return void 0;
|
|
186
|
+
}
|
|
187
|
+
function sanitizeHistoryItemsForModel(items) {
|
|
188
|
+
if (items.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
192
|
+
const openCallIdsByResultType = /* @__PURE__ */ new Map();
|
|
193
|
+
items.forEach((item, index) => {
|
|
194
|
+
const type = itemType(item);
|
|
195
|
+
const callId = callIdOf(item);
|
|
196
|
+
if (!type || !callId) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const callResultType = RESULT_TYPE_BY_CALL_TYPE[type];
|
|
200
|
+
if (callResultType) {
|
|
201
|
+
const open = openCallIdsByResultType.get(callResultType) ?? /* @__PURE__ */ new Set();
|
|
202
|
+
open.add(callId);
|
|
203
|
+
openCallIdsByResultType.set(callResultType, open);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (RESULT_TYPES.has(type)) {
|
|
207
|
+
const open = openCallIdsByResultType.get(type);
|
|
208
|
+
if (open && open.has(callId)) {
|
|
209
|
+
open.delete(callId);
|
|
210
|
+
} else {
|
|
211
|
+
dropped.add(index);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
const stillOpen = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const [resultType, open] of openCallIdsByResultType) {
|
|
217
|
+
if (open.size > 0) {
|
|
218
|
+
stillOpen.set(resultType, new Set(open));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (stillOpen.size > 0) {
|
|
222
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
223
|
+
const item = items[index];
|
|
224
|
+
const type = itemType(item);
|
|
225
|
+
const callId = callIdOf(item);
|
|
226
|
+
if (!type || !callId) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const resultType = RESULT_TYPE_BY_CALL_TYPE[type];
|
|
230
|
+
if (!resultType) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const open = stillOpen.get(resultType);
|
|
234
|
+
if (open && open.has(callId)) {
|
|
235
|
+
dropped.add(index);
|
|
236
|
+
open.delete(callId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (dropped.size > 0) {
|
|
241
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
242
|
+
if (dropped.has(index) || itemType(items[index]) !== "reasoning") {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (let next = index + 1; next < items.length; next += 1) {
|
|
246
|
+
if (itemType(items[next]) === "reasoning") {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (dropped.has(next)) {
|
|
250
|
+
dropped.add(index);
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (dropped.size === 0) {
|
|
257
|
+
return items.slice();
|
|
258
|
+
}
|
|
259
|
+
return items.filter((_item, index) => !dropped.has(index));
|
|
260
|
+
}
|
|
261
|
+
function stripReasoningEncryptedContent(item) {
|
|
262
|
+
const type = itemType(item);
|
|
263
|
+
if (type !== "reasoning" && type !== "compaction") {
|
|
264
|
+
return item;
|
|
265
|
+
}
|
|
266
|
+
const record = item;
|
|
267
|
+
const providerData = record.providerData;
|
|
268
|
+
const providerHasBlob = !!providerData && typeof providerData === "object" && ("encryptedContent" in providerData || "encrypted_content" in providerData);
|
|
269
|
+
const topLevelHasBlob = "encrypted_content" in record;
|
|
270
|
+
if (!providerHasBlob && !topLevelHasBlob) {
|
|
271
|
+
return item;
|
|
272
|
+
}
|
|
273
|
+
const clone = { ...record };
|
|
274
|
+
if (providerHasBlob) {
|
|
275
|
+
const providerClone = { ...providerData };
|
|
276
|
+
delete providerClone.encryptedContent;
|
|
277
|
+
delete providerClone.encrypted_content;
|
|
278
|
+
clone.providerData = providerClone;
|
|
279
|
+
}
|
|
280
|
+
if (topLevelHasBlob) {
|
|
281
|
+
delete clone.encrypted_content;
|
|
282
|
+
}
|
|
283
|
+
return clone;
|
|
284
|
+
}
|
|
285
|
+
function stripReasoningIdentityFromSerializedRunState(serialized) {
|
|
286
|
+
let parsed;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(serialized);
|
|
289
|
+
} catch {
|
|
290
|
+
return serialized;
|
|
291
|
+
}
|
|
292
|
+
if (!parsed || typeof parsed !== "object") {
|
|
293
|
+
return serialized;
|
|
294
|
+
}
|
|
295
|
+
let changed = false;
|
|
296
|
+
const scrubReasoning = (candidate) => {
|
|
297
|
+
if (!candidate || typeof candidate !== "object") {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const record = candidate;
|
|
301
|
+
if (record.type !== "reasoning") {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if ("id" in record) {
|
|
305
|
+
delete record.id;
|
|
306
|
+
changed = true;
|
|
307
|
+
}
|
|
308
|
+
const providerData = record.providerData;
|
|
309
|
+
if (providerData && typeof providerData === "object") {
|
|
310
|
+
const provider = providerData;
|
|
311
|
+
if ("encryptedContent" in provider) {
|
|
312
|
+
delete provider.encryptedContent;
|
|
313
|
+
changed = true;
|
|
314
|
+
}
|
|
315
|
+
if ("encrypted_content" in provider) {
|
|
316
|
+
delete provider.encrypted_content;
|
|
317
|
+
changed = true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if ("encrypted_content" in record) {
|
|
321
|
+
delete record.encrypted_content;
|
|
322
|
+
changed = true;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const scrubItemArray = (arr) => {
|
|
326
|
+
if (Array.isArray(arr)) {
|
|
327
|
+
for (const item of arr) {
|
|
328
|
+
scrubReasoning(item);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const root = parsed;
|
|
333
|
+
scrubItemArray(root.originalInput);
|
|
334
|
+
if (Array.isArray(root.generatedItems)) {
|
|
335
|
+
for (const wrapper of root.generatedItems) {
|
|
336
|
+
if (wrapper && typeof wrapper === "object" && "rawItem" in wrapper) {
|
|
337
|
+
scrubReasoning(wrapper.rawItem);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const scrubResponseOutput = (response) => {
|
|
342
|
+
if (response && typeof response === "object") {
|
|
343
|
+
scrubItemArray(response.output);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
if (Array.isArray(root.modelResponses)) {
|
|
347
|
+
for (const response of root.modelResponses) {
|
|
348
|
+
scrubResponseOutput(response);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
scrubResponseOutput(root.lastModelResponse);
|
|
352
|
+
if (!changed) {
|
|
353
|
+
return serialized;
|
|
354
|
+
}
|
|
355
|
+
return JSON.stringify(parsed);
|
|
356
|
+
}
|
|
357
|
+
function neutralizeToolSearchItemsInSerializedRunState(serialized) {
|
|
358
|
+
let parsed;
|
|
359
|
+
try {
|
|
360
|
+
parsed = JSON.parse(serialized);
|
|
361
|
+
} catch {
|
|
362
|
+
return serialized;
|
|
363
|
+
}
|
|
364
|
+
if (!parsed || typeof parsed !== "object") {
|
|
365
|
+
return serialized;
|
|
366
|
+
}
|
|
367
|
+
let changed = false;
|
|
368
|
+
const neutralize = (candidate) => {
|
|
369
|
+
if (!candidate || typeof candidate !== "object") {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const record = candidate;
|
|
373
|
+
if (record.type !== "tool_search_call" && record.type !== "tool_search_output") {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (record.execution !== "server") {
|
|
377
|
+
record.execution = "server";
|
|
378
|
+
changed = true;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
const neutralizeArray = (arr) => {
|
|
382
|
+
if (Array.isArray(arr)) {
|
|
383
|
+
for (const item of arr) {
|
|
384
|
+
neutralize(item);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const root = parsed;
|
|
389
|
+
neutralizeArray(root.originalInput);
|
|
390
|
+
if (Array.isArray(root.generatedItems)) {
|
|
391
|
+
for (const wrapper of root.generatedItems) {
|
|
392
|
+
if (wrapper && typeof wrapper === "object" && "rawItem" in wrapper) {
|
|
393
|
+
neutralize(wrapper.rawItem);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const neutralizeResponseOutput = (response) => {
|
|
398
|
+
if (response && typeof response === "object") {
|
|
399
|
+
neutralizeArray(response.output);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
if (Array.isArray(root.modelResponses)) {
|
|
403
|
+
for (const response of root.modelResponses) {
|
|
404
|
+
neutralizeResponseOutput(response);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
neutralizeResponseOutput(root.lastModelResponse);
|
|
408
|
+
if (!changed) {
|
|
409
|
+
return serialized;
|
|
410
|
+
}
|
|
411
|
+
return JSON.stringify(parsed);
|
|
412
|
+
}
|
|
413
|
+
function normalizeComputerCallActions(items) {
|
|
414
|
+
let changed = false;
|
|
415
|
+
const out = items.map((item) => {
|
|
416
|
+
if (itemType(item) !== "computer_call") {
|
|
417
|
+
return item;
|
|
418
|
+
}
|
|
419
|
+
const record = item;
|
|
420
|
+
const hasAction = record.action !== void 0 && record.action !== null;
|
|
421
|
+
const hasActions = Array.isArray(record.actions) && record.actions.length > 0;
|
|
422
|
+
if (hasAction && hasActions) {
|
|
423
|
+
changed = true;
|
|
424
|
+
const { action: _droppedAction, ...rest } = record;
|
|
425
|
+
return rest;
|
|
426
|
+
}
|
|
427
|
+
return item;
|
|
428
|
+
});
|
|
429
|
+
return changed ? out : items.slice();
|
|
430
|
+
}
|
|
431
|
+
function rewriteComputerCallsToActionsOnly(body) {
|
|
432
|
+
if (!body || typeof body !== "object") {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
const input = body.input;
|
|
436
|
+
if (!Array.isArray(input)) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
let changed = false;
|
|
440
|
+
for (const item of input) {
|
|
441
|
+
if (!item || typeof item !== "object") {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const record = item;
|
|
445
|
+
if (record.type !== "computer_call") {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const existingActions = Array.isArray(record.actions) && record.actions.length > 0 ? record.actions : void 0;
|
|
449
|
+
const actions = existingActions ?? (record.action !== void 0 && record.action !== null ? [record.action] : void 0);
|
|
450
|
+
if (actions === void 0) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const hadAction = "action" in record;
|
|
454
|
+
const actionsAlreadyExact = existingActions !== void 0 && !hadAction;
|
|
455
|
+
if (actionsAlreadyExact) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
delete record.action;
|
|
459
|
+
record.actions = actions;
|
|
460
|
+
changed = true;
|
|
461
|
+
}
|
|
462
|
+
return changed;
|
|
463
|
+
}
|
|
464
|
+
var EMPTY_IMAGE_URL_PLACEHOLDER = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
|
|
465
|
+
function rewriteEmptyComputerCallOutputImageUrls(body) {
|
|
466
|
+
if (!body || typeof body !== "object") {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const input = body.input;
|
|
470
|
+
if (!Array.isArray(input)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
let changed = false;
|
|
474
|
+
for (const item of input) {
|
|
475
|
+
if (!item || typeof item !== "object") {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const record = item;
|
|
479
|
+
if (record.type !== "computer_call_output") {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const output = record.output;
|
|
483
|
+
if (!output || typeof output !== "object") {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const out = output;
|
|
487
|
+
const imageUrl = out.image_url;
|
|
488
|
+
if (typeof imageUrl !== "string" || imageUrl.length === 0) {
|
|
489
|
+
out.image_url = EMPTY_IMAGE_URL_PLACEHOLDER;
|
|
490
|
+
changed = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return changed;
|
|
494
|
+
}
|
|
495
|
+
function computerCallNormalizingFetch(base) {
|
|
496
|
+
return (input, init) => {
|
|
497
|
+
if (init && typeof init.body === "string" && init.body.includes('"computer_call')) {
|
|
498
|
+
try {
|
|
499
|
+
const parsed = JSON.parse(init.body);
|
|
500
|
+
const changed1 = rewriteComputerCallsToActionsOnly(parsed);
|
|
501
|
+
const changed2 = rewriteEmptyComputerCallOutputImageUrls(parsed);
|
|
502
|
+
if (changed1 || changed2) {
|
|
503
|
+
return base(input, { ...init, body: JSON.stringify(parsed) });
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return base(input, init);
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/codex-tool-search.ts
|
|
513
|
+
import { toolSearchTool } from "@openai/agents";
|
|
514
|
+
var CODEX_APPS_TOOL_PREFIX = "codex_apps__";
|
|
515
|
+
var DEFAULT_SEARCH_LIMIT = 8;
|
|
516
|
+
var MAX_SEARCH_LIMIT = 20;
|
|
517
|
+
function isCodexAppsFunctionTool(tool2) {
|
|
518
|
+
return !!tool2 && typeof tool2 === "object" && tool2.type === "function" && typeof tool2.name === "string" && tool2.name.startsWith(CODEX_APPS_TOOL_PREFIX);
|
|
519
|
+
}
|
|
520
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
521
|
+
"a",
|
|
522
|
+
"an",
|
|
523
|
+
"the",
|
|
524
|
+
"and",
|
|
525
|
+
"or",
|
|
526
|
+
"of",
|
|
527
|
+
"to",
|
|
528
|
+
"in",
|
|
529
|
+
"on",
|
|
530
|
+
"for",
|
|
531
|
+
"with",
|
|
532
|
+
"by",
|
|
533
|
+
"at",
|
|
534
|
+
"is",
|
|
535
|
+
"are",
|
|
536
|
+
"be",
|
|
537
|
+
"do",
|
|
538
|
+
"does",
|
|
539
|
+
"my",
|
|
540
|
+
"me",
|
|
541
|
+
"your",
|
|
542
|
+
"you",
|
|
543
|
+
"it",
|
|
544
|
+
"its",
|
|
545
|
+
"this",
|
|
546
|
+
"that",
|
|
547
|
+
"from",
|
|
548
|
+
"as",
|
|
549
|
+
"up",
|
|
550
|
+
"out",
|
|
551
|
+
"all",
|
|
552
|
+
"some",
|
|
553
|
+
"any",
|
|
554
|
+
"can",
|
|
555
|
+
"will",
|
|
556
|
+
"would",
|
|
557
|
+
"should",
|
|
558
|
+
"want",
|
|
559
|
+
"need",
|
|
560
|
+
"please",
|
|
561
|
+
"user",
|
|
562
|
+
"users"
|
|
563
|
+
]);
|
|
564
|
+
function stem(token) {
|
|
565
|
+
if (token.length > 5 && token.endsWith("ing")) return token.slice(0, -3);
|
|
566
|
+
if (token.length > 4 && token.endsWith("ed")) return token.slice(0, -2);
|
|
567
|
+
if (token.length > 4 && token.endsWith("es")) return token.slice(0, -2);
|
|
568
|
+
if (token.length > 3 && token.endsWith("s") && !token.endsWith("ss")) return token.slice(0, -1);
|
|
569
|
+
return token;
|
|
570
|
+
}
|
|
571
|
+
function tokenize(text) {
|
|
572
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOPWORDS.has(t)).map(stem);
|
|
573
|
+
}
|
|
574
|
+
function toolSearchText(tool2) {
|
|
575
|
+
const raw = tool2.name ?? "";
|
|
576
|
+
const name = raw.startsWith(CODEX_APPS_TOOL_PREFIX) ? raw.slice(CODEX_APPS_TOOL_PREFIX.length) : raw;
|
|
577
|
+
const description = typeof tool2.description === "string" ? tool2.description : "";
|
|
578
|
+
const params = tool2.parameters?.properties;
|
|
579
|
+
const paramNames = params && typeof params === "object" ? Object.keys(params).join(" ") : "";
|
|
580
|
+
return `${name} ${name} ${description} ${paramNames}`;
|
|
581
|
+
}
|
|
582
|
+
function bm25RankTools(tools, query, limit) {
|
|
583
|
+
const qTokens = Array.from(new Set(tokenize(query)));
|
|
584
|
+
if (tools.length === 0 || qTokens.length === 0) return [];
|
|
585
|
+
const docs = tools.map((tool2) => {
|
|
586
|
+
const tokens = tokenize(toolSearchText(tool2));
|
|
587
|
+
const tf = /* @__PURE__ */ new Map();
|
|
588
|
+
for (const t of tokens) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
589
|
+
return { tool: tool2, len: tokens.length, tf };
|
|
590
|
+
});
|
|
591
|
+
const N = docs.length;
|
|
592
|
+
const avgdl = Math.max(1, docs.reduce((s, d) => s + d.len, 0) / N);
|
|
593
|
+
const df = /* @__PURE__ */ new Map();
|
|
594
|
+
for (const d of docs) for (const t of d.tf.keys()) df.set(t, (df.get(t) ?? 0) + 1);
|
|
595
|
+
const k1 = 1.5;
|
|
596
|
+
const b = 0.75;
|
|
597
|
+
const scored = docs.map((d) => {
|
|
598
|
+
let score = 0;
|
|
599
|
+
for (const qt of qTokens) {
|
|
600
|
+
const n = df.get(qt);
|
|
601
|
+
const f = d.tf.get(qt);
|
|
602
|
+
if (!n || !f) continue;
|
|
603
|
+
const idf = Math.log(1 + (N - n + 0.5) / (n + 0.5));
|
|
604
|
+
score += idf * (f * (k1 + 1) / (f + k1 * (1 - b + b * d.len / avgdl)));
|
|
605
|
+
}
|
|
606
|
+
return { tool: d.tool, score };
|
|
607
|
+
});
|
|
608
|
+
const hits = scored.filter((s) => s.score > 0).sort((a, b2) => b2.score - a.score);
|
|
609
|
+
return hits.slice(0, limit).map((s) => s.tool);
|
|
610
|
+
}
|
|
611
|
+
function parseSearchArgs(raw) {
|
|
612
|
+
let obj = {};
|
|
613
|
+
try {
|
|
614
|
+
obj = typeof raw === "string" ? raw.length ? JSON.parse(raw) : {} : raw && typeof raw === "object" ? raw : {};
|
|
615
|
+
} catch {
|
|
616
|
+
obj = {};
|
|
617
|
+
}
|
|
618
|
+
const query = typeof obj.query === "string" ? obj.query : "";
|
|
619
|
+
const limitRaw = typeof obj.limit === "number" && Number.isFinite(obj.limit) ? obj.limit : DEFAULT_SEARCH_LIMIT;
|
|
620
|
+
return { query, limit: Math.max(1, Math.min(MAX_SEARCH_LIMIT, Math.round(limitRaw))) };
|
|
621
|
+
}
|
|
622
|
+
function renderSearchToolDescription(connectorNamespaces) {
|
|
623
|
+
const base = `Search the user's connected app tools by capability. Describe in plain language WHAT you need to do (for example: "send an email", "create a calendar event") rather than guessing exact tool names. Returns the matching connector tools, which then become callable.`;
|
|
624
|
+
const sources = Array.from(connectorNamespaces).filter((n) => typeof n === "string" && n.length > 0).sort();
|
|
625
|
+
if (sources.length === 0) {
|
|
626
|
+
return `${base}
|
|
627
|
+
Connected sources: none currently available.`;
|
|
628
|
+
}
|
|
629
|
+
return `${base}
|
|
630
|
+
You have access to tools from the following sources:
|
|
631
|
+
${sources.map((s) => `- ${s}`).join("\n")}`;
|
|
632
|
+
}
|
|
633
|
+
var SEARCH_TOOL_PARAMETERS = {
|
|
634
|
+
type: "object",
|
|
635
|
+
properties: {
|
|
636
|
+
query: { type: "string", description: "Plain-language description of the capability you need." },
|
|
637
|
+
limit: { type: "number", description: "Maximum number of tools to return (default 8)." }
|
|
638
|
+
},
|
|
639
|
+
required: ["query"],
|
|
640
|
+
additionalProperties: false
|
|
641
|
+
};
|
|
642
|
+
function codexToolSearchExecutor(args) {
|
|
643
|
+
const deferred = (args.availableTools ?? []).filter(isCodexAppsFunctionTool);
|
|
644
|
+
if (deferred.length === 0) return [];
|
|
645
|
+
const { query, limit } = parseSearchArgs(args.toolCall?.arguments);
|
|
646
|
+
return bm25RankTools(deferred, query, limit);
|
|
647
|
+
}
|
|
648
|
+
var NO_NAMESPACES = /* @__PURE__ */ new Set();
|
|
649
|
+
function buildCodexToolSearchTool(connectorNamespaces = NO_NAMESPACES) {
|
|
650
|
+
return toolSearchTool({
|
|
651
|
+
execution: "client",
|
|
652
|
+
description: renderSearchToolDescription(connectorNamespaces),
|
|
653
|
+
parameters: SEARCH_TOOL_PARAMETERS,
|
|
654
|
+
execute: codexToolSearchExecutor
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function isToolSearchTool(tool2) {
|
|
658
|
+
const t = tool2;
|
|
659
|
+
return !!t && (t.name === "tool_search" || t.providerData?.type === "tool_search");
|
|
660
|
+
}
|
|
661
|
+
function applyCodexToolSearch(tools, connectorNamespaces = NO_NAMESPACES) {
|
|
662
|
+
for (const tool2 of tools) {
|
|
663
|
+
if (isCodexAppsFunctionTool(tool2) && tool2.deferLoading !== true) {
|
|
664
|
+
tool2.deferLoading = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (tools.some(isToolSearchTool)) {
|
|
668
|
+
return tools;
|
|
669
|
+
}
|
|
670
|
+
return [...tools, buildCodexToolSearchTool(connectorNamespaces)];
|
|
671
|
+
}
|
|
672
|
+
function installCodexToolSearch(agent, connectorNamespaces = NO_NAMESPACES) {
|
|
673
|
+
const originalGetAllTools = agent.getAllTools.bind(agent);
|
|
674
|
+
agent.getAllTools = (async (runContext) => applyCodexToolSearch(await originalGetAllTools(runContext), connectorNamespaces));
|
|
675
|
+
const originalClone = agent.clone?.bind(agent);
|
|
676
|
+
if (originalClone) {
|
|
677
|
+
const cloneWithToolSearch = (config) => {
|
|
678
|
+
const cloned = originalClone(config);
|
|
679
|
+
installCodexToolSearch(cloned, connectorNamespaces);
|
|
680
|
+
return cloned;
|
|
681
|
+
};
|
|
682
|
+
agent.clone = cloneWithToolSearch;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/context-compaction.ts
|
|
687
|
+
var COMPACTION_SUMMARY_MARKER = "opengeni_context_summary";
|
|
688
|
+
var SUMMARY_PREFIX = [
|
|
689
|
+
"[CONTEXT CHECKPOINT] The earlier part of this conversation was automatically compacted to stay within the model context window.",
|
|
690
|
+
"Durable facts already live in the workspace notebook / document bases (via MCP) \u2014 the summary below is a light working-memory bridge, not a full transcript.",
|
|
691
|
+
"Trust it for current objective, decisions, blockers, deployed/infra state, and next steps; re-read the notebook for anything authoritative.",
|
|
692
|
+
"",
|
|
693
|
+
"SUMMARY:"
|
|
694
|
+
].join("\n");
|
|
695
|
+
var RESULT_TYPE_BY_CALL_TYPE2 = {
|
|
696
|
+
function_call: "function_call_result",
|
|
697
|
+
computer_call: "computer_call_result",
|
|
698
|
+
shell_call: "shell_call_output",
|
|
699
|
+
apply_patch_call: "apply_patch_call_output"
|
|
700
|
+
};
|
|
701
|
+
var RESULT_TYPES2 = new Set(Object.values(RESULT_TYPE_BY_CALL_TYPE2));
|
|
702
|
+
function itemType2(item) {
|
|
703
|
+
if (!item || typeof item !== "object") {
|
|
704
|
+
return void 0;
|
|
705
|
+
}
|
|
706
|
+
const type = item.type;
|
|
707
|
+
return typeof type === "string" ? type : void 0;
|
|
708
|
+
}
|
|
709
|
+
function itemRole(item) {
|
|
710
|
+
if (!item || typeof item !== "object") {
|
|
711
|
+
return void 0;
|
|
712
|
+
}
|
|
713
|
+
const role = item.role;
|
|
714
|
+
return typeof role === "string" ? role : void 0;
|
|
715
|
+
}
|
|
716
|
+
function isUserMessage(item) {
|
|
717
|
+
return itemType2(item) === "message" && itemRole(item) === "user";
|
|
718
|
+
}
|
|
719
|
+
function isCompactionSummary(item) {
|
|
720
|
+
return isUserMessage(item) && item[COMPACTION_SUMMARY_MARKER] === true;
|
|
721
|
+
}
|
|
722
|
+
function estimateItemTokens(item) {
|
|
723
|
+
let text;
|
|
724
|
+
try {
|
|
725
|
+
text = JSON.stringify(item);
|
|
726
|
+
} catch {
|
|
727
|
+
text = String(item);
|
|
728
|
+
}
|
|
729
|
+
return Math.ceil(text.length / 4);
|
|
730
|
+
}
|
|
731
|
+
function estimateTokens(items) {
|
|
732
|
+
let total = 0;
|
|
733
|
+
for (const item of items) {
|
|
734
|
+
total += estimateItemTokens(item);
|
|
735
|
+
}
|
|
736
|
+
return total;
|
|
737
|
+
}
|
|
738
|
+
function findKeepBoundary(items, keepRecentTokens) {
|
|
739
|
+
const boundaries = [];
|
|
740
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
741
|
+
if (isUserMessage(items[i])) {
|
|
742
|
+
boundaries.push(i);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (boundaries.length === 0) {
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
for (let b = 0; b < boundaries.length; b += 1) {
|
|
749
|
+
const boundary = boundaries[b];
|
|
750
|
+
if (estimateTokens(items.slice(boundary)) <= keepRecentTokens) {
|
|
751
|
+
return boundary;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return boundaries[boundaries.length - 1];
|
|
755
|
+
}
|
|
756
|
+
function enforceInputBudget(items, maxTokens, trailingTokens = 0) {
|
|
757
|
+
const total = estimateTokens(items) + Math.max(0, trailingTokens);
|
|
758
|
+
if (items.length === 0 || total <= maxTokens) {
|
|
759
|
+
return { items: items.slice(), trimmed: false, droppedCount: 0, estimatedTokens: total };
|
|
760
|
+
}
|
|
761
|
+
const historyBudget = Math.max(0, maxTokens - Math.max(0, trailingTokens));
|
|
762
|
+
const boundary = findKeepBoundary(items, historyBudget);
|
|
763
|
+
if (boundary <= 0) {
|
|
764
|
+
return { items: items.slice(), trimmed: false, droppedCount: 0, estimatedTokens: total };
|
|
765
|
+
}
|
|
766
|
+
const kept = items.slice(boundary);
|
|
767
|
+
return {
|
|
768
|
+
items: kept,
|
|
769
|
+
trimmed: true,
|
|
770
|
+
droppedCount: boundary,
|
|
771
|
+
estimatedTokens: estimateTokens(kept) + Math.max(0, trailingTokens)
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function planCompaction(input) {
|
|
775
|
+
const softLimit = Math.floor(input.inputBudgetTokens * input.softFraction);
|
|
776
|
+
const hardLimit = Math.floor(input.inputBudgetTokens * input.hardFraction);
|
|
777
|
+
const recorded = typeof input.lastInputTokens === "number" && input.lastInputTokens > 0 ? input.lastInputTokens : 0;
|
|
778
|
+
const signalTokens = Math.max(recorded, estimateTokens(input.items));
|
|
779
|
+
const hardForced = signalTokens >= hardLimit;
|
|
780
|
+
const empty = {
|
|
781
|
+
shouldCompact: false,
|
|
782
|
+
reason: "below_threshold",
|
|
783
|
+
signalTokens,
|
|
784
|
+
hardForced,
|
|
785
|
+
boundaryIndex: input.items.length,
|
|
786
|
+
prefixItems: [],
|
|
787
|
+
priorSummaryItem: null,
|
|
788
|
+
tailItems: [...input.items]
|
|
789
|
+
};
|
|
790
|
+
if (!input.force && signalTokens < softLimit) {
|
|
791
|
+
return empty;
|
|
792
|
+
}
|
|
793
|
+
const effectiveKeepRecent = hardForced ? Math.min(
|
|
794
|
+
Math.floor(input.keepRecentTokens / 2),
|
|
795
|
+
Math.floor(input.inputBudgetTokens / 4)
|
|
796
|
+
) : input.keepRecentTokens;
|
|
797
|
+
const boundaryIndex = findKeepBoundary(input.items, effectiveKeepRecent);
|
|
798
|
+
if (boundaryIndex <= 0) {
|
|
799
|
+
return { ...empty, reason: "no_boundary", boundaryIndex };
|
|
800
|
+
}
|
|
801
|
+
const prefix = input.items.slice(0, boundaryIndex);
|
|
802
|
+
const tailItems = input.items.slice(boundaryIndex);
|
|
803
|
+
let priorSummaryItem = null;
|
|
804
|
+
const prefixItems = [];
|
|
805
|
+
for (const item of prefix) {
|
|
806
|
+
if (isCompactionSummary(item)) {
|
|
807
|
+
priorSummaryItem = item;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
prefixItems.push(item);
|
|
811
|
+
}
|
|
812
|
+
if (prefixItems.length === 0) {
|
|
813
|
+
return { ...empty, reason: "nothing_to_summarize", boundaryIndex };
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
shouldCompact: true,
|
|
817
|
+
reason: "compact",
|
|
818
|
+
signalTokens,
|
|
819
|
+
hardForced,
|
|
820
|
+
boundaryIndex,
|
|
821
|
+
prefixItems,
|
|
822
|
+
priorSummaryItem,
|
|
823
|
+
tailItems
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function compactionSummaryText(item) {
|
|
827
|
+
if (!item) {
|
|
828
|
+
return "";
|
|
829
|
+
}
|
|
830
|
+
const content = item.content;
|
|
831
|
+
if (typeof content === "string") {
|
|
832
|
+
return stripSummaryPrefix(content);
|
|
833
|
+
}
|
|
834
|
+
if (Array.isArray(content)) {
|
|
835
|
+
const text = content.map((part) => {
|
|
836
|
+
if (part && typeof part === "object") {
|
|
837
|
+
const t = part.text;
|
|
838
|
+
return typeof t === "string" ? t : "";
|
|
839
|
+
}
|
|
840
|
+
return "";
|
|
841
|
+
}).join("");
|
|
842
|
+
return stripSummaryPrefix(text);
|
|
843
|
+
}
|
|
844
|
+
return "";
|
|
845
|
+
}
|
|
846
|
+
function stripSummaryPrefix(text) {
|
|
847
|
+
const marker = "SUMMARY:";
|
|
848
|
+
const idx = text.indexOf(marker);
|
|
849
|
+
return idx >= 0 ? text.slice(idx + marker.length) : text;
|
|
850
|
+
}
|
|
851
|
+
function buildSummaryItem(summaryBody) {
|
|
852
|
+
return {
|
|
853
|
+
type: "message",
|
|
854
|
+
role: "user",
|
|
855
|
+
content: `${SUMMARY_PREFIX}${summaryBody}`,
|
|
856
|
+
[COMPACTION_SUMMARY_MARKER]: true
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
var SUMMARY_INSTRUCTIONS = [
|
|
860
|
+
"You are compacting the earlier part of a long-running agent conversation into a compact working-memory checkpoint so the agent can continue past the model's context limit.",
|
|
861
|
+
"Durable facts already live in the workspace notebook and document bases (via MCP). Do NOT re-derive or copy those; summarize POINTERS, not contents.",
|
|
862
|
+
"Capture, concisely and factually:",
|
|
863
|
+
"- The current objective and the key decisions made so far.",
|
|
864
|
+
"- Open blockers and anything in-progress.",
|
|
865
|
+
"- Deployed / infrastructure state that has changed (what exists now).",
|
|
866
|
+
"- Environment and credential facts BY REFERENCE ONLY \u2014 name the env var keys, secret names, or notebook/document ids; NEVER copy a secret value, token, key, or password.",
|
|
867
|
+
"- Concrete next steps.",
|
|
868
|
+
"Say explicitly that durable facts are in the notebook and that this summary lists pointers, not contents.",
|
|
869
|
+
"Output only the summary body \u2014 no preamble, no markdown headers, plain prose or terse bullets."
|
|
870
|
+
].join("\n");
|
|
871
|
+
function renderPrefixTranscript(items, priorSummaryText) {
|
|
872
|
+
const lines = [];
|
|
873
|
+
if (priorSummaryText.trim().length > 0) {
|
|
874
|
+
lines.push("PRIOR CHECKPOINT SUMMARY (fold this forward; it already replaced even older history):");
|
|
875
|
+
lines.push(priorSummaryText.trim());
|
|
876
|
+
lines.push("");
|
|
877
|
+
lines.push("CONVERSATION SINCE THAT CHECKPOINT:");
|
|
878
|
+
} else {
|
|
879
|
+
lines.push("CONVERSATION TO SUMMARIZE:");
|
|
880
|
+
}
|
|
881
|
+
for (const item of items) {
|
|
882
|
+
lines.push(renderItem(item));
|
|
883
|
+
}
|
|
884
|
+
return lines.join("\n");
|
|
885
|
+
}
|
|
886
|
+
function renderItem(item) {
|
|
887
|
+
const type = itemType2(item) ?? "unknown";
|
|
888
|
+
if (type === "message") {
|
|
889
|
+
const role = itemRole(item) ?? "assistant";
|
|
890
|
+
return `[${role}] ${truncate(messageText(item), 4e3)}`;
|
|
891
|
+
}
|
|
892
|
+
if (type === "reasoning") {
|
|
893
|
+
return "[reasoning] (omitted)";
|
|
894
|
+
}
|
|
895
|
+
if (RESULT_TYPES2.has(type)) {
|
|
896
|
+
return `[tool_result] ${truncate(resultText(item), 2e3)}`;
|
|
897
|
+
}
|
|
898
|
+
if (RESULT_TYPE_BY_CALL_TYPE2[type]) {
|
|
899
|
+
return `[tool_call ${type}] ${truncate(callText(item), 1e3)}`;
|
|
900
|
+
}
|
|
901
|
+
return `[${type}] ${truncate(safeStringify(item), 1e3)}`;
|
|
902
|
+
}
|
|
903
|
+
function messageText(item) {
|
|
904
|
+
const content = item.content;
|
|
905
|
+
if (typeof content === "string") {
|
|
906
|
+
return content;
|
|
907
|
+
}
|
|
908
|
+
if (Array.isArray(content)) {
|
|
909
|
+
return content.map((part) => {
|
|
910
|
+
if (part && typeof part === "object") {
|
|
911
|
+
const t = part.text;
|
|
912
|
+
return typeof t === "string" ? t : "";
|
|
913
|
+
}
|
|
914
|
+
return "";
|
|
915
|
+
}).join("");
|
|
916
|
+
}
|
|
917
|
+
return "";
|
|
918
|
+
}
|
|
919
|
+
function resultText(item) {
|
|
920
|
+
const output = item.output;
|
|
921
|
+
if (typeof output === "string") {
|
|
922
|
+
return output;
|
|
923
|
+
}
|
|
924
|
+
return safeStringify(output ?? item);
|
|
925
|
+
}
|
|
926
|
+
function callText(item) {
|
|
927
|
+
const name = item.name;
|
|
928
|
+
const args = item.arguments;
|
|
929
|
+
const namePart = typeof name === "string" ? name : "";
|
|
930
|
+
const argPart = typeof args === "string" ? args : safeStringify(args ?? {});
|
|
931
|
+
return `${namePart} ${argPart}`.trim();
|
|
932
|
+
}
|
|
933
|
+
function safeStringify(value) {
|
|
934
|
+
try {
|
|
935
|
+
return JSON.stringify(value);
|
|
936
|
+
} catch {
|
|
937
|
+
return String(value);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function truncate(text, max) {
|
|
941
|
+
if (text.length <= max) {
|
|
942
|
+
return text;
|
|
943
|
+
}
|
|
944
|
+
return `${text.slice(0, max)}\u2026 (${text.length - max} more chars)`;
|
|
945
|
+
}
|
|
946
|
+
function buildCompactionMessages(plan) {
|
|
947
|
+
const priorText = compactionSummaryText(plan.priorSummaryItem);
|
|
948
|
+
return {
|
|
949
|
+
system: SUMMARY_INSTRUCTIONS,
|
|
950
|
+
user: renderPrefixTranscript(plan.prefixItems, priorText)
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/sandbox-computer.ts
|
|
955
|
+
import { computerTool, tool } from "@openai/agents";
|
|
956
|
+
import { Capability } from "@openai/agents/sandbox";
|
|
957
|
+
import { KeyAction, PointerAction, PointerButton } from "@opengeni/agent-proto";
|
|
958
|
+
function requireBoundSession(capabilityType, session) {
|
|
959
|
+
if (!session) {
|
|
960
|
+
throw new ComputerUnavailableError(`capability "${capabilityType}" used before bind(session)`);
|
|
961
|
+
}
|
|
962
|
+
return session;
|
|
963
|
+
}
|
|
964
|
+
var DEFAULT_DISPLAY = ":0";
|
|
965
|
+
var DEFAULT_DIMENSIONS = [1280, 800];
|
|
966
|
+
var ACTION_YIELD_MS = 15e3;
|
|
967
|
+
var SCROLL_NOTCH_PIXELS = 100;
|
|
968
|
+
var SCROLL_MAX_CLICKS = 15;
|
|
969
|
+
var SCREENSHOT_MAX_ATTEMPTS = 3;
|
|
970
|
+
var SCREENSHOT_RETRY_DELAY_MS = 400;
|
|
971
|
+
var KEYSYM = {
|
|
972
|
+
ctrl: "ctrl",
|
|
973
|
+
control: "ctrl",
|
|
974
|
+
alt: "alt",
|
|
975
|
+
option: "alt",
|
|
976
|
+
shift: "shift",
|
|
977
|
+
cmd: "super",
|
|
978
|
+
meta: "super",
|
|
979
|
+
win: "super",
|
|
980
|
+
super: "super",
|
|
981
|
+
enter: "Return",
|
|
982
|
+
return: "Return",
|
|
983
|
+
tab: "Tab",
|
|
984
|
+
esc: "Escape",
|
|
985
|
+
escape: "Escape",
|
|
986
|
+
backspace: "BackSpace",
|
|
987
|
+
delete: "Delete",
|
|
988
|
+
space: "space",
|
|
989
|
+
up: "Up",
|
|
990
|
+
down: "Down",
|
|
991
|
+
left: "Left",
|
|
992
|
+
right: "Right",
|
|
993
|
+
pageup: "Prior",
|
|
994
|
+
pagedown: "Next",
|
|
995
|
+
home: "Home",
|
|
996
|
+
end: "End"
|
|
997
|
+
};
|
|
998
|
+
function toKeysym(k) {
|
|
999
|
+
const low = k.toLowerCase();
|
|
1000
|
+
if (KEYSYM[low]) return KEYSYM[low];
|
|
1001
|
+
if (/^f([1-9]|1[0-2])$/.test(low)) return low.toUpperCase();
|
|
1002
|
+
return low.length === 1 ? low : k;
|
|
1003
|
+
}
|
|
1004
|
+
var BUTTON_NUM = { left: 1, wheel: 2, right: 3, back: 8, forward: 9 };
|
|
1005
|
+
var ComputerUnavailableError = class extends Error {
|
|
1006
|
+
constructor(message) {
|
|
1007
|
+
super(message);
|
|
1008
|
+
this.name = "ComputerUnavailableError";
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
var ComputerReadOnlyError = class extends Error {
|
|
1012
|
+
constructor() {
|
|
1013
|
+
super("computer-use is read-only \u2014 write actions are disabled");
|
|
1014
|
+
this.name = "ComputerReadOnlyError";
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
var ComputerActionError = class extends Error {
|
|
1018
|
+
constructor(cmd, exitCode, stderr) {
|
|
1019
|
+
super(`computer action failed (${exitCode}): ${cmd}${stderr ? `
|
|
1020
|
+
${stderr}` : ""}`);
|
|
1021
|
+
this.cmd = cmd;
|
|
1022
|
+
this.exitCode = exitCode;
|
|
1023
|
+
this.stderr = stderr;
|
|
1024
|
+
this.name = "ComputerActionError";
|
|
1025
|
+
}
|
|
1026
|
+
cmd;
|
|
1027
|
+
exitCode;
|
|
1028
|
+
stderr;
|
|
1029
|
+
};
|
|
1030
|
+
var SandboxComputer = class {
|
|
1031
|
+
environment = "ubuntu";
|
|
1032
|
+
dimensions;
|
|
1033
|
+
session;
|
|
1034
|
+
display;
|
|
1035
|
+
runAs;
|
|
1036
|
+
typeDelayMs;
|
|
1037
|
+
readOnly;
|
|
1038
|
+
tmp;
|
|
1039
|
+
constructor(session, opts = {}) {
|
|
1040
|
+
this.session = session;
|
|
1041
|
+
this.display = opts.display ?? DEFAULT_DISPLAY;
|
|
1042
|
+
this.dimensions = opts.dimensions ?? DEFAULT_DIMENSIONS;
|
|
1043
|
+
if (opts.runAs !== void 0) {
|
|
1044
|
+
this.runAs = opts.runAs;
|
|
1045
|
+
}
|
|
1046
|
+
this.typeDelayMs = opts.typeDelayMs ?? 12;
|
|
1047
|
+
this.readOnly = opts.readOnly ?? false;
|
|
1048
|
+
this.tmp = opts.screenshotTmpDir ?? "/tmp";
|
|
1049
|
+
}
|
|
1050
|
+
/** Rebind to a freshly resumed-by-id session after a box rollover / re-establish. */
|
|
1051
|
+
rebind(session) {
|
|
1052
|
+
this.session = session;
|
|
1053
|
+
}
|
|
1054
|
+
// The single command primitive. Dual-paths exec/execCommand (F1), then uses the
|
|
1055
|
+
// established string-aware parsers (F2/F3): exitCode from the preamble, and
|
|
1056
|
+
// "still running" → a retriable failure. Returns the command OUTPUT body.
|
|
1057
|
+
async x(cmd) {
|
|
1058
|
+
const args = {
|
|
1059
|
+
cmd: `DISPLAY=${this.display} ${cmd}`,
|
|
1060
|
+
...this.runAs ? { runAs: this.runAs } : {},
|
|
1061
|
+
yieldTimeMs: ACTION_YIELD_MS,
|
|
1062
|
+
maxOutputTokens: 4e3
|
|
1063
|
+
};
|
|
1064
|
+
let result;
|
|
1065
|
+
if (typeof this.session.exec === "function") {
|
|
1066
|
+
result = await this.session.exec(args);
|
|
1067
|
+
} else if (typeof this.session.execCommand === "function") {
|
|
1068
|
+
result = await this.session.execCommand(args);
|
|
1069
|
+
} else {
|
|
1070
|
+
throw new ComputerUnavailableError("session cannot run commands (no exec/execCommand)");
|
|
1071
|
+
}
|
|
1072
|
+
const output = sandboxCommandOutput(result);
|
|
1073
|
+
if (sandboxCommandStillRunning(result)) {
|
|
1074
|
+
console.warn(
|
|
1075
|
+
`[SandboxComputer] action command did not finish before the ${ACTION_YIELD_MS}ms yield window \u2014 proceeding to screenshot: ${cmd}`
|
|
1076
|
+
);
|
|
1077
|
+
return output;
|
|
1078
|
+
}
|
|
1079
|
+
const exitCode = sandboxCommandExitCode(result);
|
|
1080
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
1081
|
+
throw new ComputerActionError(cmd, exitCode, output);
|
|
1082
|
+
}
|
|
1083
|
+
return output;
|
|
1084
|
+
}
|
|
1085
|
+
guardWrite() {
|
|
1086
|
+
if (this.readOnly) throw new ComputerReadOnlyError();
|
|
1087
|
+
}
|
|
1088
|
+
shq(s) {
|
|
1089
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1090
|
+
}
|
|
1091
|
+
async screenshot() {
|
|
1092
|
+
let lastError;
|
|
1093
|
+
for (let attempt = 0; attempt < SCREENSHOT_MAX_ATTEMPTS; attempt++) {
|
|
1094
|
+
if (attempt > 0) {
|
|
1095
|
+
await new Promise((r) => setTimeout(r, SCREENSHOT_RETRY_DELAY_MS));
|
|
1096
|
+
}
|
|
1097
|
+
const f = `${this.tmp}/og-shot-${Date.now()}-${Math.random().toString(36).slice(2)}.png`;
|
|
1098
|
+
try {
|
|
1099
|
+
await this.x(`scrot --pointer --overwrite ${f}`);
|
|
1100
|
+
const bytes = await this.readScreenshotBytes(f);
|
|
1101
|
+
if (bytes.length === 0) {
|
|
1102
|
+
throw new ComputerUnavailableError("scrot produced an empty screenshot (display not up?)");
|
|
1103
|
+
}
|
|
1104
|
+
return Buffer.from(bytes).toString("base64");
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
lastError = error;
|
|
1107
|
+
} finally {
|
|
1108
|
+
await this.x(`rm -f ${f}`).catch(() => void 0);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (lastError instanceof Error) {
|
|
1112
|
+
throw lastError;
|
|
1113
|
+
}
|
|
1114
|
+
throw new ComputerUnavailableError("scrot produced an empty screenshot (display not up?)");
|
|
1115
|
+
}
|
|
1116
|
+
// Read the screenshot PNG bytes by base64-ing the absolute /tmp path through the
|
|
1117
|
+
// SAME command primitive (exec ?? execCommand) — NOT `session.readFile` (Modal
|
|
1118
|
+
// path-validates against /workspace and rejects /tmp) and NOT `this.x()` (its
|
|
1119
|
+
// `sandboxCommandOutput` parser drops the execCommand STRING body, returning ""
|
|
1120
|
+
// — only the exec-object path has a structured body). We capture the RAW result,
|
|
1121
|
+
// strip the execCommand banner ("…Output:\n<base64>"), strip whitespace, and
|
|
1122
|
+
// decode. Binary-safe: base64 of the scrot is plain ASCII over stdout, no
|
|
1123
|
+
// truncation (maxOutputTokens:null), mirroring recording.ts readRecordingBytes.
|
|
1124
|
+
async readScreenshotBytes(path) {
|
|
1125
|
+
const args = {
|
|
1126
|
+
cmd: `DISPLAY=${this.display} base64 ${path}`,
|
|
1127
|
+
...this.runAs ? { runAs: this.runAs } : {},
|
|
1128
|
+
yieldTimeMs: ACTION_YIELD_MS,
|
|
1129
|
+
// null disables the provider's output truncation so a full-screen PNG's
|
|
1130
|
+
// base64 is never clipped (the SDK's truncateOutput passes through on null).
|
|
1131
|
+
maxOutputTokens: null
|
|
1132
|
+
};
|
|
1133
|
+
let raw;
|
|
1134
|
+
if (typeof this.session.exec === "function") {
|
|
1135
|
+
raw = sandboxCommandOutput(await this.session.exec(args));
|
|
1136
|
+
} else if (typeof this.session.execCommand === "function") {
|
|
1137
|
+
raw = stripExecBanner(await this.session.execCommand(args));
|
|
1138
|
+
} else {
|
|
1139
|
+
throw new ComputerUnavailableError("session cannot run commands (no exec/execCommand) \u2014 screenshots unavailable");
|
|
1140
|
+
}
|
|
1141
|
+
const b64 = raw.replace(/\s+/g, "");
|
|
1142
|
+
if (b64.length === 0) return new Uint8Array();
|
|
1143
|
+
return Uint8Array.from(Buffer.from(b64, "base64"));
|
|
1144
|
+
}
|
|
1145
|
+
async click(xp, yp, button) {
|
|
1146
|
+
this.guardWrite();
|
|
1147
|
+
await this.x(`xdotool mousemove --sync ${xp} ${yp} click ${BUTTON_NUM[button] ?? 1}`);
|
|
1148
|
+
}
|
|
1149
|
+
async doubleClick(xp, yp) {
|
|
1150
|
+
this.guardWrite();
|
|
1151
|
+
await this.x(`xdotool mousemove --sync ${xp} ${yp} click --repeat 2 --delay 60 1`);
|
|
1152
|
+
}
|
|
1153
|
+
async move(xp, yp) {
|
|
1154
|
+
this.guardWrite();
|
|
1155
|
+
await this.x(`xdotool mousemove --sync ${xp} ${yp}`);
|
|
1156
|
+
}
|
|
1157
|
+
async scroll(xp, yp, sx, sy) {
|
|
1158
|
+
this.guardWrite();
|
|
1159
|
+
const notches = (px) => Math.min(SCROLL_MAX_CLICKS, Math.max(0, Math.round(Math.abs(px) / SCROLL_NOTCH_PIXELS)));
|
|
1160
|
+
const vBtn = sy < 0 ? 4 : 5;
|
|
1161
|
+
const hBtn = sx < 0 ? 6 : 7;
|
|
1162
|
+
const vN = notches(sy);
|
|
1163
|
+
const hN = notches(sx);
|
|
1164
|
+
let cmd = `xdotool mousemove --sync ${xp} ${yp}`;
|
|
1165
|
+
if (vN) cmd += ` click --repeat ${vN} ${vBtn}`;
|
|
1166
|
+
if (hN) cmd += ` click --repeat ${hN} ${hBtn}`;
|
|
1167
|
+
await this.x(cmd);
|
|
1168
|
+
}
|
|
1169
|
+
async type(text) {
|
|
1170
|
+
this.guardWrite();
|
|
1171
|
+
await this.x(`xdotool type --delay ${this.typeDelayMs} -- ${this.shq(text)}`);
|
|
1172
|
+
}
|
|
1173
|
+
async keypress(keys) {
|
|
1174
|
+
this.guardWrite();
|
|
1175
|
+
const combo = keys.map(toKeysym).join("+");
|
|
1176
|
+
await this.x(`xdotool key -- ${this.shq(combo)}`);
|
|
1177
|
+
}
|
|
1178
|
+
async drag(path) {
|
|
1179
|
+
this.guardWrite();
|
|
1180
|
+
if (path.length === 0) return;
|
|
1181
|
+
const [sx0, sy0] = path[0];
|
|
1182
|
+
let cmd = `xdotool mousemove --sync ${sx0} ${sy0} mousedown 1`;
|
|
1183
|
+
for (const [px, py] of path.slice(1)) cmd += ` mousemove --sync ${px} ${py}`;
|
|
1184
|
+
cmd += ` mouseup 1`;
|
|
1185
|
+
await this.x(cmd);
|
|
1186
|
+
}
|
|
1187
|
+
async wait() {
|
|
1188
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
var POINTER_BUTTON = {
|
|
1192
|
+
left: PointerButton.POINTER_BUTTON_LEFT,
|
|
1193
|
+
right: PointerButton.POINTER_BUTTON_RIGHT,
|
|
1194
|
+
wheel: PointerButton.POINTER_BUTTON_MIDDLE,
|
|
1195
|
+
back: PointerButton.POINTER_BUTTON_UNSPECIFIED,
|
|
1196
|
+
forward: PointerButton.POINTER_BUTTON_UNSPECIFIED
|
|
1197
|
+
};
|
|
1198
|
+
var NativeDesktopComputer = class {
|
|
1199
|
+
environment;
|
|
1200
|
+
dimensions;
|
|
1201
|
+
session;
|
|
1202
|
+
readOnly;
|
|
1203
|
+
constructor(session, opts = {}) {
|
|
1204
|
+
this.session = session;
|
|
1205
|
+
this.dimensions = opts.dimensions ?? DEFAULT_DIMENSIONS;
|
|
1206
|
+
this.environment = opts.environment ?? "ubuntu";
|
|
1207
|
+
this.readOnly = opts.readOnly ?? false;
|
|
1208
|
+
}
|
|
1209
|
+
/** Rebind to a freshly resumed-by-id session after a box rollover / re-establish. */
|
|
1210
|
+
rebind(session) {
|
|
1211
|
+
this.session = session;
|
|
1212
|
+
}
|
|
1213
|
+
guardWrite() {
|
|
1214
|
+
if (this.readOnly) throw new ComputerReadOnlyError();
|
|
1215
|
+
}
|
|
1216
|
+
async pointer(x, y, action, button) {
|
|
1217
|
+
await this.session.desktopInput({ $case: "pointer", pointer: { x, y, action, button } });
|
|
1218
|
+
}
|
|
1219
|
+
async screenshot() {
|
|
1220
|
+
const { png } = await this.session.screenshot();
|
|
1221
|
+
if (png.length === 0) {
|
|
1222
|
+
throw new ComputerUnavailableError("native desktop screenshot returned an empty frame (display not up?)");
|
|
1223
|
+
}
|
|
1224
|
+
return Buffer.from(png).toString("base64");
|
|
1225
|
+
}
|
|
1226
|
+
async click(x, y, button) {
|
|
1227
|
+
this.guardWrite();
|
|
1228
|
+
await this.pointer(x, y, PointerAction.POINTER_ACTION_CLICK, POINTER_BUTTON[button] ?? PointerButton.POINTER_BUTTON_LEFT);
|
|
1229
|
+
}
|
|
1230
|
+
async doubleClick(x, y) {
|
|
1231
|
+
this.guardWrite();
|
|
1232
|
+
await this.pointer(x, y, PointerAction.POINTER_ACTION_DOUBLE_CLICK, PointerButton.POINTER_BUTTON_LEFT);
|
|
1233
|
+
}
|
|
1234
|
+
async move(x, y) {
|
|
1235
|
+
this.guardWrite();
|
|
1236
|
+
await this.pointer(x, y, PointerAction.POINTER_ACTION_MOVE, PointerButton.POINTER_BUTTON_UNSPECIFIED);
|
|
1237
|
+
}
|
|
1238
|
+
async scroll(x, y, sx, sy) {
|
|
1239
|
+
this.guardWrite();
|
|
1240
|
+
await this.session.desktopInput({ $case: "scroll", scroll: { x, y, deltaX: sx, deltaY: sy } });
|
|
1241
|
+
}
|
|
1242
|
+
async type(text) {
|
|
1243
|
+
this.guardWrite();
|
|
1244
|
+
await this.session.desktopInput({ $case: "key", key: { key: text, isText: true, action: KeyAction.KEY_ACTION_PRESS } });
|
|
1245
|
+
}
|
|
1246
|
+
async keypress(keys) {
|
|
1247
|
+
this.guardWrite();
|
|
1248
|
+
await this.session.desktopInput({ $case: "key", key: { key: keys.join("+"), isText: false, action: KeyAction.KEY_ACTION_PRESS } });
|
|
1249
|
+
}
|
|
1250
|
+
async drag(path) {
|
|
1251
|
+
this.guardWrite();
|
|
1252
|
+
if (path.length === 0) return;
|
|
1253
|
+
const [sx, sy] = path[0];
|
|
1254
|
+
await this.pointer(sx, sy, PointerAction.POINTER_ACTION_DOWN, PointerButton.POINTER_BUTTON_LEFT);
|
|
1255
|
+
for (const [px, py] of path.slice(1)) {
|
|
1256
|
+
await this.pointer(px, py, PointerAction.POINTER_ACTION_MOVE, PointerButton.POINTER_BUTTON_LEFT);
|
|
1257
|
+
}
|
|
1258
|
+
const [ex, ey] = path[path.length - 1];
|
|
1259
|
+
await this.pointer(ex, ey, PointerAction.POINTER_ACTION_UP, PointerButton.POINTER_BUTTON_LEFT);
|
|
1260
|
+
}
|
|
1261
|
+
async wait() {
|
|
1262
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
function isNativeDesktopSession(session) {
|
|
1266
|
+
const s = session;
|
|
1267
|
+
return typeof s.desktopInput === "function" && typeof s.screenshot === "function";
|
|
1268
|
+
}
|
|
1269
|
+
function sniffScreenshotMediaType(bytes) {
|
|
1270
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
|
|
1271
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
|
|
1272
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
|
|
1273
|
+
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
|
|
1274
|
+
return "image/png";
|
|
1275
|
+
}
|
|
1276
|
+
function imageOutputFromScreenshotBytes(bytes) {
|
|
1277
|
+
return { type: "image", image: { data: Uint8Array.from(bytes), mediaType: sniffScreenshotMediaType(bytes) } };
|
|
1278
|
+
}
|
|
1279
|
+
function renderImageForTextTransport(output) {
|
|
1280
|
+
if (typeof output === "string") return output;
|
|
1281
|
+
const { image } = output;
|
|
1282
|
+
const mediaType = typeof image.mediaType === "string" ? image.mediaType : "application/octet-stream";
|
|
1283
|
+
return `data:${mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
1284
|
+
}
|
|
1285
|
+
function supportsStructuredToolOutputTransport(modelInstance) {
|
|
1286
|
+
if (!modelInstance) return false;
|
|
1287
|
+
const constructorName = typeof modelInstance === "object" && modelInstance && typeof modelInstance.constructor === "function" ? modelInstance.constructor.name ?? "" : "";
|
|
1288
|
+
return !constructorName.includes("ChatCompletions");
|
|
1289
|
+
}
|
|
1290
|
+
var COMPUTER_READ_ONLY_MESSAGE = "computer-use is read-only for this session \u2014 click, double_click, move, scroll, type, keypress, and drag are disabled. Call computer_screenshot to observe the desktop.";
|
|
1291
|
+
var COORD_PROPS = {
|
|
1292
|
+
x: { type: "integer", description: "X coordinate in the pixels of the most recent computer_screenshot" },
|
|
1293
|
+
y: { type: "integer", description: "Y coordinate in the pixels of the most recent computer_screenshot" }
|
|
1294
|
+
};
|
|
1295
|
+
function objectSchema(properties, required) {
|
|
1296
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
1297
|
+
}
|
|
1298
|
+
function computerFunctionTools(computer, readOnly, needsApproval, imageFunctionResults = false) {
|
|
1299
|
+
const approval = needsApproval !== void 0 ? { needsApproval } : {};
|
|
1300
|
+
const write = async (confirmation, action) => {
|
|
1301
|
+
if (readOnly) return COMPUTER_READ_ONLY_MESSAGE;
|
|
1302
|
+
try {
|
|
1303
|
+
await action();
|
|
1304
|
+
return confirmation;
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
if (error instanceof ComputerReadOnlyError) return COMPUTER_READ_ONLY_MESSAGE;
|
|
1307
|
+
return `computer action failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
return [
|
|
1311
|
+
tool({
|
|
1312
|
+
name: "computer_screenshot",
|
|
1313
|
+
description: "Capture the current desktop and return it as an image. Call this FIRST and again after each action \u2014 all coordinates for click/move/scroll/drag are pixels of the most recent screenshot.",
|
|
1314
|
+
parameters: objectSchema({}, []),
|
|
1315
|
+
strict: false,
|
|
1316
|
+
execute: async () => {
|
|
1317
|
+
const b64 = await computer.screenshot();
|
|
1318
|
+
const bytes = Uint8Array.from(Buffer.from(b64, "base64"));
|
|
1319
|
+
const image = imageOutputFromScreenshotBytes(bytes);
|
|
1320
|
+
return imageFunctionResults ? image : renderImageForTextTransport(image);
|
|
1321
|
+
}
|
|
1322
|
+
}),
|
|
1323
|
+
tool({
|
|
1324
|
+
name: "computer_click",
|
|
1325
|
+
description: "Click the mouse at (x, y). `button` is one of left|right|wheel|back|forward (default left). Take a computer_screenshot first to find coordinates.",
|
|
1326
|
+
parameters: objectSchema(
|
|
1327
|
+
{ ...COORD_PROPS, button: { type: "string", enum: ["left", "right", "wheel", "back", "forward"], description: "Mouse button; defaults to left" } },
|
|
1328
|
+
["x", "y"]
|
|
1329
|
+
),
|
|
1330
|
+
strict: false,
|
|
1331
|
+
...approval,
|
|
1332
|
+
execute: async (input) => {
|
|
1333
|
+
const { x, y, button } = input;
|
|
1334
|
+
return write(`clicked ${button ?? "left"} at (${x}, ${y})`, () => computer.click(x, y, button ?? "left"));
|
|
1335
|
+
}
|
|
1336
|
+
}),
|
|
1337
|
+
tool({
|
|
1338
|
+
name: "computer_double_click",
|
|
1339
|
+
description: "Double-click the left mouse button at (x, y). Take a computer_screenshot first to find coordinates.",
|
|
1340
|
+
parameters: objectSchema({ ...COORD_PROPS }, ["x", "y"]),
|
|
1341
|
+
strict: false,
|
|
1342
|
+
...approval,
|
|
1343
|
+
execute: async (input) => {
|
|
1344
|
+
const { x, y } = input;
|
|
1345
|
+
return write(`double-clicked at (${x}, ${y})`, () => computer.doubleClick(x, y));
|
|
1346
|
+
}
|
|
1347
|
+
}),
|
|
1348
|
+
tool({
|
|
1349
|
+
name: "computer_move",
|
|
1350
|
+
description: "Move the mouse cursor to (x, y) without clicking.",
|
|
1351
|
+
parameters: objectSchema({ ...COORD_PROPS }, ["x", "y"]),
|
|
1352
|
+
strict: false,
|
|
1353
|
+
...approval,
|
|
1354
|
+
execute: async (input) => {
|
|
1355
|
+
const { x, y } = input;
|
|
1356
|
+
return write(`moved to (${x}, ${y})`, () => computer.move(x, y));
|
|
1357
|
+
}
|
|
1358
|
+
}),
|
|
1359
|
+
tool({
|
|
1360
|
+
name: "computer_scroll",
|
|
1361
|
+
description: "Scroll at (x, y) by scroll_x / scroll_y pixels (positive scroll_y scrolls down, negative up; positive scroll_x scrolls right).",
|
|
1362
|
+
parameters: objectSchema(
|
|
1363
|
+
{
|
|
1364
|
+
...COORD_PROPS,
|
|
1365
|
+
scroll_x: { type: "integer", description: "Horizontal scroll amount in pixels (positive = right)" },
|
|
1366
|
+
scroll_y: { type: "integer", description: "Vertical scroll amount in pixels (positive = down)" }
|
|
1367
|
+
},
|
|
1368
|
+
["x", "y", "scroll_x", "scroll_y"]
|
|
1369
|
+
),
|
|
1370
|
+
strict: false,
|
|
1371
|
+
...approval,
|
|
1372
|
+
execute: async (input) => {
|
|
1373
|
+
const { x, y, scroll_x, scroll_y } = input;
|
|
1374
|
+
return write(`scrolled (${scroll_x}, ${scroll_y}) at (${x}, ${y})`, () => computer.scroll(x, y, scroll_x, scroll_y));
|
|
1375
|
+
}
|
|
1376
|
+
}),
|
|
1377
|
+
tool({
|
|
1378
|
+
name: "computer_type",
|
|
1379
|
+
description: "Type a literal text string at the current keyboard focus. Click the target field first.",
|
|
1380
|
+
parameters: objectSchema({ text: { type: "string", description: "The literal text to type" } }, ["text"]),
|
|
1381
|
+
strict: false,
|
|
1382
|
+
...approval,
|
|
1383
|
+
execute: async (input) => {
|
|
1384
|
+
const { text } = input;
|
|
1385
|
+
return write(`typed ${text.length} character(s)`, () => computer.type(text));
|
|
1386
|
+
}
|
|
1387
|
+
}),
|
|
1388
|
+
tool({
|
|
1389
|
+
name: "computer_keypress",
|
|
1390
|
+
description: 'Press a key or chord. `keys` is an ordered list pressed together, e.g. ["ctrl","c"] or ["Enter"]. Use key names (ctrl, alt, shift, cmd, enter, tab, esc, arrows\u2026), not characters.',
|
|
1391
|
+
parameters: objectSchema(
|
|
1392
|
+
{ keys: { type: "array", items: { type: "string" }, description: "Keys pressed together as a chord" } },
|
|
1393
|
+
["keys"]
|
|
1394
|
+
),
|
|
1395
|
+
strict: false,
|
|
1396
|
+
...approval,
|
|
1397
|
+
execute: async (input) => {
|
|
1398
|
+
const { keys } = input;
|
|
1399
|
+
return write(`pressed ${keys.join("+")}`, () => computer.keypress(keys));
|
|
1400
|
+
}
|
|
1401
|
+
}),
|
|
1402
|
+
tool({
|
|
1403
|
+
name: "computer_drag",
|
|
1404
|
+
description: "Drag the left mouse button along a path of points. `path` is an ordered list of {x, y} pixels; the button is pressed at the first point, moved through each, and released at the last.",
|
|
1405
|
+
parameters: objectSchema(
|
|
1406
|
+
{
|
|
1407
|
+
path: {
|
|
1408
|
+
type: "array",
|
|
1409
|
+
description: "Ordered list of points to drag through",
|
|
1410
|
+
items: {
|
|
1411
|
+
type: "object",
|
|
1412
|
+
properties: { x: { type: "integer" }, y: { type: "integer" } },
|
|
1413
|
+
required: ["x", "y"],
|
|
1414
|
+
additionalProperties: false
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
["path"]
|
|
1419
|
+
),
|
|
1420
|
+
strict: false,
|
|
1421
|
+
...approval,
|
|
1422
|
+
execute: async (input) => {
|
|
1423
|
+
const { path } = input;
|
|
1424
|
+
const points = path.map((p) => [p.x, p.y]);
|
|
1425
|
+
return write(`dragged through ${points.length} point(s)`, () => computer.drag(points));
|
|
1426
|
+
}
|
|
1427
|
+
})
|
|
1428
|
+
];
|
|
1429
|
+
}
|
|
1430
|
+
function computerUse(args = {}) {
|
|
1431
|
+
return new ComputerUseCapability(args);
|
|
1432
|
+
}
|
|
1433
|
+
var ComputerUseCapability = class extends Capability {
|
|
1434
|
+
constructor(args = {}) {
|
|
1435
|
+
super();
|
|
1436
|
+
this.args = args;
|
|
1437
|
+
}
|
|
1438
|
+
args;
|
|
1439
|
+
type = "computer-use";
|
|
1440
|
+
tools() {
|
|
1441
|
+
const session = requireBoundSession("computer-use", this._session);
|
|
1442
|
+
const computer = isNativeDesktopSession(session) ? new NativeDesktopComputer(session, {
|
|
1443
|
+
...this.args.dimensions ? { dimensions: this.args.dimensions } : {},
|
|
1444
|
+
...this.args.readOnly !== void 0 ? { readOnly: this.args.readOnly } : {}
|
|
1445
|
+
}) : new SandboxComputer(session, {
|
|
1446
|
+
...this.args.dimensions ? { dimensions: this.args.dimensions } : {},
|
|
1447
|
+
...this.args.readOnly !== void 0 ? { readOnly: this.args.readOnly } : {},
|
|
1448
|
+
...this.args.display ? { display: this.args.display } : {},
|
|
1449
|
+
// The SDK base exposes the bound runAs as a protected field.
|
|
1450
|
+
...typeof this._runAs === "string" ? { runAs: this._runAs } : {}
|
|
1451
|
+
});
|
|
1452
|
+
if (supportsStructuredToolOutputTransport(this._modelInstance)) {
|
|
1453
|
+
return [
|
|
1454
|
+
computerTool({
|
|
1455
|
+
computer,
|
|
1456
|
+
...this.args.needsApproval !== void 0 ? { needsApproval: this.args.needsApproval } : {}
|
|
1457
|
+
})
|
|
1458
|
+
];
|
|
1459
|
+
}
|
|
1460
|
+
return computerFunctionTools(computer, this.args.readOnly ?? false, this.args.needsApproval, this.args.imageFunctionResults ?? false);
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
// src/index.ts
|
|
1465
|
+
import { OpenAIChatCompletionsModel as OpenAIChatCompletionsModel2, OpenAIResponsesModel as OpenAIResponsesModel2 } from "@openai/agents";
|
|
1466
|
+
import { MaxTurnsExceededError as MaxTurnsExceededError2 } from "@openai/agents";
|
|
1467
|
+
setSelfhostedApplyDiff(applyDiff);
|
|
1468
|
+
ensureReadableStreamFrom();
|
|
1469
|
+
var SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS = 12e4;
|
|
1470
|
+
function ensureReadableStreamFrom() {
|
|
1471
|
+
const ctor = globalThis.ReadableStream;
|
|
1472
|
+
if (!ctor || typeof ctor.from === "function") {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
Object.defineProperty(ctor, "from", {
|
|
1476
|
+
configurable: true,
|
|
1477
|
+
writable: true,
|
|
1478
|
+
value(source) {
|
|
1479
|
+
const iterator = isAsyncIterable(source) ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
|
|
1480
|
+
return new ReadableStream({
|
|
1481
|
+
async pull(controller) {
|
|
1482
|
+
const next = await iterator.next();
|
|
1483
|
+
if (next.done) {
|
|
1484
|
+
controller.close();
|
|
1485
|
+
} else {
|
|
1486
|
+
controller.enqueue(next.value);
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
async cancel() {
|
|
1490
|
+
await iterator.return?.();
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
function createProductionAgentRuntime(overrides = {}) {
|
|
1497
|
+
return {
|
|
1498
|
+
configure: configureOpenAI,
|
|
1499
|
+
// A test/override model shadows the registry routing entirely (the scripted
|
|
1500
|
+
// model used in worker tests is not in any provider's allow-list), so when
|
|
1501
|
+
// one is supplied resolveTurnModel reports "no resolution" and the caller
|
|
1502
|
+
// keeps the legacy global-client path with the override model.
|
|
1503
|
+
resolveTurnModel: (settings, modelId) => overrides.model ? null : resolveTurnModel(settings, modelId),
|
|
1504
|
+
buildAgent: (settings, resources, options) => buildOpenGeniAgent(settings, resources, {
|
|
1505
|
+
...options,
|
|
1506
|
+
...overrides.model ? { model: overrides.model } : {}
|
|
1507
|
+
}),
|
|
1508
|
+
prepareTools: prepareAgentTools,
|
|
1509
|
+
prepareInput: prepareRunInput,
|
|
1510
|
+
runStream: async (agent, input, settings, options) => await runAgentStream(agent, input, settings, {
|
|
1511
|
+
...options,
|
|
1512
|
+
sandboxClient: overrides.sandboxClient
|
|
1513
|
+
}),
|
|
1514
|
+
serializeApprovals
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function buildOpenAIClientFromSettings(settings) {
|
|
1518
|
+
if (settings.openaiProvider === "azure") {
|
|
1519
|
+
const baseURL = settings.azureOpenaiBaseUrl ?? azureDeploymentBaseUrl(settings);
|
|
1520
|
+
const apiKey = settings.azureOpenaiApiKey ?? settings.azureOpenaiAdToken ?? "azure-ad-token";
|
|
1521
|
+
return new OpenAI({
|
|
1522
|
+
apiKey,
|
|
1523
|
+
baseURL,
|
|
1524
|
+
maxRetries: settings.openaiMaxRetries,
|
|
1525
|
+
defaultQuery: azureOpenAIDefaultQuery(settings, baseURL),
|
|
1526
|
+
defaultHeaders: settings.azureOpenaiAdToken && !settings.azureOpenaiApiKey ? { Authorization: `Bearer ${settings.azureOpenaiAdToken}` } : void 0,
|
|
1527
|
+
// Rewrite every outbound /responses computer_call to the ACTIONS-ONLY shape
|
|
1528
|
+
// the GA Azure computer tool (gpt-5.5) accepts. This is the lowest reachable
|
|
1529
|
+
// seam — below the SDK responses converter, which always re-synthesizes BOTH
|
|
1530
|
+
// `action` and `actions` (rejected 400 "exactly one of action or actions").
|
|
1531
|
+
// See computerCallNormalizingFetch / rewriteComputerCallsToActionsOnly.
|
|
1532
|
+
fetch: computerCallNormalizingFetch(globalThis.fetch)
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
return new OpenAI({
|
|
1536
|
+
apiKey: settings.openaiApiKey ?? process.env.OPENAI_API_KEY,
|
|
1537
|
+
...settings.openaiBaseUrl ? { baseURL: settings.openaiBaseUrl } : {},
|
|
1538
|
+
maxRetries: settings.openaiMaxRetries
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
var providerClientCache = /* @__PURE__ */ new Map();
|
|
1542
|
+
function buildProviderClient(provider, settings) {
|
|
1543
|
+
const cached = providerClientCache.get(provider.id);
|
|
1544
|
+
if (cached) {
|
|
1545
|
+
return cached;
|
|
1546
|
+
}
|
|
1547
|
+
const client = provider.builtin ? buildOpenAIClientFromSettings(settings) : provider.kind === "codex-subscription" ? new OpenAI({
|
|
1548
|
+
apiKey: provider.apiKey ?? "codex-subscription",
|
|
1549
|
+
...provider.baseUrl ? { baseURL: provider.baseUrl } : {},
|
|
1550
|
+
maxRetries: settings.openaiMaxRetries,
|
|
1551
|
+
fetch: codexSubscriptionFetch(globalThis.fetch)
|
|
1552
|
+
}) : new OpenAI({
|
|
1553
|
+
...provider.apiKey ? { apiKey: provider.apiKey } : {},
|
|
1554
|
+
...provider.baseUrl ? { baseURL: provider.baseUrl } : {},
|
|
1555
|
+
maxRetries: settings.openaiMaxRetries,
|
|
1556
|
+
...provider.defaultQuery ? { defaultQuery: provider.defaultQuery } : {},
|
|
1557
|
+
...provider.defaultHeaders ? { defaultHeaders: provider.defaultHeaders } : {}
|
|
1558
|
+
});
|
|
1559
|
+
providerClientCache.set(provider.id, client);
|
|
1560
|
+
return client;
|
|
1561
|
+
}
|
|
1562
|
+
function buildModelInstance(provider, client, modelId) {
|
|
1563
|
+
return provider.api === "chat" ? new OpenAIChatCompletionsModel(client, modelId) : new OpenAIResponsesModel(client, modelId);
|
|
1564
|
+
}
|
|
1565
|
+
function resolveTurnModel(settings, modelId) {
|
|
1566
|
+
const resolved = resolveModelProvider(settings, modelId);
|
|
1567
|
+
if (!resolved) {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
const client = buildProviderClient(resolved.provider, settings);
|
|
1571
|
+
return {
|
|
1572
|
+
provider: resolved.provider,
|
|
1573
|
+
client,
|
|
1574
|
+
model: buildModelInstance(resolved.provider, client, resolved.model.id),
|
|
1575
|
+
configured: resolved.model
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
var MultiProviderModelProvider = class {
|
|
1579
|
+
constructor(settings) {
|
|
1580
|
+
this.settings = settings;
|
|
1581
|
+
}
|
|
1582
|
+
settings;
|
|
1583
|
+
fallback;
|
|
1584
|
+
async getModel(modelName) {
|
|
1585
|
+
if (modelName) {
|
|
1586
|
+
const resolved = resolveTurnModel(this.settings, modelName);
|
|
1587
|
+
if (resolved) {
|
|
1588
|
+
if (modelName.startsWith(CODEX_MODEL_ID_PREFIX) && resolved.provider.kind !== "codex-subscription") {
|
|
1589
|
+
throw new CodexSubscriptionUnavailableError(modelName);
|
|
1590
|
+
}
|
|
1591
|
+
return resolved.model;
|
|
1592
|
+
}
|
|
1593
|
+
if (modelName.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
1594
|
+
throw new CodexSubscriptionUnavailableError(modelName);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
this.fallback ??= new OpenAIProvider();
|
|
1598
|
+
return this.fallback.getModel(modelName);
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
var CodexSubscriptionUnavailableError = class extends Error {
|
|
1602
|
+
constructor(modelName) {
|
|
1603
|
+
super(
|
|
1604
|
+
`Codex subscription model "${modelName}" is unavailable: no active Codex subscription is connected for this workspace. Connect (or reconnect) your ChatGPT/Codex subscription in Settings, then retry.`
|
|
1605
|
+
);
|
|
1606
|
+
this.name = "CodexSubscriptionUnavailableError";
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
function configureOpenAI(settings) {
|
|
1610
|
+
setOpenAIResponsesTransport(settings.openaiResponsesTransport);
|
|
1611
|
+
const router = new MultiProviderModelProvider(settings);
|
|
1612
|
+
if (settings.openaiProvider === "azure") {
|
|
1613
|
+
setDefaultOpenAIClient(buildOpenAIClientFromSettings(settings));
|
|
1614
|
+
setDefaultModelProvider(router);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (settings.openaiApiKey) {
|
|
1618
|
+
setDefaultOpenAIKey(settings.openaiApiKey);
|
|
1619
|
+
}
|
|
1620
|
+
if (settings.openaiBaseUrl) {
|
|
1621
|
+
setDefaultOpenAIClient(buildOpenAIClientFromSettings(settings));
|
|
1622
|
+
}
|
|
1623
|
+
setDefaultModelProvider(router);
|
|
1624
|
+
}
|
|
1625
|
+
async function summarizeForCompaction(settings, messages, options = {}) {
|
|
1626
|
+
const client = options.client ?? buildOpenAIClientFromSettings(settings);
|
|
1627
|
+
const api = options.api ?? "responses";
|
|
1628
|
+
const model = options.model ?? settings.openaiModel;
|
|
1629
|
+
const maxTokens = options.maxOutputTokens ?? settings.contextSummaryMaxTokens;
|
|
1630
|
+
try {
|
|
1631
|
+
if (api === "chat") {
|
|
1632
|
+
const completion = await client.chat.completions.create({
|
|
1633
|
+
model,
|
|
1634
|
+
max_tokens: maxTokens,
|
|
1635
|
+
messages: [
|
|
1636
|
+
{ role: "system", content: messages.system },
|
|
1637
|
+
{ role: "user", content: messages.user }
|
|
1638
|
+
]
|
|
1639
|
+
});
|
|
1640
|
+
const text2 = completion.choices?.[0]?.message?.content;
|
|
1641
|
+
const trimmed2 = typeof text2 === "string" ? text2.trim() : "";
|
|
1642
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
1643
|
+
}
|
|
1644
|
+
const response = await client.responses.create({
|
|
1645
|
+
model,
|
|
1646
|
+
// store:false is the OpenAI-platform-only storeless precondition; Azure
|
|
1647
|
+
// rejects it. The summarizer's resolved client is OpenAI/Azure on the
|
|
1648
|
+
// built-in path (api "responses"), so gate it on the built-in provider.
|
|
1649
|
+
...settings.openaiProvider === "azure" ? {} : { store: false },
|
|
1650
|
+
max_output_tokens: maxTokens,
|
|
1651
|
+
input: [
|
|
1652
|
+
{ role: "system", content: messages.system },
|
|
1653
|
+
{ role: "user", content: messages.user }
|
|
1654
|
+
]
|
|
1655
|
+
});
|
|
1656
|
+
const text = extractResponseOutputText(response);
|
|
1657
|
+
const trimmed = text.trim();
|
|
1658
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
console.error("context compaction summarize failed (compaction skipped this turn)", error);
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
function extractResponseOutputText(response) {
|
|
1665
|
+
if (!response || typeof response !== "object") {
|
|
1666
|
+
return "";
|
|
1667
|
+
}
|
|
1668
|
+
const direct = response.output_text;
|
|
1669
|
+
if (typeof direct === "string") {
|
|
1670
|
+
return direct;
|
|
1671
|
+
}
|
|
1672
|
+
const output = response.output;
|
|
1673
|
+
if (!Array.isArray(output)) {
|
|
1674
|
+
return "";
|
|
1675
|
+
}
|
|
1676
|
+
const parts = [];
|
|
1677
|
+
for (const item of output) {
|
|
1678
|
+
if (!item || typeof item !== "object") {
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
if (item.type !== "message") {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
if (item.role !== "assistant") {
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const content = item.content;
|
|
1688
|
+
if (!Array.isArray(content)) {
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
for (const part of content) {
|
|
1692
|
+
if (part && typeof part === "object" && typeof part.text === "string") {
|
|
1693
|
+
parts.push(part.text);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return parts.join("");
|
|
1698
|
+
}
|
|
1699
|
+
function workspaceEnvironmentInstructions(environment) {
|
|
1700
|
+
const lines = [
|
|
1701
|
+
`A workspace environment named "${environment.name}" is attached to this session; its variables are exported in the sandbox shell environment.`
|
|
1702
|
+
];
|
|
1703
|
+
const variableNames = (environment.variableNames ?? []).filter((name) => name.length > 0);
|
|
1704
|
+
if (variableNames.length > 0) {
|
|
1705
|
+
lines.push(`Exported environment variables: ${[...variableNames].sort().join(", ")}.`);
|
|
1706
|
+
}
|
|
1707
|
+
const description = environment.description?.trim();
|
|
1708
|
+
if (description) {
|
|
1709
|
+
lines.push(`Environment notes from the operator: ${description}`);
|
|
1710
|
+
}
|
|
1711
|
+
return lines;
|
|
1712
|
+
}
|
|
1713
|
+
function coreInstructions(workspaceEnvironment) {
|
|
1714
|
+
return [
|
|
1715
|
+
"If the session has a goal, you own it: keep working until you call opengeni__goal_complete with concrete evidence or opengeni__goal_pause with a rationale; revise it with opengeni__goal_update; create one with opengeni__goal_set when given a long-running objective.",
|
|
1716
|
+
...workspaceEnvironment ? workspaceEnvironmentInstructions(workspaceEnvironment) : []
|
|
1717
|
+
];
|
|
1718
|
+
}
|
|
1719
|
+
function composeAgentInstructions(template, workspaceEnvironment) {
|
|
1720
|
+
const core = coreInstructions(workspaceEnvironment).join(" ");
|
|
1721
|
+
if (template.includes(AGENT_INSTRUCTIONS_CORE_PLACEHOLDER)) {
|
|
1722
|
+
return template.split(AGENT_INSTRUCTIONS_CORE_PLACEHOLDER).join(core);
|
|
1723
|
+
}
|
|
1724
|
+
return core ? `${template} ${core}` : template;
|
|
1725
|
+
}
|
|
1726
|
+
var agentFileDownloads = /* @__PURE__ */ new WeakMap();
|
|
1727
|
+
var agentRepositoryCloneHooks = /* @__PURE__ */ new WeakMap();
|
|
1728
|
+
var agentGitTokenSeed = /* @__PURE__ */ new WeakMap();
|
|
1729
|
+
var agentActiveSandboxBackend = /* @__PURE__ */ new WeakMap();
|
|
1730
|
+
function buildOpenGeniAgent(settings, resources, options = {}) {
|
|
1731
|
+
const compactionMode = options.compactionMode ?? resolveContextCompactionMode(settings);
|
|
1732
|
+
const hostedWebSearch = options.hostedWebSearch ?? settings.webSearchEnabled;
|
|
1733
|
+
const encryptedReasoning = options.encryptedReasoning ?? settings.openaiReasoningEncryptedContent;
|
|
1734
|
+
const contextWindowTokens = options.contextWindowTokens ?? settings.contextWindowTokens;
|
|
1735
|
+
const hostedTools = hostedWebSearch ? [webSearchTool()] : [];
|
|
1736
|
+
const baseConfig = {
|
|
1737
|
+
name: "OpenGeni Agent",
|
|
1738
|
+
model: options.model ?? settings.openaiModel,
|
|
1739
|
+
// White-label persona composition. The effective template is the per-call
|
|
1740
|
+
// override (options.instructionsTemplate, resolved by the caller as
|
|
1741
|
+
// session > workspace) falling back to the deployment default
|
|
1742
|
+
// (settings.agentInstructionsTemplate, default DEFAULT_AGENT_INSTRUCTIONS).
|
|
1743
|
+
// composeAgentInstructions substitutes the non-bypassable CORE (goal-loop
|
|
1744
|
+
// ownership + workspace-environment block) at the {{core}} marker, or
|
|
1745
|
+
// appends it when the template omits the marker. With the default template
|
|
1746
|
+
// and no environment this is byte-identical to the historical preamble.
|
|
1747
|
+
instructions: options.genesisTitleHint ? `${composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment)} ${GENESIS_TITLE_DIRECTIVE}` : composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment),
|
|
1748
|
+
modelSettings: {
|
|
1749
|
+
reasoning: { effort: options.reasoningEffort ?? settings.openaiReasoningEffort, summary: "detailed" },
|
|
1750
|
+
// Server-side compaction (OpenAI platform) requires store=false: the
|
|
1751
|
+
// server emits an opaque ENCRYPTED 'compaction' item that round-trips in
|
|
1752
|
+
// the request rather than being anchored to a stored response. OpenGeni
|
|
1753
|
+
// already runs storeless (provider item ids stripped, encrypted reasoning
|
|
1754
|
+
// round-tripped), so this is consistent with the existing design and
|
|
1755
|
+
// only set where the server compaction capability is attached. Gated on
|
|
1756
|
+
// the RESOLVED compaction mode (registry providers resolve to "client",
|
|
1757
|
+
// so they never carry store:false).
|
|
1758
|
+
...compactionMode === "server" ? { store: false } : {},
|
|
1759
|
+
// Round-trip the encrypted reasoning payload with every call so chains
|
|
1760
|
+
// of thought survive without provider-side response storage (which is
|
|
1761
|
+
// what stripped provider item ids opt us out of — see
|
|
1762
|
+
// stripProviderItemIds). providerData.include replaces any
|
|
1763
|
+
// tool-derived include entries; OpenGeni's tools are MCP/sandbox
|
|
1764
|
+
// function tools, which contribute none. Gated on the resolved
|
|
1765
|
+
// encryptedReasoning flag: the chat wire API has no encrypted_content
|
|
1766
|
+
// field, so registry "chat" providers turn it off.
|
|
1767
|
+
...encryptedReasoning ? { providerData: { include: ["reasoning.encrypted_content"] } } : {}
|
|
1768
|
+
},
|
|
1769
|
+
// Explicit hosted tools (web_search when enabled). Threaded into BOTH the
|
|
1770
|
+
// `new Agent(baseConfig)` path (sandboxBackend === "none") and the
|
|
1771
|
+
// `new SandboxAgent({ ...baseConfig, ... })` path via the shared baseConfig
|
|
1772
|
+
// spread; the SDK concatenates these with MCP and sandbox capability tools.
|
|
1773
|
+
...hostedTools.length ? { tools: hostedTools } : {},
|
|
1774
|
+
...options.mcpServers?.length ? { mcpServers: options.mcpServers } : {}
|
|
1775
|
+
};
|
|
1776
|
+
if (settings.sandboxBackend === "none") {
|
|
1777
|
+
const agent2 = new Agent(baseConfig);
|
|
1778
|
+
maybeInstallCodexToolSearch(agent2, settings, options);
|
|
1779
|
+
return agent2;
|
|
1780
|
+
}
|
|
1781
|
+
const runAs = sandboxRunAs(settings);
|
|
1782
|
+
const agent = new SandboxAgent({
|
|
1783
|
+
...baseConfig,
|
|
1784
|
+
defaultManifest: buildManifest(settings, resources, options.sandboxEnvironment, options.fileResourceDownloads),
|
|
1785
|
+
...runAs ? { runAs } : {},
|
|
1786
|
+
capabilities: buildAgentCapabilities(settings, options.packSkills ?? [], {
|
|
1787
|
+
compactionMode,
|
|
1788
|
+
contextWindowTokens,
|
|
1789
|
+
...options.structuredToolTransport !== void 0 ? { structuredToolTransport: options.structuredToolTransport } : {}
|
|
1790
|
+
})
|
|
1791
|
+
});
|
|
1792
|
+
agentFileDownloads.set(agent, normalizeSandboxFileDownloads(options.fileResourceDownloads ?? []).filter((download) => !download.content));
|
|
1793
|
+
agentRepositoryCloneHooks.set(agent, sandboxRepositoryCloneHooks(settings, resources, options.activeSandboxBackend));
|
|
1794
|
+
if (options.activeSandboxBackend) {
|
|
1795
|
+
agentActiveSandboxBackend.set(agent, options.activeSandboxBackend);
|
|
1796
|
+
}
|
|
1797
|
+
if (options.gitTokenSeed) {
|
|
1798
|
+
agentGitTokenSeed.set(agent, options.gitTokenSeed);
|
|
1799
|
+
}
|
|
1800
|
+
maybeInstallCodexToolSearch(agent, settings, options);
|
|
1801
|
+
return agent;
|
|
1802
|
+
}
|
|
1803
|
+
function maybeInstallCodexToolSearch(agent, settings, options) {
|
|
1804
|
+
if (settings.codexToolSearchEnabled && options.structuredToolTransport === false) {
|
|
1805
|
+
installCodexToolSearch(
|
|
1806
|
+
agent,
|
|
1807
|
+
options.codexConnectorNamespaces ?? /* @__PURE__ */ new Set()
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
function neutralizeStructuredToolTransport(capability) {
|
|
1812
|
+
const forceFunctionTransport = function() {
|
|
1813
|
+
this._modelInstance = void 0;
|
|
1814
|
+
return this;
|
|
1815
|
+
};
|
|
1816
|
+
capability.bindModel = forceFunctionTransport;
|
|
1817
|
+
}
|
|
1818
|
+
function buildAgentCapabilities(settings, packSkills, options = {}) {
|
|
1819
|
+
const mode = options.compactionMode ?? resolveContextCompactionMode(settings);
|
|
1820
|
+
const contextWindowTokens = options.contextWindowTokens ?? settings.contextWindowTokens;
|
|
1821
|
+
const filesystemCapability = filesystem();
|
|
1822
|
+
if (options.structuredToolTransport === false) {
|
|
1823
|
+
neutralizeStructuredToolTransport(filesystemCapability);
|
|
1824
|
+
}
|
|
1825
|
+
const caps = [filesystemCapability, shell()];
|
|
1826
|
+
if (mode === "server") {
|
|
1827
|
+
caps.push(compaction({ policy: new StaticCompactionPolicy(contextServerCompactThreshold({ ...settings, contextWindowTokens })) }));
|
|
1828
|
+
}
|
|
1829
|
+
caps.push(skills({ lazyFrom: lazySkillSourceWithPackSkills(packSkills) }));
|
|
1830
|
+
if (settings.computerUseEnabled && settings.sandboxDesktopEnabled && desktopCapableBackend(settings.sandboxBackend)) {
|
|
1831
|
+
const computerCapability = computerUse({
|
|
1832
|
+
dimensions: [settings.streamResolutionWidth, settings.streamResolutionHeight],
|
|
1833
|
+
readOnly: settings.computerUseReadOnly,
|
|
1834
|
+
// On the codex path the function tools deliver screenshots as a real image the
|
|
1835
|
+
// model can see. The ChatGPT/Codex backend rejects HOSTED tool types but DOES
|
|
1836
|
+
// accept `input_image` content items inside a `function_call_output` (proven by
|
|
1837
|
+
// openai/codex codex-rs, whose view_image tool ships exactly that shape) — so a
|
|
1838
|
+
// structured image tool result is seen, where a text data-URL would be unreadable.
|
|
1839
|
+
...options.structuredToolTransport === false ? { imageFunctionResults: true } : {}
|
|
1840
|
+
});
|
|
1841
|
+
if (options.structuredToolTransport === false) {
|
|
1842
|
+
neutralizeStructuredToolTransport(computerCapability);
|
|
1843
|
+
}
|
|
1844
|
+
caps.push(computerCapability);
|
|
1845
|
+
}
|
|
1846
|
+
return caps;
|
|
1847
|
+
}
|
|
1848
|
+
function sandboxRunAs(_settings) {
|
|
1849
|
+
return void 0;
|
|
1850
|
+
}
|
|
1851
|
+
async function prepareAgentTools(settings, tools, options = {}) {
|
|
1852
|
+
const codexConnectorNamespaces = /* @__PURE__ */ new Set();
|
|
1853
|
+
if (tools.length === 0) {
|
|
1854
|
+
return { mcpServers: [], close: async () => {
|
|
1855
|
+
}, codexConnectorNamespaces };
|
|
1856
|
+
}
|
|
1857
|
+
const registry = new Map(settings.mcpServers.map((server) => [server.id, server]));
|
|
1858
|
+
const servers = await Promise.all(tools.map(async (tool2) => {
|
|
1859
|
+
const config = registry.get(tool2.id);
|
|
1860
|
+
if (!config) {
|
|
1861
|
+
throw new Error(`Unknown MCP server id: ${tool2.id}`);
|
|
1862
|
+
}
|
|
1863
|
+
const url = firstPartyMcpServerUrlForRun(settings, config, options.workspaceId) ?? config.url;
|
|
1864
|
+
const server = new PrefixedMcpServer(new MCPServerStreamableHttp({
|
|
1865
|
+
url,
|
|
1866
|
+
name: config.name ?? config.id,
|
|
1867
|
+
cacheToolsList: config.cacheToolsList,
|
|
1868
|
+
// codex_apps returns connector tools with empty `outputSchema: {}` that the
|
|
1869
|
+
// MCP SDK's strict Tool schema rejects (fails the turn during tools/list);
|
|
1870
|
+
// sanitize the response on the wire before validation. The namespace Set
|
|
1871
|
+
// also captures each tool's original connector namespace (P4 Part B.1).
|
|
1872
|
+
...isCodexAppsMcpServer(config) ? { fetch: codexAppsSanitizingFetch(globalThis.fetch, codexConnectorNamespaces) } : {},
|
|
1873
|
+
...await mcpServerRequestInit(settings, config, options),
|
|
1874
|
+
...config.timeoutMs ? {
|
|
1875
|
+
timeout: config.timeoutMs,
|
|
1876
|
+
clientSessionTimeoutSeconds: Math.ceil(config.timeoutMs / 1e3)
|
|
1877
|
+
} : {}
|
|
1878
|
+
}), config.id, config.allowedTools);
|
|
1879
|
+
const optional = tool2.optional === true;
|
|
1880
|
+
return { server, bestEffort: isCodexAppsMcpServer(config) || optional, optional };
|
|
1881
|
+
}));
|
|
1882
|
+
const requiredServers = servers.filter((entry) => !entry.bestEffort).map((entry) => entry.server);
|
|
1883
|
+
const bestEffortServers = servers.filter((entry) => entry.bestEffort).map((entry) => entry.server);
|
|
1884
|
+
const optionalServerNames = new Set(
|
|
1885
|
+
servers.filter((entry) => entry.optional).map((entry) => entry.server.name)
|
|
1886
|
+
);
|
|
1887
|
+
const connectedRequired = await connectMcpServers(requiredServers, {
|
|
1888
|
+
connectInParallel: true,
|
|
1889
|
+
strict: true
|
|
1890
|
+
});
|
|
1891
|
+
const connectedBestEffort = bestEffortServers.length ? await connectMcpServers(bestEffortServers, {
|
|
1892
|
+
connectInParallel: true,
|
|
1893
|
+
strict: false
|
|
1894
|
+
}) : null;
|
|
1895
|
+
if (connectedBestEffort) {
|
|
1896
|
+
for (const failed of connectedBestEffort.failed) {
|
|
1897
|
+
if (!optionalServerNames.has(failed.name)) {
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
const error = connectedBestEffort.errors.get(failed);
|
|
1901
|
+
console.warn(
|
|
1902
|
+
`[mcp] optional capability server "${failed.name}" failed to connect/list tools; skipping it for this turn`,
|
|
1903
|
+
error instanceof Error ? error.message : error
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return {
|
|
1908
|
+
mcpServers: [...connectedRequired.active, ...connectedBestEffort?.active ?? []],
|
|
1909
|
+
close: async () => {
|
|
1910
|
+
await connectedRequired.close();
|
|
1911
|
+
if (connectedBestEffort) {
|
|
1912
|
+
await connectedBestEffort.close();
|
|
1913
|
+
}
|
|
1914
|
+
},
|
|
1915
|
+
codexConnectorNamespaces
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
async function mcpServerRequestInit(settings, config, options) {
|
|
1919
|
+
if (isCodexAppsMcpServer(config)) {
|
|
1920
|
+
return await codexAppsMcpRequestInit(settings);
|
|
1921
|
+
}
|
|
1922
|
+
if (isFirstPartyMcpServer(settings, config)) {
|
|
1923
|
+
return await firstPartyMcpRequestInit(settings, config, options);
|
|
1924
|
+
}
|
|
1925
|
+
if (config.headers && Object.keys(config.headers).length > 0) {
|
|
1926
|
+
return { requestInit: { headers: { ...config.headers } } };
|
|
1927
|
+
}
|
|
1928
|
+
return {};
|
|
1929
|
+
}
|
|
1930
|
+
async function firstPartyMcpRequestInit(settings, config, options) {
|
|
1931
|
+
if (!isFirstPartyMcpServer(settings, config)) {
|
|
1932
|
+
return {};
|
|
1933
|
+
}
|
|
1934
|
+
const headers = {};
|
|
1935
|
+
if (settings.authRequired && settings.accessKey) {
|
|
1936
|
+
headers["x-opengeni-access-key"] = settings.accessKey;
|
|
1937
|
+
}
|
|
1938
|
+
if (settings.delegationSecret && options.accountId && options.workspaceId) {
|
|
1939
|
+
headers.authorization = `Bearer ${await signDelegatedAccessToken(settings.delegationSecret, {
|
|
1940
|
+
accountId: options.accountId,
|
|
1941
|
+
workspaceId: options.workspaceId,
|
|
1942
|
+
subjectId: options.subjectId ?? "worker:first-party-mcp",
|
|
1943
|
+
...options.subjectLabel ? { subjectLabel: options.subjectLabel } : {},
|
|
1944
|
+
permissions: options.firstPartyPermissions ?? firstPartyMcpPermissions,
|
|
1945
|
+
...options.sessionId ? { sessionId: options.sessionId } : {},
|
|
1946
|
+
exp: Math.floor(Date.now() / 1e3) + 60 * 60
|
|
1947
|
+
})}`;
|
|
1948
|
+
}
|
|
1949
|
+
if (Object.keys(headers).length === 0) {
|
|
1950
|
+
return {};
|
|
1951
|
+
}
|
|
1952
|
+
return {
|
|
1953
|
+
requestInit: {
|
|
1954
|
+
headers
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
async function codexAppsMcpRequestInit(settings) {
|
|
1959
|
+
const ctx = codexRequestStorage.getStore();
|
|
1960
|
+
if (!ctx) {
|
|
1961
|
+
return {};
|
|
1962
|
+
}
|
|
1963
|
+
let token;
|
|
1964
|
+
try {
|
|
1965
|
+
token = await ctx.getToken();
|
|
1966
|
+
} catch {
|
|
1967
|
+
return {};
|
|
1968
|
+
}
|
|
1969
|
+
const headers = {
|
|
1970
|
+
authorization: `Bearer ${token.accessToken}`,
|
|
1971
|
+
// The ChatGPT backend sits behind Cloudflare, which 403s requests bearing a
|
|
1972
|
+
// default runtime User-Agent (confirmed live: an HTML bot-block page, NOT an
|
|
1973
|
+
// auth failure). Send the codex client identity — the same originator/version/
|
|
1974
|
+
// User-Agent the model fetch uses — so the MCP connect handshake passes the edge.
|
|
1975
|
+
originator: CODEX_ORIGINATOR,
|
|
1976
|
+
"user-agent": `${CODEX_ORIGINATOR}/${ctx.clientVersion}`,
|
|
1977
|
+
version: ctx.clientVersion
|
|
1978
|
+
};
|
|
1979
|
+
if (token.chatgptAccountId) {
|
|
1980
|
+
headers["chatgpt-account-id"] = token.chatgptAccountId;
|
|
1981
|
+
}
|
|
1982
|
+
if (settings.codexProductSku) {
|
|
1983
|
+
headers["X-OpenAI-Product-Sku"] = settings.codexProductSku;
|
|
1984
|
+
}
|
|
1985
|
+
return { requestInit: { headers } };
|
|
1986
|
+
}
|
|
1987
|
+
var firstPartyMcpPermissions = [
|
|
1988
|
+
"workspace:read",
|
|
1989
|
+
"files:read",
|
|
1990
|
+
"documents:search",
|
|
1991
|
+
"scheduled_tasks:manage",
|
|
1992
|
+
"scheduled_tasks:run",
|
|
1993
|
+
"goals:manage",
|
|
1994
|
+
"sessions:read",
|
|
1995
|
+
"sessions:create",
|
|
1996
|
+
"sessions:control",
|
|
1997
|
+
"environments:use",
|
|
1998
|
+
"environments:manage",
|
|
1999
|
+
"github:use"
|
|
2000
|
+
];
|
|
2001
|
+
function isCodexAppsMcpServer(config) {
|
|
2002
|
+
return config.id === CODEX_APPS_MCP_SERVER_ID;
|
|
2003
|
+
}
|
|
2004
|
+
function isFirstPartyMcpServer(settings, config) {
|
|
2005
|
+
if (!["opengeni", "files", "docs"].includes(config.id)) {
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
if (config.url.includes("{workspaceId}")) {
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
const url = normalizeUrl(config.url);
|
|
2012
|
+
if (!url) {
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
return firstPartyMcpUrls(settings).some((candidate) => candidate === url);
|
|
2016
|
+
}
|
|
2017
|
+
function firstPartyMcpServerUrlForRun(settings, config, workspaceId) {
|
|
2018
|
+
if (!workspaceId || !["opengeni", "files", "docs"].includes(config.id)) {
|
|
2019
|
+
return null;
|
|
2020
|
+
}
|
|
2021
|
+
if (config.url.includes("{workspaceId}")) {
|
|
2022
|
+
return config.url.replaceAll("{workspaceId}", workspaceId);
|
|
2023
|
+
}
|
|
2024
|
+
if (!isFirstPartyMcpServer(settings, config)) {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
const rawBase = settings.opengeniMcpUrl?.includes("{workspaceId}") ? settings.opengeniMcpUrl.replaceAll("{workspaceId}", workspaceId) : settings.opengeniMcpUrl ? scopedMcpUrlFromConfiguredBase(settings.opengeniMcpUrl, workspaceId) : firstPartyMcpBaseUrl(settings).replaceAll("{workspaceId}", workspaceId);
|
|
2028
|
+
const url = new URL(rawBase);
|
|
2029
|
+
if (config.id === "docs") {
|
|
2030
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/docs`;
|
|
2031
|
+
}
|
|
2032
|
+
return url.toString();
|
|
2033
|
+
}
|
|
2034
|
+
function scopedMcpUrlFromConfiguredBase(raw, workspaceId) {
|
|
2035
|
+
const url = new URL(raw);
|
|
2036
|
+
url.pathname = `/v1/workspaces/${workspaceId}/mcp`;
|
|
2037
|
+
url.search = "";
|
|
2038
|
+
url.hash = "";
|
|
2039
|
+
return url.toString();
|
|
2040
|
+
}
|
|
2041
|
+
function firstPartyMcpUrls(settings) {
|
|
2042
|
+
const base = normalizeUrl(settings.opengeniMcpUrl ?? firstPartyMcpBaseUrl(settings));
|
|
2043
|
+
if (!base) {
|
|
2044
|
+
return [];
|
|
2045
|
+
}
|
|
2046
|
+
const docs = new URL(base);
|
|
2047
|
+
docs.pathname = `${docs.pathname.replace(/\/+$/, "")}/docs`;
|
|
2048
|
+
return [base, normalizeUrl(docs.toString())].filter((value) => Boolean(value));
|
|
2049
|
+
}
|
|
2050
|
+
function normalizeUrl(raw) {
|
|
2051
|
+
try {
|
|
2052
|
+
const url = new URL(raw);
|
|
2053
|
+
url.hash = "";
|
|
2054
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
2055
|
+
return url.toString();
|
|
2056
|
+
} catch {
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
function prefixedMcpToolName(registryId, toolName) {
|
|
2061
|
+
return `${registryId}__${toolName}`;
|
|
2062
|
+
}
|
|
2063
|
+
var PrefixedMcpServer = class {
|
|
2064
|
+
constructor(inner, registryId, allowedTools) {
|
|
2065
|
+
this.inner = inner;
|
|
2066
|
+
this.name = registryId;
|
|
2067
|
+
this.prefix = prefixedMcpToolName(registryId, "");
|
|
2068
|
+
this.cacheToolsList = inner.cacheToolsList;
|
|
2069
|
+
this.allowedTools = allowedTools ? new Set(allowedTools) : void 0;
|
|
2070
|
+
}
|
|
2071
|
+
inner;
|
|
2072
|
+
cacheToolsList;
|
|
2073
|
+
name;
|
|
2074
|
+
prefix;
|
|
2075
|
+
allowedTools;
|
|
2076
|
+
connect() {
|
|
2077
|
+
return this.inner.connect();
|
|
2078
|
+
}
|
|
2079
|
+
close() {
|
|
2080
|
+
return this.inner.close();
|
|
2081
|
+
}
|
|
2082
|
+
async listTools() {
|
|
2083
|
+
const tools = await this.inner.listTools();
|
|
2084
|
+
return tools.filter((tool2) => this.isAllowed(tool2.name)).map((tool2) => ({ ...tool2, name: prefixedMcpToolName(this.name, tool2.name) }));
|
|
2085
|
+
}
|
|
2086
|
+
async callTool(toolName, args, meta) {
|
|
2087
|
+
const unprefixed = this.unprefixToolName(toolName);
|
|
2088
|
+
if (!this.isAllowed(unprefixed)) {
|
|
2089
|
+
throw new Error(`MCP tool ${unprefixed} is not allowed for server ${this.name}`);
|
|
2090
|
+
}
|
|
2091
|
+
return await this.inner.callTool(unprefixed, args, meta);
|
|
2092
|
+
}
|
|
2093
|
+
invalidateToolsCache() {
|
|
2094
|
+
return this.inner.invalidateToolsCache();
|
|
2095
|
+
}
|
|
2096
|
+
async listResources(params) {
|
|
2097
|
+
const resourcesServer = this.inner;
|
|
2098
|
+
if (!resourcesServer.listResources) {
|
|
2099
|
+
throw new Error(`MCP server ${this.name} does not support resources`);
|
|
2100
|
+
}
|
|
2101
|
+
return await resourcesServer.listResources(params);
|
|
2102
|
+
}
|
|
2103
|
+
async listResourceTemplates(params) {
|
|
2104
|
+
const resourcesServer = this.inner;
|
|
2105
|
+
if (!resourcesServer.listResourceTemplates) {
|
|
2106
|
+
throw new Error(`MCP server ${this.name} does not support resource templates`);
|
|
2107
|
+
}
|
|
2108
|
+
return await resourcesServer.listResourceTemplates(params);
|
|
2109
|
+
}
|
|
2110
|
+
async readResource(uri) {
|
|
2111
|
+
const resourcesServer = this.inner;
|
|
2112
|
+
if (!resourcesServer.readResource) {
|
|
2113
|
+
throw new Error(`MCP server ${this.name} does not support resource reads`);
|
|
2114
|
+
}
|
|
2115
|
+
return await resourcesServer.readResource(uri);
|
|
2116
|
+
}
|
|
2117
|
+
isAllowed(toolName) {
|
|
2118
|
+
return !this.allowedTools || this.allowedTools.has(toolName);
|
|
2119
|
+
}
|
|
2120
|
+
unprefixToolName(toolName) {
|
|
2121
|
+
if (!toolName.startsWith(this.prefix)) {
|
|
2122
|
+
throw new Error(`MCP tool ${toolName} is missing expected ${this.name} prefix`);
|
|
2123
|
+
}
|
|
2124
|
+
return toolName.slice(this.prefix.length);
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
function guardAssembledInput(history, trailing, inputBudgetTokens) {
|
|
2128
|
+
if (typeof inputBudgetTokens !== "number" || inputBudgetTokens <= 0) {
|
|
2129
|
+
return [...history, trailing];
|
|
2130
|
+
}
|
|
2131
|
+
const trailingTokens = estimateItemTokens(trailing);
|
|
2132
|
+
const guarded = enforceInputBudget(
|
|
2133
|
+
history,
|
|
2134
|
+
inputBudgetTokens,
|
|
2135
|
+
trailingTokens
|
|
2136
|
+
);
|
|
2137
|
+
if (guarded.trimmed) {
|
|
2138
|
+
console.warn(
|
|
2139
|
+
`read-path budget guard trimmed ${guarded.droppedCount} oldest history item(s) to fit input budget (${inputBudgetTokens} tokens); the over-budget input was NOT sent`
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
return [...guarded.items, trailing];
|
|
2143
|
+
}
|
|
2144
|
+
async function prepareRunInput(agent, input, options = {}) {
|
|
2145
|
+
if (input.kind === "message") {
|
|
2146
|
+
if (input.historyItems && input.historyItems.length > 0) {
|
|
2147
|
+
const sandboxSessionState2 = input.sandboxEnvelope ? await restoredSandboxSessionStateFromEntry(input.sandboxEnvelope, options.sandboxClient) : void 0;
|
|
2148
|
+
const sanitizedHistory2 = sanitizeHistoryItemsForModel(
|
|
2149
|
+
input.historyItems
|
|
2150
|
+
);
|
|
2151
|
+
return {
|
|
2152
|
+
// Read-path budget guard: even after the orphan sanitizer, an assembled
|
|
2153
|
+
// input can exceed the model window (pre-turn compaction is best-effort
|
|
2154
|
+
// and can no-op). Trim the oldest history at a clean turn boundary so an
|
|
2155
|
+
// over-budget request is never sent. No-op when no budget is supplied.
|
|
2156
|
+
input: guardAssembledInput(
|
|
2157
|
+
sanitizedHistory2,
|
|
2158
|
+
{
|
|
2159
|
+
type: "message",
|
|
2160
|
+
role: "user",
|
|
2161
|
+
content: input.text
|
|
2162
|
+
},
|
|
2163
|
+
options.inputBudgetTokens
|
|
2164
|
+
),
|
|
2165
|
+
...sandboxSessionState2 ? { sandboxSessionState: sandboxSessionState2 } : {}
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
if (!input.serializedRunState || isClearedRunStateBlob(input.serializedRunState)) {
|
|
2169
|
+
return { input: input.text };
|
|
2170
|
+
}
|
|
2171
|
+
const state2 = await RunState.fromString(agent, input.serializedRunState);
|
|
2172
|
+
const sandboxSessionState = await restoredSandboxSessionState(state2, options.sandboxClient);
|
|
2173
|
+
const sanitizedHistory = sanitizeHistoryItemsForModel(
|
|
2174
|
+
state2.history
|
|
2175
|
+
);
|
|
2176
|
+
return {
|
|
2177
|
+
// Read-path budget guard (see the items path above): keep an over-budget
|
|
2178
|
+
// resumed history off the wire by trimming the oldest turns when a budget
|
|
2179
|
+
// is supplied.
|
|
2180
|
+
input: guardAssembledInput(
|
|
2181
|
+
sanitizedHistory,
|
|
2182
|
+
{
|
|
2183
|
+
type: "message",
|
|
2184
|
+
role: "user",
|
|
2185
|
+
content: input.text
|
|
2186
|
+
},
|
|
2187
|
+
options.inputBudgetTokens
|
|
2188
|
+
),
|
|
2189
|
+
...sandboxSessionState ? { sandboxSessionState } : {},
|
|
2190
|
+
serializedRunStateForSandbox: input.serializedRunState
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
if (isClearedRunStateBlob(input.serializedRunState)) {
|
|
2194
|
+
throw new Error("Cannot resume an approval: the session context was cleared, so the awaiting run state no longer exists.");
|
|
2195
|
+
}
|
|
2196
|
+
const state = await RunState.fromString(agent, input.serializedRunState);
|
|
2197
|
+
const interruptions = state.getInterruptions();
|
|
2198
|
+
const target = interruptions.find((item) => approvalIdentifier(item) === input.approvalId);
|
|
2199
|
+
if (!target) {
|
|
2200
|
+
throw new Error(`Approval not found in saved run state: ${input.approvalId}`);
|
|
2201
|
+
}
|
|
2202
|
+
if (input.decision === "approve") {
|
|
2203
|
+
state.approve(target);
|
|
2204
|
+
} else {
|
|
2205
|
+
state.reject(target, input.message ? { message: input.message } : void 0);
|
|
2206
|
+
}
|
|
2207
|
+
return { input: state };
|
|
2208
|
+
}
|
|
2209
|
+
var GENESIS_TITLE_DIRECTIVE = "This is the first turn of a new session. Before responding to the user, call the opengeni__set_session_title tool with a concise 3-7 word title that summarizes what this session is about, then address the user's request normally.";
|
|
2210
|
+
var stripProviderItemIdsFilter = ({ modelData }) => ({
|
|
2211
|
+
...modelData,
|
|
2212
|
+
input: modelData.input.map((item) => {
|
|
2213
|
+
if (item && typeof item === "object" && "id" in item) {
|
|
2214
|
+
const { id: _id, ...rest } = item;
|
|
2215
|
+
return rest;
|
|
2216
|
+
}
|
|
2217
|
+
return item;
|
|
2218
|
+
})
|
|
2219
|
+
});
|
|
2220
|
+
var normalizeComputerCallsFilter = ({ modelData }) => ({
|
|
2221
|
+
...modelData,
|
|
2222
|
+
input: normalizeComputerCallActions(
|
|
2223
|
+
modelData.input
|
|
2224
|
+
)
|
|
2225
|
+
});
|
|
2226
|
+
function composeCallModelInputFilters(filters) {
|
|
2227
|
+
return async (args) => {
|
|
2228
|
+
let modelData = args.modelData;
|
|
2229
|
+
for (const filter of filters) {
|
|
2230
|
+
modelData = await filter({ ...args, modelData });
|
|
2231
|
+
}
|
|
2232
|
+
return modelData;
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function callModelInputFilterForSettings(settings) {
|
|
2236
|
+
const filters = [normalizeComputerCallsFilter];
|
|
2237
|
+
if (settings.openaiProviderItemIds === "strip") {
|
|
2238
|
+
filters.push(stripProviderItemIdsFilter);
|
|
2239
|
+
}
|
|
2240
|
+
return composeCallModelInputFilters(filters);
|
|
2241
|
+
}
|
|
2242
|
+
async function runAgentStream(agent, input, settings, overrides = {}) {
|
|
2243
|
+
const prepared = typeof input === "string" || input instanceof RunState ? { input } : input;
|
|
2244
|
+
const environment = overrides.sandboxEnvironment ?? collectSandboxEnvironment2(settings);
|
|
2245
|
+
if (overrides.ownedSandbox) {
|
|
2246
|
+
const { client: ownedClient, session, sessionState } = overrides.ownedSandbox;
|
|
2247
|
+
const setupSession = overrides.ownedSandbox.setupSession ?? session;
|
|
2248
|
+
const runAs2 = sandboxRunAs(settings);
|
|
2249
|
+
const fileDownloads2 = sandboxFileDownloadsForAgent(agent);
|
|
2250
|
+
const resourceClient2 = fileDownloads2.length > 0 ? withSandboxFileDownloads(ownedClient, fileDownloads2, {
|
|
2251
|
+
...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
|
|
2252
|
+
...runAs2 ? { runAs: runAs2 } : {}
|
|
2253
|
+
}) : ownedClient;
|
|
2254
|
+
const ownedGitTokenSeed = gitTokenSeedForAgent(agent);
|
|
2255
|
+
const ownedHooks = [
|
|
2256
|
+
...sandboxLifecycleHooksForIds(sandboxLifecycleHookIds(settings)),
|
|
2257
|
+
...sandboxRepositoryCloneHooksForAgent(agent)
|
|
2258
|
+
];
|
|
2259
|
+
const ownedHookContext = {
|
|
2260
|
+
environment,
|
|
2261
|
+
...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
|
|
2262
|
+
...runAs2 ? { runAs: runAs2 } : {},
|
|
2263
|
+
...ownedGitTokenSeed ? { gitTokenSeed: ownedGitTokenSeed } : {}
|
|
2264
|
+
};
|
|
2265
|
+
if (agentActiveSandboxBackend.get(agent) !== "selfhosted") {
|
|
2266
|
+
await runBeforeAgentStartHooks(setupSession, ownedHooks, ownedHookContext);
|
|
2267
|
+
if (fileDownloads2.length > 0) {
|
|
2268
|
+
await materializeSandboxFileDownloads(setupSession, fileDownloads2, {
|
|
2269
|
+
...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
|
|
2270
|
+
...runAs2 ? { runAs: runAs2 } : {}
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
const decoratedClient = withSandboxLifecycleHooks(resourceClient2, ownedHooks, ownedHookContext);
|
|
2275
|
+
const ownedFilter = composeCallModelInputFilters(
|
|
2276
|
+
[callModelInputFilterForSettings(settings), overrides.callModelInputFilter].filter(
|
|
2277
|
+
(f) => Boolean(f)
|
|
2278
|
+
)
|
|
2279
|
+
);
|
|
2280
|
+
const ownedRunOptions = {
|
|
2281
|
+
stream: true,
|
|
2282
|
+
maxTurns: settings.agentMaxModelCallsPerTurn,
|
|
2283
|
+
callModelInputFilter: ownedFilter
|
|
2284
|
+
};
|
|
2285
|
+
ownedRunOptions.sandbox = {
|
|
2286
|
+
client: decoratedClient,
|
|
2287
|
+
session,
|
|
2288
|
+
...sessionState ? { sessionState } : {}
|
|
2289
|
+
};
|
|
2290
|
+
return await runScopedRunner(settings).run(agent, prepared.input, ownedRunOptions);
|
|
2291
|
+
}
|
|
2292
|
+
const rawClient = overrides.sandboxClient ?? createSandboxClient(settings, environment);
|
|
2293
|
+
const refreshedClient = rawClient ? withManifestRefreshOnResume(rawClient, agent.defaultManifest) : void 0;
|
|
2294
|
+
const runAs = sandboxRunAs(settings);
|
|
2295
|
+
const fileDownloads = sandboxFileDownloadsForAgent(agent);
|
|
2296
|
+
const resourceClient = refreshedClient && fileDownloads.length > 0 ? withSandboxFileDownloads(refreshedClient, fileDownloads, {
|
|
2297
|
+
...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
|
|
2298
|
+
...runAs ? { runAs } : {}
|
|
2299
|
+
}) : refreshedClient;
|
|
2300
|
+
const gitTokenSeed = gitTokenSeedForAgent(agent);
|
|
2301
|
+
const client = resourceClient ? withSandboxLifecycleHooks(resourceClient, [
|
|
2302
|
+
...sandboxLifecycleHooksForIds(sandboxLifecycleHookIds(settings)),
|
|
2303
|
+
...sandboxRepositoryCloneHooksForAgent(agent)
|
|
2304
|
+
], {
|
|
2305
|
+
environment,
|
|
2306
|
+
...overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {},
|
|
2307
|
+
...runAs ? { runAs } : {},
|
|
2308
|
+
...gitTokenSeed ? { gitTokenSeed } : {}
|
|
2309
|
+
}) : void 0;
|
|
2310
|
+
const sandboxSessionState = prepared.sandboxSessionState ?? (prepared.serializedRunStateForSandbox && client ? await restoredSandboxSessionState(await RunState.fromString(agent, prepared.serializedRunStateForSandbox), client) : void 0);
|
|
2311
|
+
const callModelInputFilter = composeCallModelInputFilters(
|
|
2312
|
+
[callModelInputFilterForSettings(settings), overrides.callModelInputFilter].filter(
|
|
2313
|
+
(f) => Boolean(f)
|
|
2314
|
+
)
|
|
2315
|
+
);
|
|
2316
|
+
const runOptions = {
|
|
2317
|
+
stream: true,
|
|
2318
|
+
maxTurns: settings.agentMaxModelCallsPerTurn,
|
|
2319
|
+
// Strip provider-assigned item ids from every model call (turn-start
|
|
2320
|
+
// history replay AND mid-turn follow-ups) so requests never depend on the
|
|
2321
|
+
// provider's server-side response store. A stored response can vanish
|
|
2322
|
+
// between two calls of the same turn, failing the run with 400 "Item with
|
|
2323
|
+
// id 'rs_…' not found"; with the ids gone the request is self-contained.
|
|
2324
|
+
callModelInputFilter
|
|
2325
|
+
};
|
|
2326
|
+
void settings.disableOpenaiTracing;
|
|
2327
|
+
if (client) {
|
|
2328
|
+
runOptions.sandbox = {
|
|
2329
|
+
client,
|
|
2330
|
+
...sandboxSessionState ? { sessionState: sandboxSessionState } : {}
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
return await runScopedRunner(settings).run(agent, prepared.input, runOptions);
|
|
2334
|
+
}
|
|
2335
|
+
function runScopedRunner(settings) {
|
|
2336
|
+
return new Runner({ modelProvider: new MultiProviderModelProvider(settings) });
|
|
2337
|
+
}
|
|
2338
|
+
function maxTurnsExceededRunState(error) {
|
|
2339
|
+
if (!(error instanceof MaxTurnsExceededError)) {
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
try {
|
|
2343
|
+
return { serializedRunState: error.state ? error.state.toString() : null };
|
|
2344
|
+
} catch {
|
|
2345
|
+
return { serializedRunState: null };
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
function agentsErrorRunState(error) {
|
|
2349
|
+
if (!(error instanceof AgentsError) || !error.state) {
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
try {
|
|
2353
|
+
return error.state.toString();
|
|
2354
|
+
} catch {
|
|
2355
|
+
return null;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
function withManifestRefreshOnResume(client, targetManifest) {
|
|
2359
|
+
if (!targetManifest || !client.resume) {
|
|
2360
|
+
return client;
|
|
2361
|
+
}
|
|
2362
|
+
return {
|
|
2363
|
+
backendId: client.backendId,
|
|
2364
|
+
...client.supportsDefaultOptions !== void 0 ? { supportsDefaultOptions: client.supportsDefaultOptions } : {},
|
|
2365
|
+
...client.create ? { create: async (...args) => await client.create(...args) } : {},
|
|
2366
|
+
resume: async (state) => {
|
|
2367
|
+
const session = await client.resume(state);
|
|
2368
|
+
await applyMissingManifestEntries(session, targetManifest);
|
|
2369
|
+
return session;
|
|
2370
|
+
},
|
|
2371
|
+
...client.delete ? { delete: async (state) => await client.delete(state) } : {},
|
|
2372
|
+
...client.serializeSessionState ? { serializeSessionState: async (state, options) => await client.serializeSessionState(state, options) } : {},
|
|
2373
|
+
...client.canPersistOwnedSessionState ? { canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state) } : {},
|
|
2374
|
+
...client.canReusePreservedOwnedSession ? { canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state) } : {},
|
|
2375
|
+
...client.deserializeSessionState ? { deserializeSessionState: async (state) => await client.deserializeSessionState(state) } : {}
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
async function applyMissingManifestEntries(session, targetManifest) {
|
|
2379
|
+
const currentManifestValue = session.state?.manifest;
|
|
2380
|
+
const currentManifest = currentManifestValue ? ensureManifest(currentManifestValue) : void 0;
|
|
2381
|
+
const target = ensureManifest(targetManifest);
|
|
2382
|
+
if (!currentManifest) {
|
|
2383
|
+
if (Object.keys(target.entries).length === 0) {
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
throw new Error("Resumed sandbox session cannot apply new manifest entries because current manifest state is unavailable");
|
|
2387
|
+
}
|
|
2388
|
+
if (!session.applyManifest && !session.materializeEntry) {
|
|
2389
|
+
if (Object.keys(target.entries).length === 0) {
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
throw new Error("Resumed sandbox session cannot apply new manifest entries because it does not support applyManifest() or materializeEntry()");
|
|
2393
|
+
}
|
|
2394
|
+
if (Object.keys(target.entries).length === 0) {
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
if (currentManifest.root !== target.root) {
|
|
2398
|
+
throw new Error("Cannot apply per-turn resources to a sandbox with a different manifest root");
|
|
2399
|
+
}
|
|
2400
|
+
const entries = {};
|
|
2401
|
+
for (const [path, entry] of Object.entries(target.entries)) {
|
|
2402
|
+
const existing = currentManifest.entries[path];
|
|
2403
|
+
if (existing === void 0) {
|
|
2404
|
+
entries[path] = entry;
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
if (stableJson(existing) !== stableJson(entry)) {
|
|
2408
|
+
throw new Error(`Cannot replace existing sandbox manifest entry: ${path}`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
const environmentChanged = stableJson(currentManifest.environment) !== stableJson(target.environment);
|
|
2412
|
+
if (environmentChanged && !session.applyManifest) {
|
|
2413
|
+
throw new Error("Resumed sandbox session cannot refresh manifest environment because it does not support applyManifest()");
|
|
2414
|
+
}
|
|
2415
|
+
if (Object.keys(entries).length === 0 && !environmentChanged) {
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
const extraPathGrants = mergePathGrants(currentManifest.extraPathGrants, target.extraPathGrants);
|
|
2419
|
+
const delta = new Manifest({
|
|
2420
|
+
root: currentManifest.root,
|
|
2421
|
+
entries,
|
|
2422
|
+
environment: target.environment,
|
|
2423
|
+
...extraPathGrants.length ? { extraPathGrants } : {}
|
|
2424
|
+
});
|
|
2425
|
+
if (session.applyManifest) {
|
|
2426
|
+
await session.applyManifest(delta);
|
|
2427
|
+
} else {
|
|
2428
|
+
for (const [path, entry] of Object.entries(entries)) {
|
|
2429
|
+
await session.materializeEntry({ path, entry });
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
session.state.manifest = new Manifest({
|
|
2433
|
+
root: currentManifest.root,
|
|
2434
|
+
environment: environmentChanged ? target.environment : currentManifest.environment,
|
|
2435
|
+
entries: {
|
|
2436
|
+
...currentManifest.entries,
|
|
2437
|
+
...entries
|
|
2438
|
+
},
|
|
2439
|
+
...extraPathGrants.length ? { extraPathGrants } : {}
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
function mergePathGrants(current, target) {
|
|
2443
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2444
|
+
for (const grant of [...current ?? [], ...target ?? []]) {
|
|
2445
|
+
merged.set(grant.path, grant);
|
|
2446
|
+
}
|
|
2447
|
+
return [...merged.values()];
|
|
2448
|
+
}
|
|
2449
|
+
function withSandboxFileDownloads(client, downloads, context = {}) {
|
|
2450
|
+
const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
|
|
2451
|
+
if (normalizedDownloads.length === 0) {
|
|
2452
|
+
return client;
|
|
2453
|
+
}
|
|
2454
|
+
const completed = /* @__PURE__ */ new WeakSet();
|
|
2455
|
+
const wrapSession = async (session) => {
|
|
2456
|
+
if (typeof session === "object" && session !== null && !completed.has(session)) {
|
|
2457
|
+
await materializeSandboxFileDownloads(session, normalizedDownloads, context);
|
|
2458
|
+
completed.add(session);
|
|
2459
|
+
}
|
|
2460
|
+
return session;
|
|
2461
|
+
};
|
|
2462
|
+
return {
|
|
2463
|
+
backendId: client.backendId,
|
|
2464
|
+
...client.supportsDefaultOptions !== void 0 ? { supportsDefaultOptions: client.supportsDefaultOptions } : {},
|
|
2465
|
+
...client.create ? { create: async (...args) => await wrapSession(await client.create(...args)) } : {},
|
|
2466
|
+
...client.resume ? { resume: async (state) => await wrapSession(await client.resume(state)) } : {},
|
|
2467
|
+
...client.delete ? { delete: async (state) => await client.delete(state) } : {},
|
|
2468
|
+
...client.serializeSessionState ? { serializeSessionState: async (state, options) => await client.serializeSessionState(state, options) } : {},
|
|
2469
|
+
...client.canPersistOwnedSessionState ? { canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state) } : {},
|
|
2470
|
+
...client.canReusePreservedOwnedSession ? { canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state) } : {},
|
|
2471
|
+
...client.deserializeSessionState ? { deserializeSessionState: async (state) => await client.deserializeSessionState(state) } : {}
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
async function materializeSandboxFileDownloads(session, downloads, context = {}) {
|
|
2475
|
+
const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
|
|
2476
|
+
if (normalizedDownloads.length === 0) {
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
if (!session.exec && !session.execCommand) {
|
|
2480
|
+
throw new Error("Sandbox file download materialization requires command execution support");
|
|
2481
|
+
}
|
|
2482
|
+
for (const download of normalizedDownloads) {
|
|
2483
|
+
const targetPath = sandboxDownloadTargetPath(download);
|
|
2484
|
+
const payload = {
|
|
2485
|
+
fileId: download.fileId,
|
|
2486
|
+
path: targetPath,
|
|
2487
|
+
sizeBytes: download.sizeBytes ?? null,
|
|
2488
|
+
expiresAt: download.expiresAt ? new Date(download.expiresAt).toISOString() : null
|
|
2489
|
+
};
|
|
2490
|
+
await context.onRuntimeEvent?.({ type: "sandbox.operation.started", payload: { name: "file-resource-download", ...payload } });
|
|
2491
|
+
try {
|
|
2492
|
+
const result = session.exec ? await session.exec({
|
|
2493
|
+
cmd: sandboxFileDownloadCommand(download, targetPath),
|
|
2494
|
+
workdir: "/workspace",
|
|
2495
|
+
...context.runAs ? { runAs: context.runAs } : {},
|
|
2496
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
2497
|
+
maxOutputTokens: 2e4
|
|
2498
|
+
}) : await session.execCommand({
|
|
2499
|
+
cmd: sandboxFileDownloadCommand(download, targetPath),
|
|
2500
|
+
workdir: "/workspace",
|
|
2501
|
+
...context.runAs ? { runAs: context.runAs } : {},
|
|
2502
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
2503
|
+
maxOutputTokens: 2e4
|
|
2504
|
+
});
|
|
2505
|
+
assertSandboxCommandSucceeded(result, `Sandbox file resource download ${download.fileId}`);
|
|
2506
|
+
await context.onRuntimeEvent?.({ type: "sandbox.operation.completed", payload: { name: "file-resource-download", ...payload } });
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
await context.onRuntimeEvent?.({
|
|
2509
|
+
type: "sandbox.operation.failed",
|
|
2510
|
+
payload: {
|
|
2511
|
+
name: "file-resource-download",
|
|
2512
|
+
...payload,
|
|
2513
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
throw error;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
function sandboxFileDownloadsForAgent(agent) {
|
|
2521
|
+
return typeof agent === "object" && agent !== null ? [...agentFileDownloads.get(agent) ?? []] : [];
|
|
2522
|
+
}
|
|
2523
|
+
function ensureManifest(manifest) {
|
|
2524
|
+
if (manifest instanceof Manifest && typeof manifest.mountTargetsForMaterialization === "function") {
|
|
2525
|
+
return manifest;
|
|
2526
|
+
}
|
|
2527
|
+
return new Manifest({
|
|
2528
|
+
...manifest.root ? { root: manifest.root } : {},
|
|
2529
|
+
entries: manifest.entries ?? {},
|
|
2530
|
+
environment: manifest.environment ?? {},
|
|
2531
|
+
...manifest.extraPathGrants?.length ? { extraPathGrants: manifest.extraPathGrants } : {}
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
function toImageBytes(data) {
|
|
2535
|
+
if (data instanceof Uint8Array) {
|
|
2536
|
+
return data;
|
|
2537
|
+
}
|
|
2538
|
+
if (Array.isArray(data)) {
|
|
2539
|
+
return data.every((n) => typeof n === "number") ? Uint8Array.from(data) : null;
|
|
2540
|
+
}
|
|
2541
|
+
if (data && typeof data === "object") {
|
|
2542
|
+
const values = Object.values(data);
|
|
2543
|
+
if (values.length > 0 && values.every((n) => typeof n === "number")) {
|
|
2544
|
+
return Uint8Array.from(values);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
function structuredImageToDataUrl(value) {
|
|
2550
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2551
|
+
return null;
|
|
2552
|
+
}
|
|
2553
|
+
const v = value;
|
|
2554
|
+
if (v.type === "input_image") {
|
|
2555
|
+
return typeof v.image === "string" && v.image.length > 0 ? v.image : null;
|
|
2556
|
+
}
|
|
2557
|
+
if (v.type !== "image" || !v.image || typeof v.image !== "object") {
|
|
2558
|
+
return null;
|
|
2559
|
+
}
|
|
2560
|
+
const image = v.image;
|
|
2561
|
+
if (typeof image.url === "string" && image.url.length > 0) {
|
|
2562
|
+
return image.url;
|
|
2563
|
+
}
|
|
2564
|
+
const mediaType = typeof image.mediaType === "string" && image.mediaType.length > 0 ? image.mediaType : "image/png";
|
|
2565
|
+
if (typeof image.data === "string") {
|
|
2566
|
+
return image.data.startsWith("data:") ? image.data : `data:${mediaType};base64,${image.data}`;
|
|
2567
|
+
}
|
|
2568
|
+
const bytes = toImageBytes(image.data);
|
|
2569
|
+
return bytes ? `data:${mediaType};base64,${Buffer.from(bytes).toString("base64")}` : null;
|
|
2570
|
+
}
|
|
2571
|
+
function normalizeToolOutputForEvent(output) {
|
|
2572
|
+
const single = structuredImageToDataUrl(output);
|
|
2573
|
+
if (single !== null) {
|
|
2574
|
+
return single;
|
|
2575
|
+
}
|
|
2576
|
+
if (Array.isArray(output)) {
|
|
2577
|
+
const normalized = output.map((el) => structuredImageToDataUrl(el) ?? el);
|
|
2578
|
+
if (normalized.length === 1 && typeof normalized[0] === "string") {
|
|
2579
|
+
return normalized[0];
|
|
2580
|
+
}
|
|
2581
|
+
return normalized;
|
|
2582
|
+
}
|
|
2583
|
+
return output;
|
|
2584
|
+
}
|
|
2585
|
+
function normalizeSdkEvent(event) {
|
|
2586
|
+
const out = [];
|
|
2587
|
+
if (event.type === "raw_model_stream_event") {
|
|
2588
|
+
const data = event.data;
|
|
2589
|
+
if (data?.type === "output_text_delta" && typeof data.delta === "string") {
|
|
2590
|
+
out.push({ type: "agent.message.delta", payload: { text: data.delta } });
|
|
2591
|
+
return out;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
if (isOpenAIResponsesRawModelStreamEvent(event)) {
|
|
2595
|
+
const raw = event.data?.event;
|
|
2596
|
+
if (raw?.type === "response.reasoning_summary_text.delta" && typeof raw.delta === "string") {
|
|
2597
|
+
out.push({ type: "agent.reasoning.delta", payload: { text: raw.delta } });
|
|
2598
|
+
}
|
|
2599
|
+
return out;
|
|
2600
|
+
}
|
|
2601
|
+
if (event.type === "agent_updated_stream_event") {
|
|
2602
|
+
out.push({ type: "agent.updated", payload: { agent: event.agent?.name ?? null } });
|
|
2603
|
+
return out;
|
|
2604
|
+
}
|
|
2605
|
+
if (event.type !== "run_item_stream_event") {
|
|
2606
|
+
return out;
|
|
2607
|
+
}
|
|
2608
|
+
const item = event.item;
|
|
2609
|
+
if (!item) {
|
|
2610
|
+
return out;
|
|
2611
|
+
}
|
|
2612
|
+
if (item.type === "tool_call_item") {
|
|
2613
|
+
const raw = item.rawItem ?? {};
|
|
2614
|
+
out.push({
|
|
2615
|
+
type: "agent.toolCall.created",
|
|
2616
|
+
payload: {
|
|
2617
|
+
id: raw.callId ?? raw.id ?? item.id ?? null,
|
|
2618
|
+
name: raw.name ?? raw.type ?? "tool",
|
|
2619
|
+
arguments: raw.arguments ?? raw.input ?? null,
|
|
2620
|
+
raw
|
|
2621
|
+
}
|
|
2622
|
+
});
|
|
2623
|
+
} else if (item.type === "tool_call_output_item") {
|
|
2624
|
+
out.push({
|
|
2625
|
+
type: "agent.toolCall.output",
|
|
2626
|
+
payload: {
|
|
2627
|
+
id: item.rawItem?.callId ?? item.id ?? null,
|
|
2628
|
+
// Compact any structured/binary image output to a data-URL string so a
|
|
2629
|
+
// screenshot never bloats session_events ~10x as an object-of-numbers.
|
|
2630
|
+
output: normalizeToolOutputForEvent(item.output)
|
|
2631
|
+
}
|
|
2632
|
+
});
|
|
2633
|
+
} else if (item.type === "tool_search_call_item") {
|
|
2634
|
+
const raw = item.rawItem ?? {};
|
|
2635
|
+
out.push({
|
|
2636
|
+
type: "agent.toolCall.created",
|
|
2637
|
+
payload: {
|
|
2638
|
+
id: raw.call_id ?? raw.callId ?? raw.id ?? item.id ?? null,
|
|
2639
|
+
name: "tool_search",
|
|
2640
|
+
arguments: raw.arguments ?? null,
|
|
2641
|
+
raw
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
} else if (item.type === "tool_search_output_item") {
|
|
2645
|
+
const raw = item.rawItem ?? {};
|
|
2646
|
+
const disclosed = Array.isArray(raw.tools) ? raw.tools.map((tool2) => typeof tool2?.name === "string" ? tool2.name : "").filter(Boolean) : [];
|
|
2647
|
+
out.push({
|
|
2648
|
+
type: "agent.toolCall.output",
|
|
2649
|
+
payload: {
|
|
2650
|
+
id: raw.call_id ?? raw.callId ?? item.id ?? null,
|
|
2651
|
+
output: { type: "text", text: disclosed.length > 0 ? `Disclosed tools: ${disclosed.join(", ")}` : "No matching tools found." }
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
} else if (item.type === "message_output_item") {
|
|
2655
|
+
const text = typeof item.text === "string" ? item.text : void 0;
|
|
2656
|
+
if (text) {
|
|
2657
|
+
out.push({ type: "agent.message.completed", payload: { text } });
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
return out;
|
|
2661
|
+
}
|
|
2662
|
+
function modelResponseUsageFromSdkEvent(event) {
|
|
2663
|
+
const response = modelResponseFromSdkEvent(event);
|
|
2664
|
+
const usage = usageFromResponse(response);
|
|
2665
|
+
if (!usage) {
|
|
2666
|
+
return null;
|
|
2667
|
+
}
|
|
2668
|
+
const responseId = typeof response?.id === "string" ? response.id : typeof response?.responseId === "string" ? response.responseId : void 0;
|
|
2669
|
+
return {
|
|
2670
|
+
...responseId ? { responseId } : {},
|
|
2671
|
+
usage
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
function modelResponseFromSdkEvent(event) {
|
|
2675
|
+
if (event.type === "raw_model_stream_event") {
|
|
2676
|
+
const data = event.data;
|
|
2677
|
+
if (data?.type === "response_done") {
|
|
2678
|
+
return data.response;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (isOpenAIResponsesRawModelStreamEvent(event)) {
|
|
2682
|
+
const raw = event.data?.event;
|
|
2683
|
+
if (raw?.type === "response.completed") {
|
|
2684
|
+
return raw.response;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2689
|
+
function usageFromResponse(response) {
|
|
2690
|
+
const raw = response?.usage;
|
|
2691
|
+
if (!raw || typeof raw !== "object") {
|
|
2692
|
+
return null;
|
|
2693
|
+
}
|
|
2694
|
+
const usage = {
|
|
2695
|
+
...numberProp(raw, "inputTokens", "inputTokens", "input_tokens"),
|
|
2696
|
+
...numberProp(raw, "outputTokens", "outputTokens", "output_tokens"),
|
|
2697
|
+
...numberProp(raw, "totalTokens", "totalTokens", "total_tokens"),
|
|
2698
|
+
...inputTokenDetailsProp(raw)
|
|
2699
|
+
};
|
|
2700
|
+
return Object.keys(usage).length > 0 ? usage : null;
|
|
2701
|
+
}
|
|
2702
|
+
function numberProp(raw, outputKey, camel, snake) {
|
|
2703
|
+
const value = raw[camel] ?? raw[snake];
|
|
2704
|
+
return typeof value === "number" && Number.isFinite(value) ? { [outputKey]: value } : {};
|
|
2705
|
+
}
|
|
2706
|
+
function inputTokenDetailsProp(raw) {
|
|
2707
|
+
const details = raw.inputTokensDetails ?? raw.input_tokens_details;
|
|
2708
|
+
if (!details || typeof details !== "object") {
|
|
2709
|
+
return {};
|
|
2710
|
+
}
|
|
2711
|
+
return { inputTokensDetails: details };
|
|
2712
|
+
}
|
|
2713
|
+
function serializeApprovals(interruptions) {
|
|
2714
|
+
return interruptions.map((item) => {
|
|
2715
|
+
if (typeof item?.toJSON === "function") {
|
|
2716
|
+
return item.toJSON();
|
|
2717
|
+
}
|
|
2718
|
+
return {
|
|
2719
|
+
id: approvalIdentifier(item),
|
|
2720
|
+
name: item?.name ?? item?.rawItem?.name ?? "tool",
|
|
2721
|
+
arguments: item?.arguments ?? item?.rawItem?.arguments ?? null,
|
|
2722
|
+
raw: item
|
|
2723
|
+
};
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
function buildManifest(settings, resources, environment = collectSandboxEnvironment2(settings), fileResourceDownloads = []) {
|
|
2727
|
+
const entries = {};
|
|
2728
|
+
const downloadsByFileId = new Map(normalizeSandboxFileDownloads(fileResourceDownloads).map((download) => [download.fileId, download]));
|
|
2729
|
+
for (const resource of resources) {
|
|
2730
|
+
if (resource.kind === "repository") {
|
|
2731
|
+
const url = new URL(resource.uri);
|
|
2732
|
+
const host = url.hostname.toLowerCase();
|
|
2733
|
+
const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
2734
|
+
const mountPath = normalizeManifestPath(resource.mountPath ?? `repos/${repo}`);
|
|
2735
|
+
if (repositoryUsesSandboxClone(settings, resource)) {
|
|
2736
|
+
entries[mountPath] = dir();
|
|
2737
|
+
continue;
|
|
2738
|
+
}
|
|
2739
|
+
entries[mountPath] = gitRepo({
|
|
2740
|
+
host,
|
|
2741
|
+
repo,
|
|
2742
|
+
ref: resource.ref,
|
|
2743
|
+
...resource.subpath ? { subpath: normalizeManifestPath(resource.subpath) } : {}
|
|
2744
|
+
});
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
if (resource.kind === "file") {
|
|
2748
|
+
const mountPath = normalizeManifestPath(resource.mountPath ?? `files/${resource.fileId}`);
|
|
2749
|
+
const download = downloadsByFileId.get(resource.fileId);
|
|
2750
|
+
entries[mountPath] = download ? sandboxDownloadDirectory(download, mountPath) : objectStorageFileMount(settings, `files/${resource.fileId}/original`);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
return new Manifest({
|
|
2754
|
+
root: "/workspace",
|
|
2755
|
+
entries,
|
|
2756
|
+
environment
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
function sandboxDownloadDirectory(download, mountPath) {
|
|
2760
|
+
if (download.mountPath !== mountPath) {
|
|
2761
|
+
throw new Error(`File download materialization path mismatch for ${download.fileId}: expected ${mountPath}, got ${download.mountPath}`);
|
|
2762
|
+
}
|
|
2763
|
+
assertSafeSandboxFilename(download.filename, download.fileId);
|
|
2764
|
+
if (download.content) {
|
|
2765
|
+
return dir({
|
|
2766
|
+
children: {
|
|
2767
|
+
[download.filename]: file({ content: download.content })
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
return dir();
|
|
2772
|
+
}
|
|
2773
|
+
function objectStorageFileMount(settings, prefix) {
|
|
2774
|
+
const nativeBucketMount = CAPABILITY_DESCRIPTORS2[settings.sandboxBackend].nativeBucketMount;
|
|
2775
|
+
if (settings.objectStorageBackend === "azure-blob") {
|
|
2776
|
+
if (nativeBucketMount) {
|
|
2777
|
+
throw new Error("Modal sandbox Azure Blob file resources require pre-signed download materialization because the current OpenAI Agents SDK Modal client does not support Azure Blob mount entries.");
|
|
2778
|
+
}
|
|
2779
|
+
const config2 = azureBlobMountConfig(settings);
|
|
2780
|
+
return azureBlobMount({
|
|
2781
|
+
container: config2.container,
|
|
2782
|
+
prefix,
|
|
2783
|
+
accountName: config2.accountName,
|
|
2784
|
+
accountKey: config2.accountKey,
|
|
2785
|
+
endpointUrl: config2.endpointUrl,
|
|
2786
|
+
readOnly: true,
|
|
2787
|
+
mountStrategy: inContainerMountStrategy({ pattern: { type: "rclone", mode: "fuse" } })
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
if (settings.objectStorageBackend === "aws-s3" || settings.objectStorageBackend === "gcs") {
|
|
2791
|
+
throw new Error(`${settings.objectStorageBackend} file resources require pre-signed download materialization`);
|
|
2792
|
+
}
|
|
2793
|
+
const config = s3CompatibleMountConfig(settings);
|
|
2794
|
+
return s3Mount({
|
|
2795
|
+
bucket: config.bucket,
|
|
2796
|
+
prefix,
|
|
2797
|
+
endpointUrl: config.endpointUrl,
|
|
2798
|
+
region: config.region,
|
|
2799
|
+
s3Provider: config.s3Provider,
|
|
2800
|
+
accessKeyId: config.accessKeyId,
|
|
2801
|
+
secretAccessKey: config.secretAccessKey,
|
|
2802
|
+
readOnly: true,
|
|
2803
|
+
mountStrategy: nativeBucketMount ? new ModalCloudBucketMountStrategy() : inContainerMountStrategy({ pattern: { type: "rclone", mode: "fuse" } })
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
function s3CompatibleMountConfig(settings) {
|
|
2807
|
+
const endpointUrl = settings.objectStorageSandboxEndpoint ?? settings.objectStorageEndpoint;
|
|
2808
|
+
if (!endpointUrl || !settings.objectStorageAccessKeyId || !settings.objectStorageSecretAccessKey) {
|
|
2809
|
+
throw new Error("File resources require configured S3-compatible object storage");
|
|
2810
|
+
}
|
|
2811
|
+
return {
|
|
2812
|
+
bucket: settings.objectStorageBucket,
|
|
2813
|
+
endpointUrl,
|
|
2814
|
+
region: settings.objectStorageRegion,
|
|
2815
|
+
s3Provider: settings.objectStorageS3Provider,
|
|
2816
|
+
accessKeyId: settings.objectStorageAccessKeyId,
|
|
2817
|
+
secretAccessKey: settings.objectStorageSecretAccessKey
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function azureBlobMountConfig(settings) {
|
|
2821
|
+
const parsed = settings.objectStorageAzureConnectionString ? parseAzureConnectionString(settings.objectStorageAzureConnectionString) : {};
|
|
2822
|
+
const accountName = settings.objectStorageAzureAccountName ?? parsed.AccountName;
|
|
2823
|
+
const accountKey = settings.objectStorageAzureAccountKey ?? parsed.AccountKey;
|
|
2824
|
+
if (!accountName || !accountKey) {
|
|
2825
|
+
throw new Error("File resources require Azure Blob account name and account key");
|
|
2826
|
+
}
|
|
2827
|
+
const endpointUrl = azureBlobManifestEndpoint(settings.objectStorageAzureEndpoint ?? parsed.BlobEndpoint, accountName);
|
|
2828
|
+
return {
|
|
2829
|
+
container: settings.objectStorageBucket,
|
|
2830
|
+
accountName,
|
|
2831
|
+
accountKey,
|
|
2832
|
+
...endpointUrl ? { endpointUrl } : {}
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
function azureBlobManifestEndpoint(endpoint, accountName) {
|
|
2836
|
+
if (!endpoint) {
|
|
2837
|
+
return void 0;
|
|
2838
|
+
}
|
|
2839
|
+
const normalized = endpoint.replace(/\/+$/, "");
|
|
2840
|
+
const standardAccountEndpoint = `https://${accountName}.blob.core.windows.net`;
|
|
2841
|
+
return normalized === standardAccountEndpoint ? void 0 : normalized;
|
|
2842
|
+
}
|
|
2843
|
+
function parseAzureConnectionString(value) {
|
|
2844
|
+
return Object.fromEntries(value.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
2845
|
+
const index = part.indexOf("=");
|
|
2846
|
+
return index === -1 ? [part, ""] : [part.slice(0, index), part.slice(index + 1)];
|
|
2847
|
+
}));
|
|
2848
|
+
}
|
|
2849
|
+
function normalizeManifestPath(path) {
|
|
2850
|
+
const normalized = path.replace(/^\/+|\/+$/g, "");
|
|
2851
|
+
if (!normalized || normalized.includes("..")) {
|
|
2852
|
+
throw new Error(`Invalid sandbox resource path: ${path}`);
|
|
2853
|
+
}
|
|
2854
|
+
return normalized;
|
|
2855
|
+
}
|
|
2856
|
+
function normalizeSandboxFileDownloads(downloads) {
|
|
2857
|
+
return downloads.map((download) => {
|
|
2858
|
+
const mountPath = normalizeManifestPath(download.mountPath);
|
|
2859
|
+
assertSafeSandboxFilename(download.filename, download.fileId);
|
|
2860
|
+
if (!download.content && !download.url?.trim()) {
|
|
2861
|
+
throw new Error(`File download materialization requires content or a URL for ${download.fileId}`);
|
|
2862
|
+
}
|
|
2863
|
+
return {
|
|
2864
|
+
...download,
|
|
2865
|
+
mountPath
|
|
2866
|
+
};
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
function assertSafeSandboxFilename(filename, fileId) {
|
|
2870
|
+
if (!filename || filename.includes("/") || filename.includes("\\") || filename === "." || filename === ".." || filename.includes("..")) {
|
|
2871
|
+
throw new Error(`Invalid sandbox file name for ${fileId}: ${filename}`);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
function sandboxDownloadTargetPath(download) {
|
|
2875
|
+
return posixPath.join("/workspace", download.mountPath, download.filename);
|
|
2876
|
+
}
|
|
2877
|
+
function sandboxFileDownloadCommand(download, targetPath) {
|
|
2878
|
+
if (!download.url) {
|
|
2879
|
+
throw new Error(`File download materialization URL is empty for ${download.fileId}`);
|
|
2880
|
+
}
|
|
2881
|
+
const targetDir = posixPath.dirname(targetPath);
|
|
2882
|
+
const tmpPath = `${targetPath}.opengeni-download-$$`;
|
|
2883
|
+
return [
|
|
2884
|
+
"set -euo pipefail",
|
|
2885
|
+
`mkdir -p -- ${shellQuote(targetDir)}`,
|
|
2886
|
+
`if [ ! -f ${shellQuote(targetPath)} ]; then`,
|
|
2887
|
+
` tmp=${shellQuote(tmpPath)}`,
|
|
2888
|
+
' cleanup() { rm -f -- "$tmp"; }',
|
|
2889
|
+
" trap cleanup EXIT",
|
|
2890
|
+
` curl --fail --location --silent --show-error --retry 3 --retry-delay 1 --output "$tmp" ${shellQuote(download.url)}`,
|
|
2891
|
+
` mv -- "$tmp" ${shellQuote(targetPath)}`,
|
|
2892
|
+
" trap - EXIT",
|
|
2893
|
+
"fi",
|
|
2894
|
+
`chmod a-w -- ${shellQuote(targetPath)} 2>/dev/null || true`
|
|
2895
|
+
].join("\n");
|
|
2896
|
+
}
|
|
2897
|
+
function shellQuote(value) {
|
|
2898
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
2899
|
+
}
|
|
2900
|
+
async function restoredSandboxSessionState(state, client) {
|
|
2901
|
+
if (!client) {
|
|
2902
|
+
return void 0;
|
|
2903
|
+
}
|
|
2904
|
+
const sandboxState = state._sandbox;
|
|
2905
|
+
const entry = sandboxState?.sessionsByAgent?.[sandboxState.currentAgentKey] ?? (sandboxState?.currentAgentKey && sandboxState?.sessionState ? {
|
|
2906
|
+
backendId: sandboxState.backendId,
|
|
2907
|
+
currentAgentKey: sandboxState.currentAgentKey,
|
|
2908
|
+
currentAgentName: sandboxState.currentAgentName,
|
|
2909
|
+
sessionState: sandboxState.sessionState
|
|
2910
|
+
} : void 0);
|
|
2911
|
+
if (!entry) {
|
|
2912
|
+
return void 0;
|
|
2913
|
+
}
|
|
2914
|
+
if (client.backendId !== entry.backendId) {
|
|
2915
|
+
throw new Error("RunState sandbox backend does not match the configured sandbox client");
|
|
2916
|
+
}
|
|
2917
|
+
return await deserializeSandboxSessionStateEnvelope(client, entry.sessionState);
|
|
2918
|
+
}
|
|
2919
|
+
var builtInSandboxLifecycleHooks = {
|
|
2920
|
+
"azure-cli-login": {
|
|
2921
|
+
id: "azure-cli-login",
|
|
2922
|
+
phase: "beforeAgentStart",
|
|
2923
|
+
shouldRun: ({ environment }) => hasAzureServicePrincipal(environment),
|
|
2924
|
+
run: runAzureCliLoginHook
|
|
2925
|
+
}
|
|
2926
|
+
};
|
|
2927
|
+
function sandboxLifecycleHooksForIds(ids) {
|
|
2928
|
+
return ids.map((id) => {
|
|
2929
|
+
const hook = builtInSandboxLifecycleHooks[id];
|
|
2930
|
+
if (!hook) {
|
|
2931
|
+
throw new Error(`Unknown sandbox lifecycle hook ${id}`);
|
|
2932
|
+
}
|
|
2933
|
+
return hook;
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
function applicableBeforeAgentStartHooks(hooks, context) {
|
|
2937
|
+
return hooks.filter((hook) => hook.phase === "beforeAgentStart" && (hook.shouldRun?.(context) ?? true));
|
|
2938
|
+
}
|
|
2939
|
+
async function runBeforeAgentStartHooks(session, hooks, context) {
|
|
2940
|
+
for (const hook of applicableBeforeAgentStartHooks(hooks, context)) {
|
|
2941
|
+
await hook.run(session, context);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
function withSandboxLifecycleHooks(client, hooks, context) {
|
|
2945
|
+
const beforeAgentStartHooks = applicableBeforeAgentStartHooks(hooks, context);
|
|
2946
|
+
if (beforeAgentStartHooks.length === 0) {
|
|
2947
|
+
return client;
|
|
2948
|
+
}
|
|
2949
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
2950
|
+
const wrapSession = async (session) => {
|
|
2951
|
+
if (typeof session === "object" && session !== null && !seen.has(session)) {
|
|
2952
|
+
for (const hook of beforeAgentStartHooks) {
|
|
2953
|
+
await hook.run(session, context);
|
|
2954
|
+
}
|
|
2955
|
+
seen.add(session);
|
|
2956
|
+
}
|
|
2957
|
+
return session;
|
|
2958
|
+
};
|
|
2959
|
+
const wrapped = {
|
|
2960
|
+
backendId: client.backendId,
|
|
2961
|
+
...client.supportsDefaultOptions !== void 0 ? { supportsDefaultOptions: client.supportsDefaultOptions } : {},
|
|
2962
|
+
...client.create ? { create: async (...args) => await wrapSession(await client.create(...args)) } : {},
|
|
2963
|
+
...client.resume ? { resume: async (state) => await wrapSession(await client.resume(state)) } : {},
|
|
2964
|
+
...client.delete ? { delete: async (state) => await client.delete(state) } : {},
|
|
2965
|
+
...client.serializeSessionState ? { serializeSessionState: async (state, options) => await client.serializeSessionState(state, options) } : {},
|
|
2966
|
+
...client.canPersistOwnedSessionState ? { canPersistOwnedSessionState: async (state) => await client.canPersistOwnedSessionState(state) } : {},
|
|
2967
|
+
...client.canReusePreservedOwnedSession ? { canReusePreservedOwnedSession: async (state) => await client.canReusePreservedOwnedSession(state) } : {},
|
|
2968
|
+
...client.deserializeSessionState ? { deserializeSessionState: async (state) => await client.deserializeSessionState(state) } : {}
|
|
2969
|
+
};
|
|
2970
|
+
return wrapped;
|
|
2971
|
+
}
|
|
2972
|
+
function sandboxRepositoryCloneHooksForAgent(agent) {
|
|
2973
|
+
return agentRepositoryCloneHooks.get(agent) ?? [];
|
|
2974
|
+
}
|
|
2975
|
+
function gitTokenSeedForAgent(agent) {
|
|
2976
|
+
return agentGitTokenSeed.get(agent);
|
|
2977
|
+
}
|
|
2978
|
+
function sandboxRepositoryCloneHooks(settings, resources, activeSandboxBackend = settings.sandboxBackend) {
|
|
2979
|
+
const repositories = resources.filter((resource) => resource.kind === "repository" && repositoryUsesSandboxClone(settings, resource, activeSandboxBackend));
|
|
2980
|
+
if (repositories.length === 0) {
|
|
2981
|
+
return [];
|
|
2982
|
+
}
|
|
2983
|
+
return [{
|
|
2984
|
+
id: "repository-clone",
|
|
2985
|
+
phase: "beforeAgentStart",
|
|
2986
|
+
run: async (session, context) => {
|
|
2987
|
+
await runRepositoryCloneHook(session, repositories, context);
|
|
2988
|
+
}
|
|
2989
|
+
}];
|
|
2990
|
+
}
|
|
2991
|
+
function repositoryUsesSandboxClone(settings, resource, activeSandboxBackend = settings.sandboxBackend) {
|
|
2992
|
+
if (activeSandboxBackend === "selfhosted") {
|
|
2993
|
+
return false;
|
|
2994
|
+
}
|
|
2995
|
+
return settings.sandboxBackend === "modal" || Boolean(resource.githubInstallationId && resource.githubRepositoryId);
|
|
2996
|
+
}
|
|
2997
|
+
function repositoryCloneCommand(resources) {
|
|
2998
|
+
const commands = [
|
|
2999
|
+
"set -eu",
|
|
3000
|
+
'export HOME="${HOME:-/workspace}"',
|
|
3001
|
+
'export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"',
|
|
3002
|
+
// TOKEN-BROKER (B1/B2): seed the run-scoped GitHub token into the STABLE token FILE
|
|
3003
|
+
// AND provision the git-askpass helper into the box AT SETUP (runtime) BEFORE any
|
|
3004
|
+
// clone runs, so GIT_ASKPASS points at a per-box, user-writable script that reads
|
|
3005
|
+
// that file for the fetch below. Provisioning the askpass here (rather than relying
|
|
3006
|
+
// on a baked image script at /usr/local/bin/opengeni-git-askpass) removes the
|
|
3007
|
+
// image-rebuild rollout gate: the askpass is correct on ANY box image, including
|
|
3008
|
+
// pre-existing warm boxes on their next turn's clone hook, and no product image has
|
|
3009
|
+
// to carry it. The seed rides the per-exec env (OPENGENI_GIT_TOKEN_SEED) — NEVER the
|
|
3010
|
+
// box/agent manifest (validateNoEnvironmentDelta must not see a rotating value), so
|
|
3011
|
+
// this whole block is a no-op when the seed is absent (e.g. the selfhosted path,
|
|
3012
|
+
// which uses its own git creds). The token file lives at $OPENGENI_GIT_TOKEN_FILE
|
|
3013
|
+
// (stable, from the shared base) with a $HOME/.opengeni/git-token fallback.
|
|
3014
|
+
// $GIT_ASKPASS is on the box manifest env (set by
|
|
3015
|
+
// sandboxEnvironmentForRun to $HOME/.opengeni/askpass), so it is available to this
|
|
3016
|
+
// exec; the askpass script we write is byte-identical to docker/opengeni-git-askpass
|
|
3017
|
+
// and is written via a QUOTED heredoc (<<'ASKPASS_EOF') so NOTHING inside it expands
|
|
3018
|
+
// ($1, $HOME, ${OPENGENI_GIT_TOKEN_FILE:-...}, and the literal \n in printf all land
|
|
3019
|
+
// verbatim), then chmod 0755 so git can exec it.
|
|
3020
|
+
//
|
|
3021
|
+
// ATOMIC REWRITE: this block now re-runs at the start of EVERY turn on a warm box
|
|
3022
|
+
// that other turn holders may be actively using — an in-flight `git fetch` from a
|
|
3023
|
+
// concurrent turn can invoke the askpass (which cats the token file) at any moment.
|
|
3024
|
+
// Both files are therefore written to a pid-suffixed temp under umask 077 and
|
|
3025
|
+
// renamed into place: rename is atomic, concurrent readers keep the old inode, and
|
|
3026
|
+
// the token is never observable world-readable (no post-hoc chmod window).
|
|
3027
|
+
'if [ -n "${OPENGENI_GIT_TOKEN_SEED:-}" ]; then',
|
|
3028
|
+
' seed_umask="$(umask)"',
|
|
3029
|
+
" umask 077",
|
|
3030
|
+
' git_token_file="${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}"',
|
|
3031
|
+
' mkdir -p "$(dirname "$git_token_file")"',
|
|
3032
|
+
` printf '%s' "$OPENGENI_GIT_TOKEN_SEED" > "$git_token_file.tmp.$$"`,
|
|
3033
|
+
' mv -f "$git_token_file.tmp.$$" "$git_token_file"',
|
|
3034
|
+
' git_askpass="${GIT_ASKPASS:-$HOME/.opengeni/askpass}"',
|
|
3035
|
+
' mkdir -p "$(dirname "$git_askpass")"',
|
|
3036
|
+
` cat > "$git_askpass.tmp.$$" <<'ASKPASS_EOF'`,
|
|
3037
|
+
"#!/usr/bin/env sh",
|
|
3038
|
+
'case "$1" in',
|
|
3039
|
+
` *Username*) printf '%s\\n' "x-access-token" ;;`,
|
|
3040
|
+
` *Password*) cat "\${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" 2>/dev/null || printf '\\n' ;;`,
|
|
3041
|
+
" *) printf '\\n' ;;",
|
|
3042
|
+
"esac",
|
|
3043
|
+
"ASKPASS_EOF",
|
|
3044
|
+
' chmod 0755 "$git_askpass.tmp.$$"',
|
|
3045
|
+
' mv -f "$git_askpass.tmp.$$" "$git_askpass"',
|
|
3046
|
+
' umask "$seed_umask"',
|
|
3047
|
+
"fi",
|
|
3048
|
+
"ensure_git() {",
|
|
3049
|
+
" if command -v git >/dev/null 2>&1; then",
|
|
3050
|
+
" return 0",
|
|
3051
|
+
" fi",
|
|
3052
|
+
" if command -v apt-get >/dev/null 2>&1; then",
|
|
3053
|
+
" export DEBIAN_FRONTEND=noninteractive",
|
|
3054
|
+
" apt-get update >/dev/null",
|
|
3055
|
+
" apt-get install -y --no-install-recommends ca-certificates git >/dev/null",
|
|
3056
|
+
" rm -rf /var/lib/apt/lists/*",
|
|
3057
|
+
" command -v git >/dev/null 2>&1 && return 0",
|
|
3058
|
+
" fi",
|
|
3059
|
+
' echo "git is not installed in the sandbox and could not be bootstrapped" >&2',
|
|
3060
|
+
" exit 127",
|
|
3061
|
+
"}",
|
|
3062
|
+
"ensure_git",
|
|
3063
|
+
"clone_repository() {",
|
|
3064
|
+
' target="$1"',
|
|
3065
|
+
' uri="$2"',
|
|
3066
|
+
' ref="$3"',
|
|
3067
|
+
' subpath="$4"',
|
|
3068
|
+
' if [ -e "$target" ] && { [ -f "$target" ] || [ -n "$(find "$target" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]; }; then',
|
|
3069
|
+
// This hook re-runs every turn on a long-lived box, so \"non-empty\" alone is not
|
|
3070
|
+
// proof of a completed materialization: an interrupted clone (worker crash /
|
|
3071
|
+
// lifecycle timeout mid-mv/cp) leaves a partial tree that would otherwise pass
|
|
3072
|
+
// this check forever. A full-repo target must actually BE a work tree to be
|
|
3073
|
+
// skipped; a partial one is wiped and rebuilt (nothing legitimate writes under
|
|
3074
|
+
// the mount path before the repo exists). Subpath extracts are not git repos —
|
|
3075
|
+
// for those the plain non-empty check stands (no stronger signal available).
|
|
3076
|
+
' if [ -n "$subpath" ] || git -C "$target" rev-parse --is-inside-work-tree >/dev/null 2>&1; then',
|
|
3077
|
+
' echo "Repository resource already present at $target"',
|
|
3078
|
+
" return 0",
|
|
3079
|
+
" fi",
|
|
3080
|
+
' echo "Re-materializing partial repository resource at $target" >&2',
|
|
3081
|
+
' find "$target" -mindepth 1 -maxdepth 1 -exec rm -rf {} +',
|
|
3082
|
+
" fi",
|
|
3083
|
+
' mkdir -p "$(dirname "$target")"',
|
|
3084
|
+
' tmp="${target}.tmp.$$"',
|
|
3085
|
+
' rm -rf "$tmp"',
|
|
3086
|
+
// Fetch failures must not leak the pid-suffixed tmp clone beside the mount
|
|
3087
|
+
// (set -eu would exit before any cleanup).
|
|
3088
|
+
' if ! { git init "$tmp" >/dev/null && git -C "$tmp" remote add origin "$uri" && git -C "$tmp" fetch --depth 1 --no-tags --filter=blob:none origin "$ref" && git -C "$tmp" checkout --detach FETCH_HEAD >/dev/null; }; then',
|
|
3089
|
+
' rm -rf "$tmp"',
|
|
3090
|
+
' echo "Repository resource fetch failed for $target" >&2',
|
|
3091
|
+
" exit 1",
|
|
3092
|
+
" fi",
|
|
3093
|
+
' if [ -n "$subpath" ]; then',
|
|
3094
|
+
' if [ ! -e "$tmp/$subpath" ]; then',
|
|
3095
|
+
' echo "Repository subpath not found: $subpath" >&2',
|
|
3096
|
+
' rm -rf "$tmp"',
|
|
3097
|
+
" exit 1",
|
|
3098
|
+
" fi",
|
|
3099
|
+
' if [ -d "$tmp/$subpath" ]; then',
|
|
3100
|
+
' mkdir -p "$target"',
|
|
3101
|
+
' cp -a "$tmp/$subpath/." "$target/"',
|
|
3102
|
+
" else",
|
|
3103
|
+
' rmdir "$target" 2>/dev/null || true',
|
|
3104
|
+
' cp -a "$tmp/$subpath" "$target"',
|
|
3105
|
+
" fi",
|
|
3106
|
+
' rm -rf "$tmp"',
|
|
3107
|
+
" else",
|
|
3108
|
+
' rmdir "$target" 2>/dev/null || true',
|
|
3109
|
+
// Two concurrent turn holders can race this install: without the existence
|
|
3110
|
+
// re-check the loser's un-flagged `mv` would nest its tmp clone INSIDE the
|
|
3111
|
+
// winner's tree as <name>.tmp.<pid>. If the winner produced a valid work tree,
|
|
3112
|
+
// accept it; a non-empty non-repo survivor here is a mount point the manifest
|
|
3113
|
+
// re-filled — install into it by content copy instead of rename.
|
|
3114
|
+
' if [ -e "$target" ]; then',
|
|
3115
|
+
' if git -C "$target" rev-parse --is-inside-work-tree >/dev/null 2>&1; then',
|
|
3116
|
+
' rm -rf "$tmp"',
|
|
3117
|
+
' echo "Repository resource already present at $target"',
|
|
3118
|
+
" return 0",
|
|
3119
|
+
" fi",
|
|
3120
|
+
' cp -a "$tmp/." "$target/"',
|
|
3121
|
+
' rm -rf "$tmp"',
|
|
3122
|
+
" else",
|
|
3123
|
+
' mv "$tmp" "$target"',
|
|
3124
|
+
" fi",
|
|
3125
|
+
' git -C "$target" rev-parse --is-inside-work-tree >/dev/null',
|
|
3126
|
+
" fi",
|
|
3127
|
+
' if [ ! -e "$target" ]; then',
|
|
3128
|
+
' echo "Repository resource was not materialized at $target" >&2',
|
|
3129
|
+
" exit 1",
|
|
3130
|
+
" fi",
|
|
3131
|
+
' echo "Repository resource ready at $target"',
|
|
3132
|
+
"}"
|
|
3133
|
+
];
|
|
3134
|
+
for (const resource of resources) {
|
|
3135
|
+
const url = new URL(resource.uri);
|
|
3136
|
+
const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
3137
|
+
const mountPath = normalizeManifestPath(resource.mountPath ?? `repos/${repo}`);
|
|
3138
|
+
commands.push([
|
|
3139
|
+
"clone_repository",
|
|
3140
|
+
shellQuote(posixPath.join("/workspace", mountPath)),
|
|
3141
|
+
shellQuote(resource.uri),
|
|
3142
|
+
shellQuote(resource.ref),
|
|
3143
|
+
shellQuote(resource.subpath ? normalizeManifestPath(resource.subpath) : "")
|
|
3144
|
+
].join(" "));
|
|
3145
|
+
}
|
|
3146
|
+
return commands.join("\n");
|
|
3147
|
+
}
|
|
3148
|
+
async function runRepositoryCloneHook(session, resources, context = { environment: {} }) {
|
|
3149
|
+
const payload = { name: "repository-clone", repositoryCount: resources.length };
|
|
3150
|
+
await context.onRuntimeEvent?.({ type: "sandbox.operation.started", payload });
|
|
3151
|
+
try {
|
|
3152
|
+
const command = context.gitTokenSeed ? `export OPENGENI_GIT_TOKEN_SEED=${shellQuote(context.gitTokenSeed)}
|
|
3153
|
+
${repositoryCloneCommand(resources)}` : repositoryCloneCommand(resources);
|
|
3154
|
+
if (session.exec) {
|
|
3155
|
+
const result = await session.exec({
|
|
3156
|
+
cmd: command,
|
|
3157
|
+
workdir: "/workspace",
|
|
3158
|
+
...context.runAs ? { runAs: context.runAs } : {},
|
|
3159
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
3160
|
+
maxOutputTokens: 2e4
|
|
3161
|
+
});
|
|
3162
|
+
assertSandboxCommandSucceeded(result, "Repository clone hook");
|
|
3163
|
+
} else if (session.execCommand) {
|
|
3164
|
+
const result = await session.execCommand({
|
|
3165
|
+
cmd: command,
|
|
3166
|
+
workdir: "/workspace",
|
|
3167
|
+
...context.runAs ? { runAs: context.runAs } : {},
|
|
3168
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
3169
|
+
maxOutputTokens: 2e4
|
|
3170
|
+
});
|
|
3171
|
+
assertSandboxCommandSucceeded(result, "Repository clone hook");
|
|
3172
|
+
} else {
|
|
3173
|
+
throw new Error("Sandbox session does not support command execution");
|
|
3174
|
+
}
|
|
3175
|
+
await context.onRuntimeEvent?.({ type: "sandbox.operation.completed", payload });
|
|
3176
|
+
} catch (error) {
|
|
3177
|
+
await context.onRuntimeEvent?.({
|
|
3178
|
+
type: "sandbox.operation.failed",
|
|
3179
|
+
payload: {
|
|
3180
|
+
...payload,
|
|
3181
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3182
|
+
}
|
|
3183
|
+
});
|
|
3184
|
+
throw error;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
function azureCliLoginCommand() {
|
|
3188
|
+
return [
|
|
3189
|
+
'export HOME="${HOME:-/workspace}"',
|
|
3190
|
+
'mkdir -p "$HOME/.azure"',
|
|
3191
|
+
'CLIENT_ID="${AZURE_CLIENT_ID:-${ARM_CLIENT_ID:-}}"',
|
|
3192
|
+
'CLIENT_SECRET="${AZURE_CLIENT_SECRET:-${ARM_CLIENT_SECRET:-}}"',
|
|
3193
|
+
'TENANT_ID="${AZURE_TENANT_ID:-${ARM_TENANT_ID:-}}"',
|
|
3194
|
+
'SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-${ARM_SUBSCRIPTION_ID:-}}"',
|
|
3195
|
+
'if [ -n "$CLIENT_ID" ] && [ -n "$CLIENT_SECRET" ] && [ -n "$TENANT_ID" ]; then',
|
|
3196
|
+
' command -v az >/dev/null 2>&1 || { echo "Azure CLI is not installed in the sandbox" >&2; exit 127; }',
|
|
3197
|
+
' az account show --only-show-errors >/dev/null 2>&1 || az login --service-principal --username "$CLIENT_ID" --password "$CLIENT_SECRET" --tenant "$TENANT_ID" --allow-no-subscriptions --only-show-errors --output none',
|
|
3198
|
+
// if/fi, NOT `[ -n ] && az`: this line ends the credentialed if-body, so with a
|
|
3199
|
+
// no-subscription SP (an explicitly supported config — the login above passes
|
|
3200
|
+
// --allow-no-subscriptions) the bare `[ -n ]` would exit the whole script 1 and
|
|
3201
|
+
// fail the turn.
|
|
3202
|
+
' if [ -n "$SUBSCRIPTION_ID" ]; then az account set --subscription "$SUBSCRIPTION_ID" --only-show-errors; fi',
|
|
3203
|
+
"fi"
|
|
3204
|
+
].join("\n");
|
|
3205
|
+
}
|
|
3206
|
+
function sandboxCommandExitCode(result) {
|
|
3207
|
+
if (typeof result === "string") {
|
|
3208
|
+
const match = result.match(/Process exited with code (-?\d+)/);
|
|
3209
|
+
return match ? Number(match[1]) : null;
|
|
3210
|
+
}
|
|
3211
|
+
if (!result || typeof result !== "object") {
|
|
3212
|
+
return null;
|
|
3213
|
+
}
|
|
3214
|
+
const candidate = result;
|
|
3215
|
+
for (const value of [candidate.exitCode, candidate.exit_code, candidate.code, candidate.status]) {
|
|
3216
|
+
if (typeof value === "number") {
|
|
3217
|
+
return value;
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
return null;
|
|
3221
|
+
}
|
|
3222
|
+
function sandboxCommandOutput(result) {
|
|
3223
|
+
if (!result || typeof result !== "object") {
|
|
3224
|
+
return "";
|
|
3225
|
+
}
|
|
3226
|
+
const candidate = result;
|
|
3227
|
+
return [candidate.output, candidate.stderr, candidate.stdout].filter((value) => typeof value === "string" && value.length > 0).join("\n");
|
|
3228
|
+
}
|
|
3229
|
+
function assertSandboxCommandSucceeded(result, operation) {
|
|
3230
|
+
const output = sandboxCommandOutput(result);
|
|
3231
|
+
if (sandboxCommandStillRunning(result)) {
|
|
3232
|
+
throw new Error(`${operation} did not finish before the lifecycle command timeout${output ? `:
|
|
3233
|
+
${output}` : ""}`);
|
|
3234
|
+
}
|
|
3235
|
+
const exitCode = sandboxCommandExitCode(result);
|
|
3236
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
3237
|
+
throw new Error(output || `${operation} failed with exit code ${exitCode}`);
|
|
3238
|
+
}
|
|
3239
|
+
if (exitCode === null) {
|
|
3240
|
+
throw new Error(output || `${operation} did not return a command exit code`);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
function sandboxCommandStillRunning(result) {
|
|
3244
|
+
if (typeof result === "string") {
|
|
3245
|
+
return /Process running with session ID \d+/u.test(result);
|
|
3246
|
+
}
|
|
3247
|
+
if (!result || typeof result !== "object") {
|
|
3248
|
+
return false;
|
|
3249
|
+
}
|
|
3250
|
+
const candidate = result;
|
|
3251
|
+
return typeof candidate.sessionId === "number" || typeof candidate.session_id === "number";
|
|
3252
|
+
}
|
|
3253
|
+
function hasAzureServicePrincipal(environment) {
|
|
3254
|
+
const clientId = environment.AZURE_CLIENT_ID || environment.ARM_CLIENT_ID;
|
|
3255
|
+
const clientSecret = environment.AZURE_CLIENT_SECRET || environment.ARM_CLIENT_SECRET;
|
|
3256
|
+
const tenantId = environment.AZURE_TENANT_ID || environment.ARM_TENANT_ID;
|
|
3257
|
+
return Boolean(clientId && clientSecret && tenantId);
|
|
3258
|
+
}
|
|
3259
|
+
async function runAzureCliLoginHook(session, context = { environment: {} }) {
|
|
3260
|
+
const payload = { name: "azure-cli-login", command: "az login --service-principal" };
|
|
3261
|
+
await context.onRuntimeEvent?.({ type: "sandbox.operation.started", payload });
|
|
3262
|
+
try {
|
|
3263
|
+
if (session.exec) {
|
|
3264
|
+
const result = await session.exec({
|
|
3265
|
+
cmd: azureCliLoginCommand(),
|
|
3266
|
+
workdir: "/workspace",
|
|
3267
|
+
...context.runAs ? { runAs: context.runAs } : {},
|
|
3268
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
3269
|
+
maxOutputTokens: 2e4
|
|
3270
|
+
});
|
|
3271
|
+
assertSandboxCommandSucceeded(result, "Azure CLI login hook");
|
|
3272
|
+
} else if (session.execCommand) {
|
|
3273
|
+
const result = await session.execCommand({
|
|
3274
|
+
cmd: azureCliLoginCommand(),
|
|
3275
|
+
workdir: "/workspace",
|
|
3276
|
+
...context.runAs ? { runAs: context.runAs } : {},
|
|
3277
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
3278
|
+
maxOutputTokens: 2e4
|
|
3279
|
+
});
|
|
3280
|
+
assertSandboxCommandSucceeded(result, "Azure CLI login hook");
|
|
3281
|
+
} else {
|
|
3282
|
+
throw new Error("Sandbox session does not support command execution");
|
|
3283
|
+
}
|
|
3284
|
+
await context.onRuntimeEvent?.({ type: "sandbox.operation.completed", payload });
|
|
3285
|
+
} catch (error) {
|
|
3286
|
+
await context.onRuntimeEvent?.({
|
|
3287
|
+
type: "sandbox.operation.failed",
|
|
3288
|
+
payload: {
|
|
3289
|
+
...payload,
|
|
3290
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3291
|
+
}
|
|
3292
|
+
});
|
|
3293
|
+
throw error;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
function azureDeploymentBaseUrl(settings) {
|
|
3297
|
+
const endpoint = settings.azureOpenaiEndpoint?.replace(/\/+$/, "");
|
|
3298
|
+
if (!endpoint || !settings.azureOpenaiDeployment) {
|
|
3299
|
+
throw new Error("Azure OpenAI endpoint/deployment settings are incomplete");
|
|
3300
|
+
}
|
|
3301
|
+
return `${endpoint}/openai/deployments/${settings.azureOpenaiDeployment}`;
|
|
3302
|
+
}
|
|
3303
|
+
function azureOpenAIDefaultQuery(settings, baseURL) {
|
|
3304
|
+
if (!settings.azureOpenaiApiVersion) return void 0;
|
|
3305
|
+
const normalized = baseURL.replace(/\/+$/, "").toLowerCase();
|
|
3306
|
+
if (normalized.endsWith("/openai/v1")) {
|
|
3307
|
+
return void 0;
|
|
3308
|
+
}
|
|
3309
|
+
return { "api-version": settings.azureOpenaiApiVersion };
|
|
3310
|
+
}
|
|
3311
|
+
var stagedBundledSkillsDir = null;
|
|
3312
|
+
function bundledSkillsDir() {
|
|
3313
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
3314
|
+
const packaged = [
|
|
3315
|
+
join(moduleDir, "bundled_hashicorp_terraform_skills"),
|
|
3316
|
+
join(moduleDir, "..", "src", "bundled_hashicorp_terraform_skills")
|
|
3317
|
+
].find((candidate) => existsSync(candidate)) ?? join(moduleDir, "bundled_hashicorp_terraform_skills");
|
|
3318
|
+
if (isPathWithin(process.cwd(), packaged)) {
|
|
3319
|
+
return packaged;
|
|
3320
|
+
}
|
|
3321
|
+
if (!stagedBundledSkillsDir) {
|
|
3322
|
+
stagedBundledSkillsDir = stageBundledSkills(packaged, join(process.cwd(), ".opengeni", "bundled_hashicorp_terraform_skills"));
|
|
3323
|
+
}
|
|
3324
|
+
return stagedBundledSkillsDir;
|
|
3325
|
+
}
|
|
3326
|
+
function stageBundledSkills(packaged, target) {
|
|
3327
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
3328
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
3329
|
+
mkdirSync(dirname(tmp), { recursive: true });
|
|
3330
|
+
cpSync(packaged, tmp, { recursive: true });
|
|
3331
|
+
rmSync(target, { recursive: true, force: true });
|
|
3332
|
+
try {
|
|
3333
|
+
renameSync(tmp, target);
|
|
3334
|
+
} catch (error) {
|
|
3335
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
3336
|
+
if (!existsSync(target)) {
|
|
3337
|
+
throw error;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
return target;
|
|
3341
|
+
}
|
|
3342
|
+
function isPathWithin(root, candidate) {
|
|
3343
|
+
const relativePath = relative(root, candidate);
|
|
3344
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
3345
|
+
}
|
|
3346
|
+
function lazySkillSourceWithPackSkills(packSkills) {
|
|
3347
|
+
const bundledDir = bundledSkillsDir();
|
|
3348
|
+
const bundled = localDirLazySkillSource({ src: bundledDir });
|
|
3349
|
+
if (packSkills.length === 0) {
|
|
3350
|
+
return bundled;
|
|
3351
|
+
}
|
|
3352
|
+
const children = {};
|
|
3353
|
+
for (const name of bundledSkillDirNames(bundledDir)) {
|
|
3354
|
+
children[name] = localDir({ src: join(bundledDir, name) });
|
|
3355
|
+
}
|
|
3356
|
+
const packIndex = [];
|
|
3357
|
+
const packNames = /* @__PURE__ */ new Set();
|
|
3358
|
+
const packNameKeys = /* @__PURE__ */ new Set();
|
|
3359
|
+
for (const skill of packSkills) {
|
|
3360
|
+
assertSafePackSkillName(skill.name);
|
|
3361
|
+
if (packNameKeys.has(skill.name.toLowerCase())) {
|
|
3362
|
+
throw new Error(`Duplicate pack skill name: ${skill.name}`);
|
|
3363
|
+
}
|
|
3364
|
+
packNameKeys.add(skill.name.toLowerCase());
|
|
3365
|
+
packNames.add(skill.name);
|
|
3366
|
+
children[skill.name] = packSkillDirEntry(skill);
|
|
3367
|
+
packIndex.push({ name: skill.name, description: packSkillDescription(skill), path: skill.name });
|
|
3368
|
+
}
|
|
3369
|
+
return {
|
|
3370
|
+
source: dir({ children }),
|
|
3371
|
+
getIndex: (manifest, skillsPath) => [
|
|
3372
|
+
...(bundled.getIndex?.(manifest, skillsPath) ?? []).filter((entry) => !packNames.has(entry.path ?? entry.name)),
|
|
3373
|
+
...packIndex
|
|
3374
|
+
]
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
function bundledSkillDirNames(root) {
|
|
3378
|
+
return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
|
|
3379
|
+
}
|
|
3380
|
+
function packSkillDirEntry(skill) {
|
|
3381
|
+
const root = { dirs: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map() };
|
|
3382
|
+
for (const skillFile of skill.files) {
|
|
3383
|
+
const segments = packSkillPathSegments(skill.name, skillFile.path);
|
|
3384
|
+
let node = root;
|
|
3385
|
+
for (const segment of segments.slice(0, -1)) {
|
|
3386
|
+
if (node.files.has(segment)) {
|
|
3387
|
+
throw new Error(`Pack skill ${skill.name} uses ${segment} as both a file and a directory`);
|
|
3388
|
+
}
|
|
3389
|
+
let next = node.dirs.get(segment);
|
|
3390
|
+
if (!next) {
|
|
3391
|
+
next = { dirs: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map() };
|
|
3392
|
+
node.dirs.set(segment, next);
|
|
3393
|
+
}
|
|
3394
|
+
node = next;
|
|
3395
|
+
}
|
|
3396
|
+
const filename = segments[segments.length - 1];
|
|
3397
|
+
if (node.dirs.has(filename) || node.files.has(filename)) {
|
|
3398
|
+
throw new Error(`Duplicate pack skill file path in ${skill.name}: ${skillFile.path}`);
|
|
3399
|
+
}
|
|
3400
|
+
node.files.set(filename, skillFile.content);
|
|
3401
|
+
}
|
|
3402
|
+
if (!root.files.has("SKILL.md")) {
|
|
3403
|
+
throw new Error(`Pack skill ${skill.name} is missing a top-level SKILL.md file`);
|
|
3404
|
+
}
|
|
3405
|
+
return packSkillDirFromNode(root);
|
|
3406
|
+
}
|
|
3407
|
+
function packSkillDirFromNode(node) {
|
|
3408
|
+
const children = {};
|
|
3409
|
+
for (const [name, child] of node.dirs) {
|
|
3410
|
+
children[name] = packSkillDirFromNode(child);
|
|
3411
|
+
}
|
|
3412
|
+
for (const [name, content] of node.files) {
|
|
3413
|
+
children[name] = file({ content });
|
|
3414
|
+
}
|
|
3415
|
+
return dir({ children });
|
|
3416
|
+
}
|
|
3417
|
+
function assertSafePackSkillName(name) {
|
|
3418
|
+
if (packSkillPathSegments(name, name).length !== 1) {
|
|
3419
|
+
throw new Error(`Invalid pack skill name: ${name}`);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
function packSkillPathSegments(skillName, path) {
|
|
3423
|
+
const segments = path.split("/");
|
|
3424
|
+
if (path.startsWith("/") || path.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
3425
|
+
throw new Error(`Invalid pack skill file path for ${skillName}: ${path}`);
|
|
3426
|
+
}
|
|
3427
|
+
return segments;
|
|
3428
|
+
}
|
|
3429
|
+
function packSkillDescription(skill) {
|
|
3430
|
+
const explicit = skill.description?.trim();
|
|
3431
|
+
if (explicit) {
|
|
3432
|
+
return explicit;
|
|
3433
|
+
}
|
|
3434
|
+
const markdown = skill.files.find((skillFile) => skillFile.path === "SKILL.md")?.content ?? "";
|
|
3435
|
+
return skillFrontmatterDescription(markdown) ?? "No description provided.";
|
|
3436
|
+
}
|
|
3437
|
+
function skillFrontmatterDescription(markdown) {
|
|
3438
|
+
const lines = markdown.split(/\r?\n/);
|
|
3439
|
+
if (lines[0]?.trim() !== "---") {
|
|
3440
|
+
return null;
|
|
3441
|
+
}
|
|
3442
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
3443
|
+
if (end === -1) {
|
|
3444
|
+
return null;
|
|
3445
|
+
}
|
|
3446
|
+
const collected = [];
|
|
3447
|
+
let inDescription = false;
|
|
3448
|
+
for (const line of lines.slice(1, end)) {
|
|
3449
|
+
const match = line.match(/^description:\s*(.*)$/);
|
|
3450
|
+
if (match) {
|
|
3451
|
+
const inline = match[1].trim();
|
|
3452
|
+
if (inline && inline !== ">-" && inline !== ">" && inline !== "|" && inline !== "|-") {
|
|
3453
|
+
return unquoteFrontmatterValue(inline);
|
|
3454
|
+
}
|
|
3455
|
+
inDescription = true;
|
|
3456
|
+
continue;
|
|
3457
|
+
}
|
|
3458
|
+
if (inDescription) {
|
|
3459
|
+
if (/^\s+\S/.test(line)) {
|
|
3460
|
+
collected.push(line.trim());
|
|
3461
|
+
continue;
|
|
3462
|
+
}
|
|
3463
|
+
break;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
const blockValue = collected.join(" ").trim();
|
|
3467
|
+
return blockValue ? blockValue : null;
|
|
3468
|
+
}
|
|
3469
|
+
function unquoteFrontmatterValue(value) {
|
|
3470
|
+
if (value.length >= 2 && value[0] === value[value.length - 1] && (value[0] === '"' || value[0] === "'")) {
|
|
3471
|
+
return value.slice(1, -1);
|
|
3472
|
+
}
|
|
3473
|
+
return value;
|
|
3474
|
+
}
|
|
3475
|
+
function isAsyncIterable(source) {
|
|
3476
|
+
return typeof source[Symbol.asyncIterator] === "function";
|
|
3477
|
+
}
|
|
3478
|
+
function stableJson(value) {
|
|
3479
|
+
return JSON.stringify(sortJson(value));
|
|
3480
|
+
}
|
|
3481
|
+
function sortJson(value) {
|
|
3482
|
+
if (Array.isArray(value)) {
|
|
3483
|
+
return value.map(sortJson);
|
|
3484
|
+
}
|
|
3485
|
+
if (value && typeof value === "object") {
|
|
3486
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nested]) => [key, sortJson(nested)]));
|
|
3487
|
+
}
|
|
3488
|
+
return value;
|
|
3489
|
+
}
|
|
3490
|
+
function approvalIdentifier(item) {
|
|
3491
|
+
return String(item?.rawItem?.callId ?? item?.rawItem?.id ?? item?.id ?? item?.name ?? "approval");
|
|
3492
|
+
}
|
|
3493
|
+
export {
|
|
3494
|
+
ActiveBackendUnresolvableError,
|
|
3495
|
+
CAPABILITY_DESCRIPTORS,
|
|
3496
|
+
COMPACTION_SUMMARY_MARKER,
|
|
3497
|
+
ChannelAConflictError,
|
|
3498
|
+
ChannelANotFoundError,
|
|
3499
|
+
ChannelAUnsupportedError,
|
|
3500
|
+
ChannelAValidationError,
|
|
3501
|
+
CodexSubscriptionUnavailableError,
|
|
3502
|
+
ComputerActionError,
|
|
3503
|
+
ComputerReadOnlyError,
|
|
3504
|
+
ComputerUnavailableError,
|
|
3505
|
+
ComputerUseCapability,
|
|
3506
|
+
DEFAULT_DESKTOP_GEOMETRY,
|
|
3507
|
+
DESKTOP_STREAM_PORT,
|
|
3508
|
+
DISPLAY_STACK_TIMEOUT_MS,
|
|
3509
|
+
DisplayStackError,
|
|
3510
|
+
DisplayStackUnsupportedError,
|
|
3511
|
+
GENESIS_TITLE_DIRECTIVE,
|
|
3512
|
+
MaxTurnsExceededError2 as MaxTurnsExceededError,
|
|
3513
|
+
MockAgentResponder,
|
|
3514
|
+
MultiProviderModelProvider,
|
|
3515
|
+
NatsControlRpc,
|
|
3516
|
+
OpenAIChatCompletionsModel2 as OpenAIChatCompletionsModel,
|
|
3517
|
+
OpenAIResponsesModel2 as OpenAIResponsesModel,
|
|
3518
|
+
PROVIDER_REGISTRY,
|
|
3519
|
+
RecordingError,
|
|
3520
|
+
RecordingUnavailableError,
|
|
3521
|
+
RoutingSandboxSession,
|
|
3522
|
+
RoutingUnsupportedError,
|
|
3523
|
+
SELFHOSTED_DEFAULT_TIMEOUT_MS,
|
|
3524
|
+
SELFHOSTED_RECONNECT_WINDOW_MS,
|
|
3525
|
+
SELFHOSTED_RELAY_STREAM_PATH,
|
|
3526
|
+
STREAM_PORT,
|
|
3527
|
+
STREAM_TOKEN_DEFAULT_TTL_SECONDS,
|
|
3528
|
+
SUMMARY_INSTRUCTIONS,
|
|
3529
|
+
SUMMARY_PREFIX,
|
|
3530
|
+
SandboxChannelAService,
|
|
3531
|
+
SandboxComputer,
|
|
3532
|
+
SandboxConfigError,
|
|
3533
|
+
SandboxProviderUnavailableError,
|
|
3534
|
+
SelfhostedControlError,
|
|
3535
|
+
SelfhostedSandboxClient,
|
|
3536
|
+
SelfhostedSession,
|
|
3537
|
+
StreamPortUnavailableError,
|
|
3538
|
+
StreamTokenPayload,
|
|
3539
|
+
TERMINAL_SERVER_TIMEOUT_MS,
|
|
3540
|
+
TERMINAL_STREAM_PORT,
|
|
3541
|
+
TerminalServerError,
|
|
3542
|
+
TerminalServerUnsupportedError,
|
|
3543
|
+
agentErrorToControlError,
|
|
3544
|
+
agentsErrorRunState,
|
|
3545
|
+
applyMissingManifestEntries,
|
|
3546
|
+
assertDescriptorRegistryInvariants,
|
|
3547
|
+
assertProviderRegistryInvariants,
|
|
3548
|
+
assertSafeRelPath,
|
|
3549
|
+
azureCliLoginCommand,
|
|
3550
|
+
azureOpenAIDefaultQuery,
|
|
3551
|
+
backendSupportsOs,
|
|
3552
|
+
buildAgentCapabilities,
|
|
3553
|
+
buildCompactionMessages,
|
|
3554
|
+
buildDisplayStackScript,
|
|
3555
|
+
buildManifest,
|
|
3556
|
+
buildModelInstance,
|
|
3557
|
+
buildOpenAIClientFromSettings,
|
|
3558
|
+
buildOpenGeniAgent,
|
|
3559
|
+
buildProviderClient,
|
|
3560
|
+
buildSelfhostedBackendSession,
|
|
3561
|
+
buildStreamUrl,
|
|
3562
|
+
buildSummaryItem,
|
|
3563
|
+
buildTerminalServerScript,
|
|
3564
|
+
callModelInputFilterForSettings,
|
|
3565
|
+
collectSandboxEnvironment,
|
|
3566
|
+
compactionSummaryText,
|
|
3567
|
+
composeAgentInstructions,
|
|
3568
|
+
computerUse,
|
|
3569
|
+
configureOpenAI,
|
|
3570
|
+
contentTypeForCodec,
|
|
3571
|
+
coreInstructions,
|
|
3572
|
+
createProductionAgentRuntime,
|
|
3573
|
+
createSandboxClient,
|
|
3574
|
+
createSandboxClientForBackend,
|
|
3575
|
+
decodeModalSnapshotId,
|
|
3576
|
+
deletePriorPersistedSnapshot,
|
|
3577
|
+
deleteRecordingArtifacts,
|
|
3578
|
+
deserializeSandboxSessionStateEnvelope,
|
|
3579
|
+
desktopCapableBackend,
|
|
3580
|
+
enforceInputBudget,
|
|
3581
|
+
ensureDisplayStack,
|
|
3582
|
+
ensureReadableStreamFrom,
|
|
3583
|
+
ensureTerminalServer,
|
|
3584
|
+
establishSandboxSessionFromEnvelope,
|
|
3585
|
+
estimateItemTokens,
|
|
3586
|
+
estimateTokens,
|
|
3587
|
+
exposeStreamPort,
|
|
3588
|
+
extForCodec,
|
|
3589
|
+
extractResponseOutputText,
|
|
3590
|
+
findKeepBoundary,
|
|
3591
|
+
isCompactionSummary,
|
|
3592
|
+
isExecSessionLostBanner,
|
|
3593
|
+
isProviderSandboxNotFoundError,
|
|
3594
|
+
isSelfhostedProviderNotFoundError,
|
|
3595
|
+
isUserMessage,
|
|
3596
|
+
isWorkspaceEscapeError,
|
|
3597
|
+
lazySkillSourceWithPackSkills,
|
|
3598
|
+
makeActiveBackendResolver,
|
|
3599
|
+
materializeSandboxFileDownloads,
|
|
3600
|
+
maxTurnsExceededRunState,
|
|
3601
|
+
mintStreamToken,
|
|
3602
|
+
modelResponseUsageFromSdkEvent,
|
|
3603
|
+
negotiateCapabilities,
|
|
3604
|
+
negotiateSelfhostedCapabilities,
|
|
3605
|
+
neutralizeToolSearchItemsInSerializedRunState,
|
|
3606
|
+
normalizeComputerCallsFilter,
|
|
3607
|
+
normalizeSdkEvent,
|
|
3608
|
+
normalizeToolOutputForEvent,
|
|
3609
|
+
offlineAgentError,
|
|
3610
|
+
offlineControlResponse,
|
|
3611
|
+
parseExecBannerSessionId,
|
|
3612
|
+
parseExposedPorts,
|
|
3613
|
+
parseNumstatZ,
|
|
3614
|
+
parsePorcelainV2,
|
|
3615
|
+
parseUnifiedPatch,
|
|
3616
|
+
planCompaction,
|
|
3617
|
+
prefixedMcpToolName,
|
|
3618
|
+
prepareAgentTools,
|
|
3619
|
+
prepareRunInput,
|
|
3620
|
+
readRecordingBytes,
|
|
3621
|
+
readWorkspaceArchiveFromEnvelopeSessionState,
|
|
3622
|
+
recordingStorageKey,
|
|
3623
|
+
renderPrefixTranscript,
|
|
3624
|
+
repositoryCloneCommand,
|
|
3625
|
+
repositoryUsesSandboxClone,
|
|
3626
|
+
resolveTurnModel,
|
|
3627
|
+
restoredSandboxSessionStateFromEntry,
|
|
3628
|
+
runAgentStream,
|
|
3629
|
+
runAzureCliLoginHook,
|
|
3630
|
+
runBeforeAgentStartHooks,
|
|
3631
|
+
runRepositoryCloneHook,
|
|
3632
|
+
sandboxCommandExitCode,
|
|
3633
|
+
sandboxCommandOutput,
|
|
3634
|
+
sandboxCommandStillRunning,
|
|
3635
|
+
sandboxFileDownloadsForAgent,
|
|
3636
|
+
sandboxLifecycleHooksForIds,
|
|
3637
|
+
sandboxRunAs,
|
|
3638
|
+
sandboxStateEntryFromRunState,
|
|
3639
|
+
sanitizeHistoryItemsForModel,
|
|
3640
|
+
selectBackend,
|
|
3641
|
+
selfhostedLiveness,
|
|
3642
|
+
serializeApprovals,
|
|
3643
|
+
serializeEstablishedSandboxEnvelope,
|
|
3644
|
+
setSelfhostedApplyDiff,
|
|
3645
|
+
startRecording,
|
|
3646
|
+
stopRecording,
|
|
3647
|
+
stripExecBanner,
|
|
3648
|
+
stripProviderItemIdsFilter,
|
|
3649
|
+
stripReasoningEncryptedContent,
|
|
3650
|
+
stripReasoningIdentityFromSerializedRunState,
|
|
3651
|
+
subjectFor,
|
|
3652
|
+
summarizeForCompaction,
|
|
3653
|
+
tearDownDisplayStack,
|
|
3654
|
+
tearDownTerminalServer,
|
|
3655
|
+
timeoutAgentError,
|
|
3656
|
+
timeoutControlResponse,
|
|
3657
|
+
verifyStreamToken,
|
|
3658
|
+
withManifestRefreshOnResume,
|
|
3659
|
+
withSandboxFileDownloads,
|
|
3660
|
+
withSandboxLifecycleHooks,
|
|
3661
|
+
workspaceEnvironmentInstructions
|
|
3662
|
+
};
|
|
3663
|
+
//# sourceMappingURL=index.js.map
|