@opengeni/runtime 0.7.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES +32 -0
- package/dist/agent-UMUIWWD2.js +8 -0
- package/dist/agent-UMUIWWD2.js.map +1 -0
- package/dist/chunk-4YJ5Z52Z.js +1789 -0
- package/dist/chunk-4YJ5Z52Z.js.map +1 -0
- package/dist/chunk-DPULLAJG.js +215 -0
- package/dist/chunk-DPULLAJG.js.map +1 -0
- package/dist/chunk-K4V4QYCL.js +37683 -0
- package/dist/chunk-K4V4QYCL.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-YUAFQEFY.js +21522 -0
- package/dist/chunk-YUAFQEFY.js.map +1 -0
- package/dist/{index-DIiiwQY2.d.ts → index-D6yi-JcU.d.ts} +138 -15
- package/dist/index.d.ts +329 -121
- package/dist/index.js +8149 -307
- package/dist/index.js.map +1 -1
- package/dist/localSources-T3HZ6KY4.js +15 -0
- package/dist/localSources-T3HZ6KY4.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +42 -5
- package/dist/skill-library.d.ts +74 -0
- package/dist/skill-library.js +18 -0
- package/dist/skill-library.js.map +1 -0
- package/package.json +13 -5
- package/src/bundled_hashicorp_terraform_skills/README.md +3 -3
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +1 -1
- package/src/context-compaction.ts +421 -24
- package/src/history-sanitizer.ts +14 -5
- package/src/index.ts +1335 -280
- package/src/sandbox/channel-a.ts +535 -97
- package/src/sandbox/index.ts +36 -2
- package/src/sandbox/routing/routing-session.ts +10 -2
- package/src/sandbox/run-credentials.ts +818 -0
- package/src/sandbox/select.ts +3 -3
- package/src/sandbox/selfhosted/capabilities.ts +1 -1
- package/src/sandbox/selfhosted/op-stream.ts +40 -0
- package/src/sandbox/selfhosted/op-testing.ts +50 -3
- package/src/sandbox/selfhosted/session.ts +17 -4
- package/src/sandbox/selfhosted/testing.ts +1 -1
- package/src/sandbox/stream-port.ts +1 -1
- package/src/sandbox/stream-token.ts +1 -1
- package/src/sandbox/terminal-server.ts +6 -2
- package/src/sandbox/toolspace-token.ts +147 -0
- package/src/sandbox/turn-tool-cancellation.ts +940 -0
- package/src/skill-library.ts +323 -0
- package/dist/chunk-RYDCVTMW.js +0 -5531
- package/dist/chunk-RYDCVTMW.js.map +0 -1
- /package/src/{bundled_hashicorp_terraform_skills → bundled_skill_library}/azure-verified-modules/SKILL.md +0 -0
package/src/index.ts
CHANGED
|
@@ -12,16 +12,28 @@ import {
|
|
|
12
12
|
sandboxLifecycleHookIds,
|
|
13
13
|
} from "@opengeni/config";
|
|
14
14
|
import {
|
|
15
|
+
approvalIdentifier,
|
|
15
16
|
CAPABILITY_DESCRIPTORS,
|
|
17
|
+
assertUniqueResourceMountPaths,
|
|
18
|
+
gitCredentialBindingIdForRepository,
|
|
19
|
+
gitCredentialProviderForRepository,
|
|
16
20
|
isClearedRunStateBlob,
|
|
21
|
+
normalizeRepositorySubpath,
|
|
22
|
+
normalizeResourceMountPath,
|
|
17
23
|
prefixedMcpToolName as sharedPrefixedMcpToolName,
|
|
24
|
+
resourceMountPath,
|
|
25
|
+
sessionEventMediaPreview,
|
|
26
|
+
sessionEventMediaPreviewFromDataUrl,
|
|
18
27
|
signDelegatedAccessToken,
|
|
28
|
+
RequestHumanInputToolInput,
|
|
19
29
|
type GitCredentialProvider,
|
|
30
|
+
type HumanInputResponse,
|
|
20
31
|
type McpServerConnectionRef,
|
|
21
32
|
type Permission,
|
|
22
33
|
type ReasoningEffort,
|
|
23
34
|
type ResourceRef,
|
|
24
35
|
type SessionEventType,
|
|
36
|
+
type SessionEventMediaPreview,
|
|
25
37
|
type ToolAuthNeededPayload,
|
|
26
38
|
type ToolRef,
|
|
27
39
|
} from "@opengeni/contracts";
|
|
@@ -52,6 +64,7 @@ import {
|
|
|
52
64
|
setDefaultOpenAIKey,
|
|
53
65
|
setOpenAIResponsesTransport,
|
|
54
66
|
setTracingDisabled,
|
|
67
|
+
tool as agentTool,
|
|
55
68
|
// Hosted web_search tool factory. Re-exported from @openai/agents-openai via
|
|
56
69
|
// `export * from '@openai/agents-openai'` in @openai/agents' index (0.11.6);
|
|
57
70
|
// it returns a { type: 'hosted_tool', providerData: { type: 'web_search' } }
|
|
@@ -104,11 +117,13 @@ import {
|
|
|
104
117
|
CODEX_APPS_MCP_SERVER_ID,
|
|
105
118
|
CODEX_MODEL_ID_PREFIX,
|
|
106
119
|
CODEX_ORIGINATOR,
|
|
120
|
+
boundModelToolOutputItems,
|
|
107
121
|
codexAppsSanitizingFetch,
|
|
108
122
|
codexRequestStorage,
|
|
109
123
|
codexSubscriptionFetch,
|
|
110
124
|
} from "@opengeni/codex";
|
|
111
125
|
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
126
|
+
import { createHash } from "node:crypto";
|
|
112
127
|
import { dirname, isAbsolute, join, posix as posixPath, relative } from "node:path";
|
|
113
128
|
import { fileURLToPath } from "node:url";
|
|
114
129
|
|
|
@@ -121,22 +136,52 @@ import {
|
|
|
121
136
|
import { installCodexToolSearch } from "./codex-tool-search";
|
|
122
137
|
import {
|
|
123
138
|
CompactionNeededError,
|
|
139
|
+
CompactionProviderResponseError,
|
|
140
|
+
EmptyCompactionSummaryError,
|
|
124
141
|
SUMMARY_BUFFER_TOKENS,
|
|
125
142
|
compactionThresholdTokens,
|
|
126
|
-
|
|
143
|
+
estimateCompleteModelInput,
|
|
144
|
+
estimateSerializedValueTokens,
|
|
145
|
+
hasModelGeneratedItem,
|
|
127
146
|
renderCompactionPromptInputForChat,
|
|
147
|
+
type CompleteModelInputFootprint,
|
|
148
|
+
type ProviderContextTokenSignal,
|
|
128
149
|
} from "./context-compaction";
|
|
129
150
|
import {
|
|
130
151
|
createSandboxClient,
|
|
131
152
|
desktopCapableBackend,
|
|
132
153
|
restoredSandboxSessionStateFromEntry,
|
|
133
154
|
setSelfhostedApplyDiff,
|
|
155
|
+
toolspaceTokenFileFromEnvironment,
|
|
156
|
+
withToolspaceTokenClient,
|
|
157
|
+
withToolspaceTokenSession,
|
|
158
|
+
withRunCredentialsClient,
|
|
159
|
+
withRunCredentialsSession,
|
|
160
|
+
type RunCredentialSessionReady,
|
|
134
161
|
} from "./sandbox";
|
|
135
162
|
import { runWithToolCallCorrelation } from "./sandbox/op-correlation";
|
|
163
|
+
import {
|
|
164
|
+
createTurnToolCancellationController,
|
|
165
|
+
TurnSandboxCommandCancelledError,
|
|
166
|
+
wrapCapabilityToolsForTurnCancellation,
|
|
167
|
+
type TurnSandboxCommandArgs,
|
|
168
|
+
type TurnSandboxCommandSession,
|
|
169
|
+
type TurnToolCancellationFence,
|
|
170
|
+
} from "./sandbox/turn-tool-cancellation";
|
|
136
171
|
import { computerUse, type ComputerToolMode } from "./sandbox-computer";
|
|
137
172
|
import type { RuntimeMetricsHooks } from "./metrics";
|
|
173
|
+
export {
|
|
174
|
+
getSkillLibraryEntry,
|
|
175
|
+
isSkillLibraryEntryId,
|
|
176
|
+
listSkillLibraryEntries,
|
|
177
|
+
loadSkillLibrarySkill,
|
|
178
|
+
type SkillLibraryEntry,
|
|
179
|
+
type SkillLibraryFile,
|
|
180
|
+
type SkillLibrarySkill,
|
|
181
|
+
} from "./skill-library";
|
|
138
182
|
|
|
139
183
|
export type { RuntimeMetricsHooks } from "./metrics";
|
|
184
|
+
export type { TurnToolCancellationFence } from "./sandbox/turn-tool-cancellation";
|
|
140
185
|
|
|
141
186
|
// P4.3 computer-use surface (the agent's :0 driver). Re-exported from the barrel
|
|
142
187
|
// so callers (the worker, live proofs) reach SandboxComputer/ComputerUseCapability
|
|
@@ -188,17 +233,25 @@ export { OpenAIChatCompletionsModel, OpenAIResponsesModel } from "@openai/agents
|
|
|
188
233
|
|
|
189
234
|
export {
|
|
190
235
|
CompactionNeededError,
|
|
236
|
+
CompactionProviderResponseError,
|
|
237
|
+
EmptyCompactionSummaryError,
|
|
191
238
|
buildCompactionPromptInput,
|
|
192
239
|
buildCompactionReplacementHistory,
|
|
240
|
+
compactionReplacementFingerprint,
|
|
193
241
|
compactionThresholdTokens,
|
|
194
242
|
clampCompactionThresholdRatio,
|
|
195
243
|
decideCompaction,
|
|
196
244
|
buildSummaryItem,
|
|
197
245
|
findCompactionNeededError,
|
|
198
246
|
isCompactionSummary,
|
|
247
|
+
latestCompactionReplacementFingerprint,
|
|
248
|
+
prepareCompactionPromptInput,
|
|
199
249
|
isUserMessage,
|
|
200
250
|
estimateTokens,
|
|
201
251
|
estimateItemTokens,
|
|
252
|
+
estimateCompleteModelInput,
|
|
253
|
+
estimateSerializedValueTokens,
|
|
254
|
+
hasModelGeneratedItem,
|
|
202
255
|
renderCompactionPromptInputForChat,
|
|
203
256
|
COMPACTION_SUMMARY_MARKER,
|
|
204
257
|
COMPACTION_PROMPT,
|
|
@@ -211,7 +264,11 @@ export {
|
|
|
211
264
|
isEphemeralInternalContext,
|
|
212
265
|
USER_MESSAGE_TRUNCATION_MARKER,
|
|
213
266
|
} from "./context-compaction";
|
|
214
|
-
export type {
|
|
267
|
+
export type {
|
|
268
|
+
CompactionDecision,
|
|
269
|
+
CompactionItem,
|
|
270
|
+
PreparedCompactionPromptInput,
|
|
271
|
+
} from "./context-compaction";
|
|
215
272
|
export { modelCallUsageTelemetry } from "./usage-telemetry";
|
|
216
273
|
export type { ModelCallUsageTelemetry } from "./usage-telemetry";
|
|
217
274
|
|
|
@@ -259,9 +316,11 @@ export type ResolveConnectionCredentialResult =
|
|
|
259
316
|
status: "auth_needed";
|
|
260
317
|
reason: ToolAuthNeededPayload["reason"];
|
|
261
318
|
providerDomain: string;
|
|
319
|
+
provider?: string;
|
|
262
320
|
connectionId?: string;
|
|
263
321
|
scopes?: string[];
|
|
264
322
|
resource?: string;
|
|
323
|
+
selectedResources?: McpServerConnectionRef["selectedResources"];
|
|
265
324
|
authorizationUrl?: string;
|
|
266
325
|
};
|
|
267
326
|
|
|
@@ -316,6 +375,11 @@ export type AgentSegmentInput =
|
|
|
316
375
|
approvalId: string;
|
|
317
376
|
decision: "approve" | "reject";
|
|
318
377
|
message?: string;
|
|
378
|
+
}
|
|
379
|
+
| {
|
|
380
|
+
kind: "human_input";
|
|
381
|
+
serializedRunState: string;
|
|
382
|
+
toolCallId: string;
|
|
319
383
|
};
|
|
320
384
|
|
|
321
385
|
export type PreparedAgentInput = {
|
|
@@ -323,6 +387,13 @@ export type PreparedAgentInput = {
|
|
|
323
387
|
sandboxSessionState?: SandboxSessionState;
|
|
324
388
|
};
|
|
325
389
|
|
|
390
|
+
export const HUMAN_INPUT_TOOL_NAME = "request_human_input";
|
|
391
|
+
|
|
392
|
+
export type SerializedHumanInputInterruption = {
|
|
393
|
+
toolCallId: string;
|
|
394
|
+
input: ReturnType<typeof RequestHumanInputToolInput.parse>;
|
|
395
|
+
};
|
|
396
|
+
|
|
326
397
|
export type SandboxFileDownload = {
|
|
327
398
|
fileId: string;
|
|
328
399
|
mountPath: string;
|
|
@@ -381,6 +452,7 @@ export type OpenGeniRuntime = {
|
|
|
381
452
|
options?: RunAgentStreamOptions,
|
|
382
453
|
) => Promise<Awaited<ReturnType<typeof runAgentStream>>>;
|
|
383
454
|
serializeApprovals: (interruptions: unknown[]) => unknown[];
|
|
455
|
+
serializeHumanInputRequests?: (interruptions: unknown[]) => SerializedHumanInputInterruption[];
|
|
384
456
|
};
|
|
385
457
|
|
|
386
458
|
export type ProductionRuntimeOverrides = {
|
|
@@ -416,6 +488,7 @@ export function createProductionAgentRuntime(
|
|
|
416
488
|
sandboxClient: overrides.sandboxClient,
|
|
417
489
|
}),
|
|
418
490
|
serializeApprovals,
|
|
491
|
+
serializeHumanInputRequests,
|
|
419
492
|
};
|
|
420
493
|
}
|
|
421
494
|
|
|
@@ -804,19 +877,28 @@ export async function summarizeForCompaction(
|
|
|
804
877
|
const maxTokens = options.maxOutputTokens ?? SUMMARY_BUFFER_TOKENS;
|
|
805
878
|
if (api === "chat") {
|
|
806
879
|
const transcript = renderCompactionPromptInputForChat(input);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
880
|
+
let completion: unknown;
|
|
881
|
+
try {
|
|
882
|
+
completion = await client.chat.completions.create({
|
|
883
|
+
model,
|
|
884
|
+
max_tokens: maxTokens,
|
|
885
|
+
messages: [{ role: "user", content: transcript }],
|
|
886
|
+
...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
|
|
887
|
+
} as any);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(error), error);
|
|
890
|
+
}
|
|
813
891
|
const usage = modelResponseUsageFromResponse(completion);
|
|
814
892
|
if (usage) {
|
|
815
893
|
await options.onUsage?.(usage);
|
|
816
894
|
}
|
|
817
895
|
const text = (completion as { choices?: Array<{ message?: { content?: unknown } }> })
|
|
818
896
|
.choices?.[0]?.message?.content;
|
|
819
|
-
|
|
897
|
+
const summary = typeof text === "string" ? text.trim() : "";
|
|
898
|
+
if (!summary) {
|
|
899
|
+
throw new EmptyCompactionSummaryError(compactionResponseDiagnostics(completion, summary));
|
|
900
|
+
}
|
|
901
|
+
return summary;
|
|
820
902
|
}
|
|
821
903
|
// Use the same SDK Responses adapter as the real agent call. It converts the
|
|
822
904
|
// structured AgentInputItems (callId/providerData/etc.) to provider wire
|
|
@@ -839,12 +921,205 @@ export async function summarizeForCompaction(
|
|
|
839
921
|
handoffs: [],
|
|
840
922
|
tracing: false,
|
|
841
923
|
};
|
|
842
|
-
|
|
924
|
+
let response: unknown;
|
|
925
|
+
try {
|
|
926
|
+
response = await new CompactionResponsesModel(client, model).fetchResponse(request);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(error), error);
|
|
929
|
+
}
|
|
843
930
|
const usage = modelResponseUsageFromResponse(response);
|
|
844
931
|
if (usage) {
|
|
845
932
|
await options.onUsage?.(usage);
|
|
846
933
|
}
|
|
847
|
-
|
|
934
|
+
if (isFailedCompactionProviderResponse(response)) {
|
|
935
|
+
throw new CompactionProviderResponseError(compactionProviderFailureDiagnostics(response));
|
|
936
|
+
}
|
|
937
|
+
const summary = extractResponseOutputText(response).trim();
|
|
938
|
+
if (!summary) {
|
|
939
|
+
throw new EmptyCompactionSummaryError(compactionResponseDiagnostics(response, summary));
|
|
940
|
+
}
|
|
941
|
+
return summary;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function isFailedCompactionProviderResponse(response: unknown): boolean {
|
|
945
|
+
if (!response || typeof response !== "object") return false;
|
|
946
|
+
const record = response as Record<string, unknown>;
|
|
947
|
+
return (
|
|
948
|
+
record.status === "failed" ||
|
|
949
|
+
record.status === "incomplete" ||
|
|
950
|
+
(record.error !== null && record.error !== undefined)
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** Bounded provider diagnostics: identifiers and classifications, never messages or model data. */
|
|
955
|
+
export function compactionProviderFailureDiagnostics(error: unknown): Record<string, unknown> {
|
|
956
|
+
let current = error;
|
|
957
|
+
let errorName: string | null = null;
|
|
958
|
+
let httpStatus: number | null = null;
|
|
959
|
+
let responseStatus: string | null = null;
|
|
960
|
+
let responseId: string | null = null;
|
|
961
|
+
let code: string | null = null;
|
|
962
|
+
let type: string | null = null;
|
|
963
|
+
let requestId: string | null = null;
|
|
964
|
+
let eventType: string | null = null;
|
|
965
|
+
const seen = new Set<object>();
|
|
966
|
+
for (let depth = 0; depth < 6 && current && typeof current === "object"; depth += 1) {
|
|
967
|
+
if (seen.has(current)) break;
|
|
968
|
+
seen.add(current);
|
|
969
|
+
const record = current as Record<string, unknown>;
|
|
970
|
+
if (!errorName && current instanceof Error) {
|
|
971
|
+
errorName = boundCompactionDiagnosticField(current.name);
|
|
972
|
+
}
|
|
973
|
+
if (
|
|
974
|
+
httpStatus === null &&
|
|
975
|
+
typeof record.status === "number" &&
|
|
976
|
+
Number.isFinite(record.status)
|
|
977
|
+
) {
|
|
978
|
+
httpStatus = record.status;
|
|
979
|
+
}
|
|
980
|
+
if (responseStatus === null && typeof record.status === "string") {
|
|
981
|
+
responseStatus = boundCompactionDiagnosticField(record.status);
|
|
982
|
+
}
|
|
983
|
+
const directResponseStatus = record.response_status ?? record.responseStatus;
|
|
984
|
+
if (responseStatus === null && typeof directResponseStatus === "string") {
|
|
985
|
+
responseStatus = boundCompactionDiagnosticField(directResponseStatus);
|
|
986
|
+
}
|
|
987
|
+
const directResponseId = record.response_id ?? record.responseId ?? record.id;
|
|
988
|
+
if (responseId === null && typeof directResponseId === "string") {
|
|
989
|
+
responseId = boundCompactionDiagnosticField(directResponseId);
|
|
990
|
+
}
|
|
991
|
+
if (code === null && typeof record.code === "string") {
|
|
992
|
+
code = boundCompactionDiagnosticField(record.code);
|
|
993
|
+
}
|
|
994
|
+
if (type === null && typeof record.type === "string") {
|
|
995
|
+
type = boundCompactionDiagnosticField(record.type);
|
|
996
|
+
}
|
|
997
|
+
const directEventType = record.event_type ?? record.eventType;
|
|
998
|
+
if (eventType === null && typeof directEventType === "string") {
|
|
999
|
+
eventType = boundCompactionDiagnosticField(directEventType);
|
|
1000
|
+
}
|
|
1001
|
+
if (requestId === null) {
|
|
1002
|
+
const directRequestId = record.request_id ?? record.requestId ?? record._request_id;
|
|
1003
|
+
if (typeof directRequestId === "string") {
|
|
1004
|
+
requestId = boundCompactionDiagnosticField(directRequestId);
|
|
1005
|
+
}
|
|
1006
|
+
const headers = record.headers;
|
|
1007
|
+
if (!requestId && headers && typeof headers === "object") {
|
|
1008
|
+
const get = (headers as { get?: unknown }).get;
|
|
1009
|
+
if (typeof get === "function") {
|
|
1010
|
+
const headerId = get.call(headers, "x-request-id");
|
|
1011
|
+
if (typeof headerId === "string") {
|
|
1012
|
+
requestId = boundCompactionDiagnosticField(headerId);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const nestedError = record.error;
|
|
1018
|
+
if (nestedError && typeof nestedError === "object" && !seen.has(nestedError)) {
|
|
1019
|
+
const nested = nestedError as Record<string, unknown>;
|
|
1020
|
+
if (code === null && typeof nested.code === "string") {
|
|
1021
|
+
code = boundCompactionDiagnosticField(nested.code);
|
|
1022
|
+
}
|
|
1023
|
+
if (type === null && typeof nested.type === "string") {
|
|
1024
|
+
type = boundCompactionDiagnosticField(nested.type);
|
|
1025
|
+
}
|
|
1026
|
+
const nestedResponseStatus = nested.response_status ?? nested.responseStatus;
|
|
1027
|
+
if (responseStatus === null && typeof nestedResponseStatus === "string") {
|
|
1028
|
+
responseStatus = boundCompactionDiagnosticField(nestedResponseStatus);
|
|
1029
|
+
}
|
|
1030
|
+
const nestedResponseId = nested.response_id ?? nested.responseId ?? nested.id;
|
|
1031
|
+
if (responseId === null && typeof nestedResponseId === "string") {
|
|
1032
|
+
responseId = boundCompactionDiagnosticField(nestedResponseId);
|
|
1033
|
+
}
|
|
1034
|
+
const nestedEventType = nested.event_type ?? nested.eventType;
|
|
1035
|
+
if (eventType === null && typeof nestedEventType === "string") {
|
|
1036
|
+
eventType = boundCompactionDiagnosticField(nestedEventType);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
current = record.cause;
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
errorName,
|
|
1043
|
+
httpStatus,
|
|
1044
|
+
responseStatus,
|
|
1045
|
+
responseId,
|
|
1046
|
+
code,
|
|
1047
|
+
type,
|
|
1048
|
+
requestId,
|
|
1049
|
+
...(eventType ? { eventType } : {}),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES = 256;
|
|
1054
|
+
const COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER = "…[truncated]";
|
|
1055
|
+
|
|
1056
|
+
function boundCompactionDiagnosticField(value: string): string {
|
|
1057
|
+
const bytes = Buffer.from(value, "utf8");
|
|
1058
|
+
if (bytes.length <= COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES) return value;
|
|
1059
|
+
const markerBytes = Buffer.byteLength(COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER, "utf8");
|
|
1060
|
+
let end = COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES - markerBytes;
|
|
1061
|
+
while (end > 0 && isUtf8ContinuationByte(bytes[end]!)) end -= 1;
|
|
1062
|
+
return `${bytes.subarray(0, end).toString("utf8")}${COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER}`;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function isUtf8ContinuationByte(value: number): boolean {
|
|
1066
|
+
return (value & 0xc0) === 0x80;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** Bounded, content-free diagnostics for a semantically empty checkpoint. */
|
|
1070
|
+
export function compactionResponseDiagnostics(
|
|
1071
|
+
response: unknown,
|
|
1072
|
+
extractedText = extractResponseOutputText(response).trim(),
|
|
1073
|
+
): Record<string, unknown> {
|
|
1074
|
+
if (!response || typeof response !== "object") {
|
|
1075
|
+
return { responseShape: typeof response, extractedTextLength: extractedText.length };
|
|
1076
|
+
}
|
|
1077
|
+
const record = response as Record<string, unknown>;
|
|
1078
|
+
const output = Array.isArray(record.output) ? record.output : [];
|
|
1079
|
+
const outputItems = output.slice(0, 50).map((item) => {
|
|
1080
|
+
if (!item || typeof item !== "object") return { type: typeof item };
|
|
1081
|
+
const value = item as Record<string, unknown>;
|
|
1082
|
+
const content = Array.isArray(value.content) ? value.content : [];
|
|
1083
|
+
return {
|
|
1084
|
+
type: typeof value.type === "string" ? value.type : null,
|
|
1085
|
+
role: typeof value.role === "string" ? value.role : null,
|
|
1086
|
+
status: typeof value.status === "string" ? value.status : null,
|
|
1087
|
+
contentPartTypes: content
|
|
1088
|
+
.slice(0, 50)
|
|
1089
|
+
.map((part) =>
|
|
1090
|
+
part && typeof part === "object" && typeof (part as { type?: unknown }).type === "string"
|
|
1091
|
+
? (part as { type: string }).type
|
|
1092
|
+
: typeof part,
|
|
1093
|
+
),
|
|
1094
|
+
contentPartCount: content.length,
|
|
1095
|
+
};
|
|
1096
|
+
});
|
|
1097
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
1098
|
+
const usage = modelResponseUsageFromResponse(response)?.usage;
|
|
1099
|
+
const incomplete =
|
|
1100
|
+
record.incomplete_details && typeof record.incomplete_details === "object"
|
|
1101
|
+
? (record.incomplete_details as Record<string, unknown>)
|
|
1102
|
+
: null;
|
|
1103
|
+
return {
|
|
1104
|
+
responseId: typeof record.id === "string" ? record.id : null,
|
|
1105
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
1106
|
+
outputItemCount: output.length,
|
|
1107
|
+
outputItems,
|
|
1108
|
+
choiceCount: choices.length,
|
|
1109
|
+
finishReasons: choices
|
|
1110
|
+
.slice(0, 20)
|
|
1111
|
+
.map((choice) =>
|
|
1112
|
+
choice && typeof choice === "object"
|
|
1113
|
+
? ((choice as Record<string, unknown>).finish_reason ?? null)
|
|
1114
|
+
: null,
|
|
1115
|
+
),
|
|
1116
|
+
incompleteReason:
|
|
1117
|
+
incomplete && typeof incomplete.reason === "string" ? incomplete.reason : null,
|
|
1118
|
+
inputTokens: usage?.inputTokens ?? null,
|
|
1119
|
+
outputTokens: usage?.outputTokens ?? null,
|
|
1120
|
+
totalTokens: usage?.totalTokens ?? null,
|
|
1121
|
+
extractedTextLength: extractedText.length,
|
|
1122
|
+
};
|
|
848
1123
|
}
|
|
849
1124
|
|
|
850
1125
|
/**
|
|
@@ -909,10 +1184,49 @@ class CompactionResponsesModel extends OpenAIResponsesModel {
|
|
|
909
1184
|
}
|
|
910
1185
|
|
|
911
1186
|
export type GitTokenSeeds = Partial<Record<GitCredentialProvider, string>>;
|
|
1187
|
+
export type GitCredentialBindingSeed = {
|
|
1188
|
+
credentialBindingId: string;
|
|
1189
|
+
provider: GitCredentialProvider;
|
|
1190
|
+
token: string;
|
|
1191
|
+
expiresAt?: string;
|
|
1192
|
+
/** Total active bindings for this provider, used to suppress unsafe aliases. */
|
|
1193
|
+
providerBindingCount?: number;
|
|
1194
|
+
};
|
|
912
1195
|
export type GitCredentialTokenWriterSession = SandboxSessionLike;
|
|
1196
|
+
export type ToolspaceTokenWriterSession = SandboxSessionLike;
|
|
1197
|
+
|
|
1198
|
+
export type EffectiveSkillSelection = Readonly<{
|
|
1199
|
+
id: string;
|
|
1200
|
+
name: string;
|
|
1201
|
+
source: "bundled" | "library" | "pack";
|
|
1202
|
+
version: string | null;
|
|
1203
|
+
contentSha256: string | null;
|
|
1204
|
+
reason: string;
|
|
1205
|
+
}>;
|
|
1206
|
+
|
|
1207
|
+
const agentSkillSelections = new WeakMap<object, readonly EffectiveSkillSelection[]>();
|
|
1208
|
+
const emptySkillSelections: readonly EffectiveSkillSelection[] = Object.freeze([]);
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Read-only, secret-free skill provenance for an already-built agent. This is
|
|
1212
|
+
* intentionally separate from the model instructions and sandbox manifest so
|
|
1213
|
+
* effective configuration inspection cannot accidentally expose credentials or
|
|
1214
|
+
* turn skill activation into an authorization change.
|
|
1215
|
+
*/
|
|
1216
|
+
export function effectiveSkillSelectionsForAgent(
|
|
1217
|
+
agent: object,
|
|
1218
|
+
): readonly EffectiveSkillSelection[] {
|
|
1219
|
+
return agentSkillSelections.get(agent) ?? emptySkillSelections;
|
|
1220
|
+
}
|
|
913
1221
|
|
|
914
1222
|
export type BuildAgentOptions = {
|
|
915
1223
|
model?: Model;
|
|
1224
|
+
/** Settled response for the one internal human-input interruption resumed by this run. */
|
|
1225
|
+
humanInputResponse?: {
|
|
1226
|
+
requestId: string;
|
|
1227
|
+
toolCallId: string;
|
|
1228
|
+
response: HumanInputResponse;
|
|
1229
|
+
};
|
|
916
1230
|
reasoningEffort?: ReasoningEffort;
|
|
917
1231
|
// Per-turn gating overrides for the multi-provider path. Each defaults to
|
|
918
1232
|
// today's settings-derived behaviour when omitted, so the legacy
|
|
@@ -1008,11 +1322,18 @@ export type BuildAgentOptions = {
|
|
|
1008
1322
|
// repository-clone hooks; runStream forwards them into the hook context, which
|
|
1009
1323
|
// seeds provider token FILES before the clone/setup runs.
|
|
1010
1324
|
gitTokenSeeds?: GitTokenSeeds;
|
|
1325
|
+
// Provider-neutral, independently mintable credentials. Binding ids remain
|
|
1326
|
+
// off-manifest and are hashed before they influence sandbox paths.
|
|
1327
|
+
gitCredentialBindings?: GitCredentialBindingSeed[];
|
|
1011
1328
|
// TOOLSPACE: the run-scoped delegated token to seed into
|
|
1012
1329
|
// $OPENGENI_TOOLSPACE_TOKEN_FILE. Like gitTokenSeed, this stays off the
|
|
1013
1330
|
// manifest/env delta and is written into the sandbox filesystem by a lifecycle
|
|
1014
1331
|
// hook before the agent starts.
|
|
1015
1332
|
toolspaceTokenSeed?: string;
|
|
1333
|
+
// Durable OpenGeni session identity used only to derive the off-manifest,
|
|
1334
|
+
// per-session token file. Required together with toolspaceTokenSeed so two
|
|
1335
|
+
// sessions sharing one box never overwrite the same pointer.
|
|
1336
|
+
toolspaceTokenSessionId?: string;
|
|
1016
1337
|
// Genesis turn only: inject a one-shot instruction into the FIRST model
|
|
1017
1338
|
// call telling it to title the session via opengeni__set_session_title.
|
|
1018
1339
|
// Keeping this out of the persistent Agent.instructions prevents every
|
|
@@ -1040,10 +1361,32 @@ export type BuildAgentOptions = {
|
|
|
1040
1361
|
// timeline message. Omitted ⇒ the composed instructions are byte-identical to
|
|
1041
1362
|
// a workspace-only persona.
|
|
1042
1363
|
sessionInstructions?: string;
|
|
1364
|
+
// Host context for this exact accepted turn. Composed system-level after the
|
|
1365
|
+
// durable session persona and omitted from all later turns.
|
|
1366
|
+
turnInstructions?: string;
|
|
1043
1367
|
// Skills delivered by enabled capability packs. They join the bundled
|
|
1044
1368
|
// skills in the sandbox skill index (mounted under .agents/) so
|
|
1045
1369
|
// skills/<name> references resolve like any other indexed skill.
|
|
1046
1370
|
packSkills?: PackSkill[];
|
|
1371
|
+
// Explicitly selected, immutable curated-library skill content. These are
|
|
1372
|
+
// separate from packSkills so repository-local/pack compatibility does not
|
|
1373
|
+
// turn a curated entry into a mutable override.
|
|
1374
|
+
skillLibrarySkills?: PackSkill[];
|
|
1375
|
+
// Secret-free provenance for effective-configuration inspection. The worker
|
|
1376
|
+
// resolves exact ids/versions/hashes before constructing the agent.
|
|
1377
|
+
skillLibrarySelections?: EffectiveSkillSelection[];
|
|
1378
|
+
/**
|
|
1379
|
+
* Internal per-attempt cancellation boundary. The worker supplies Temporal's
|
|
1380
|
+
* signal so an in-flight shell process is interrupted immediately instead of
|
|
1381
|
+
* holding Steer/Pause behind its requested yield or natural exit.
|
|
1382
|
+
*/
|
|
1383
|
+
turnCancellationSignal?: AbortSignal;
|
|
1384
|
+
/**
|
|
1385
|
+
* Receives the physical sandbox-tool fence at agent construction time. A
|
|
1386
|
+
* cancelled/fenced worker attempt must await it before publishing its durable
|
|
1387
|
+
* attempt-quiesced receipt.
|
|
1388
|
+
*/
|
|
1389
|
+
onToolCancellationFence?: (fence: TurnToolCancellationFence) => void;
|
|
1047
1390
|
};
|
|
1048
1391
|
|
|
1049
1392
|
export type PackSkillFile = {
|
|
@@ -1171,6 +1514,12 @@ export function appendSessionInstructions(composed: string, sessionInstructions?
|
|
|
1171
1514
|
return trimmed ? `${composed} ${trimmed}` : composed;
|
|
1172
1515
|
}
|
|
1173
1516
|
|
|
1517
|
+
/** Append system instructions that apply to this exact turn only. */
|
|
1518
|
+
export function appendTurnInstructions(composed: string, turnInstructions?: string): string {
|
|
1519
|
+
const trimmed = turnInstructions?.trim();
|
|
1520
|
+
return trimmed ? `${composed} ${trimmed}` : composed;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1174
1523
|
/**
|
|
1175
1524
|
* Append the durable session metadata the model otherwise cannot observe.
|
|
1176
1525
|
* This is deliberately declarative and excludes the title text: the title is
|
|
@@ -1239,7 +1588,9 @@ const agentRepositoryCloneHooks = new WeakMap<object, SandboxLifecycleHook[]>();
|
|
|
1239
1588
|
// provided-session env; runStream reads them to build the clone hook context.
|
|
1240
1589
|
// Absent when no brokered repo is attached / on the selfhosted path.
|
|
1241
1590
|
const agentGitTokenSeeds = new WeakMap<object, GitTokenSeeds>();
|
|
1591
|
+
const agentGitCredentialBindings = new WeakMap<object, GitCredentialBindingSeed[]>();
|
|
1242
1592
|
const agentToolspaceTokenSeed = new WeakMap<object, string>();
|
|
1593
|
+
const agentToolspaceTokenSessionId = new WeakMap<object, string>();
|
|
1243
1594
|
// A genesis directive is consumed by runAgentStream exactly once for the
|
|
1244
1595
|
// freshly-built agent. It must not remain in Agent.instructions: those
|
|
1245
1596
|
// instructions are presented again on every internal model/tool loop.
|
|
@@ -1308,6 +1659,9 @@ export function buildOpenGeniAgent(
|
|
|
1308
1659
|
resources: ResourceRef[],
|
|
1309
1660
|
options: BuildAgentOptions = {},
|
|
1310
1661
|
): Agent<any, any> {
|
|
1662
|
+
if (Boolean(options.toolspaceTokenSeed) !== Boolean(options.toolspaceTokenSessionId)) {
|
|
1663
|
+
throw new Error("toolspaceTokenSeed and toolspaceTokenSessionId must be supplied together");
|
|
1664
|
+
}
|
|
1311
1665
|
// Resolved per-turn gating. Each override defaults to today's settings-derived
|
|
1312
1666
|
// behaviour, so the legacy global-client callers (no resolved model) build the
|
|
1313
1667
|
// exact same agent as before; the multi-provider worker path passes the
|
|
@@ -1329,6 +1683,28 @@ export function buildOpenGeniAgent(
|
|
|
1329
1683
|
// [...agent.tools, ...capability.tools()]), so hosted web_search coexists with
|
|
1330
1684
|
// both rather than overriding them.
|
|
1331
1685
|
const hostedTools = hostedWebSearch ? [webSearchTool()] : [];
|
|
1686
|
+
const humanInputTool = agentTool({
|
|
1687
|
+
name: HUMAN_INPUT_TOOL_NAME,
|
|
1688
|
+
description:
|
|
1689
|
+
"Pause this turn and request structured human input. Use for decisions or missing information that only a person can provide. Supports free text, single-select, multi-select, an optional Other value, multiple questions, explicit skip policy, and an optional expiry.",
|
|
1690
|
+
parameters: RequestHumanInputToolInput,
|
|
1691
|
+
needsApproval: true,
|
|
1692
|
+
// A missing/mismatched durable response is a protocol integrity failure,
|
|
1693
|
+
// not model-visible tool output the agent may reason past.
|
|
1694
|
+
errorFunction: null,
|
|
1695
|
+
execute: (_input, _context, details) => {
|
|
1696
|
+
const settled = options.humanInputResponse;
|
|
1697
|
+
if (!settled) {
|
|
1698
|
+
throw new Error("Human-input tool resumed without a durable response");
|
|
1699
|
+
}
|
|
1700
|
+
const resumedCallId = details?.toolCall?.callId;
|
|
1701
|
+
if (resumedCallId && resumedCallId !== settled.toolCallId) {
|
|
1702
|
+
throw new Error("Human-input response does not belong to the resumed tool call");
|
|
1703
|
+
}
|
|
1704
|
+
return JSON.stringify({ requestId: settled.requestId, ...settled.response });
|
|
1705
|
+
},
|
|
1706
|
+
});
|
|
1707
|
+
const agentTools = [...hostedTools, humanInputTool];
|
|
1332
1708
|
const baseConfig = {
|
|
1333
1709
|
name: "OpenGeni Agent",
|
|
1334
1710
|
model: options.model ?? settings.openaiModel,
|
|
@@ -1351,24 +1727,28 @@ export function buildOpenGeniAgent(
|
|
|
1351
1727
|
// and the worker resolved a nonblank block — appendWorkspaceMemory,
|
|
1352
1728
|
// 4. + the per-session persona instructions (session-specific, so it
|
|
1353
1729
|
// refines both the workspace persona and the substrate note),
|
|
1354
|
-
// 5. +
|
|
1730
|
+
// 5. + host context for this exact turn, when supplied,
|
|
1731
|
+
// 6. + durable session-setting state (title present + child notification
|
|
1355
1732
|
// mode), when supplied by the worker,
|
|
1356
1733
|
// The genesis title directive is deliberately NOT part of this persistent
|
|
1357
1734
|
// string. runAgentStream injects it into the first model call only.
|
|
1358
1735
|
instructions: appendPersistentSessionSettings(
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1736
|
+
appendTurnInstructions(
|
|
1737
|
+
appendSessionInstructions(
|
|
1738
|
+
appendWorkspaceMemory(
|
|
1739
|
+
appendToolspaceInstructions(
|
|
1740
|
+
composeAgentInstructions(
|
|
1741
|
+
options.instructionsTemplate ?? settings.agentInstructionsTemplate,
|
|
1742
|
+
options.workspaceEnvironment,
|
|
1743
|
+
options.rig,
|
|
1744
|
+
),
|
|
1745
|
+
settings.toolspaceEnabled && Boolean(options.toolspaceTokenSeed),
|
|
1366
1746
|
),
|
|
1367
|
-
|
|
1747
|
+
options.workspaceMemory,
|
|
1368
1748
|
),
|
|
1369
|
-
options.
|
|
1749
|
+
options.sessionInstructions,
|
|
1370
1750
|
),
|
|
1371
|
-
options.
|
|
1751
|
+
options.turnInstructions,
|
|
1372
1752
|
),
|
|
1373
1753
|
options.persistentSessionSettings,
|
|
1374
1754
|
),
|
|
@@ -1391,7 +1771,7 @@ export function buildOpenGeniAgent(
|
|
|
1391
1771
|
// `new Agent(baseConfig)` path (sandboxBackend === "none") and the
|
|
1392
1772
|
// `new SandboxAgent({ ...baseConfig, ... })` path via the shared baseConfig
|
|
1393
1773
|
// spread; the SDK concatenates these with MCP and sandbox capability tools.
|
|
1394
|
-
|
|
1774
|
+
tools: agentTools,
|
|
1395
1775
|
...(options.mcpServers?.length ? { mcpServers: options.mcpServers } : {}),
|
|
1396
1776
|
// Surface FAILED MCP tool calls as `{ isError: true }` tool output (see
|
|
1397
1777
|
// mcpToolErrorFunction / mcpToolErrorOutput) instead of the SDK's default
|
|
@@ -1423,6 +1803,9 @@ export function buildOpenGeniAgent(
|
|
|
1423
1803
|
),
|
|
1424
1804
|
...(runAs ? { runAs } : {}),
|
|
1425
1805
|
capabilities: buildAgentCapabilities(settings, options.packSkills ?? [], {
|
|
1806
|
+
...(options.skillLibrarySkills?.length
|
|
1807
|
+
? { skillLibrarySkills: options.skillLibrarySkills }
|
|
1808
|
+
: {}),
|
|
1426
1809
|
...(options.structuredToolTransport !== undefined
|
|
1427
1810
|
? { structuredToolTransport: options.structuredToolTransport }
|
|
1428
1811
|
: {}),
|
|
@@ -1430,8 +1813,24 @@ export function buildOpenGeniAgent(
|
|
|
1430
1813
|
? { computerToolMode: options.computerToolMode }
|
|
1431
1814
|
: {}),
|
|
1432
1815
|
...(options.onComputerUseReady ? { onComputerUseReady: options.onComputerUseReady } : {}),
|
|
1816
|
+
...(options.turnCancellationSignal
|
|
1817
|
+
? { turnCancellationSignal: options.turnCancellationSignal }
|
|
1818
|
+
: {}),
|
|
1819
|
+
...(options.onToolCancellationFence
|
|
1820
|
+
? { onToolCancellationFence: options.onToolCancellationFence }
|
|
1821
|
+
: {}),
|
|
1433
1822
|
}),
|
|
1434
1823
|
});
|
|
1824
|
+
agentSkillSelections.set(
|
|
1825
|
+
agent,
|
|
1826
|
+
Object.freeze(
|
|
1827
|
+
effectiveSkillSelections(
|
|
1828
|
+
options.skillLibrarySelections ?? [],
|
|
1829
|
+
options.skillLibrarySkills ?? [],
|
|
1830
|
+
options.packSkills ?? [],
|
|
1831
|
+
).map((selection) => Object.freeze(selection)),
|
|
1832
|
+
),
|
|
1833
|
+
);
|
|
1435
1834
|
if (options.genesisTitleHint) {
|
|
1436
1835
|
agentsNeedingGenesisTitleDirective.add(agent);
|
|
1437
1836
|
}
|
|
@@ -1463,8 +1862,12 @@ export function buildOpenGeniAgent(
|
|
|
1463
1862
|
if (Object.keys(gitTokenSeeds).length > 0) {
|
|
1464
1863
|
agentGitTokenSeeds.set(agent, gitTokenSeeds);
|
|
1465
1864
|
}
|
|
1865
|
+
if (options.gitCredentialBindings && options.gitCredentialBindings.length > 0) {
|
|
1866
|
+
agentGitCredentialBindings.set(agent, options.gitCredentialBindings);
|
|
1867
|
+
}
|
|
1466
1868
|
if (options.toolspaceTokenSeed) {
|
|
1467
1869
|
agentToolspaceTokenSeed.set(agent, options.toolspaceTokenSeed);
|
|
1870
|
+
agentToolspaceTokenSessionId.set(agent, options.toolspaceTokenSessionId!);
|
|
1468
1871
|
}
|
|
1469
1872
|
// M3: stash the rig setup descriptor + RESOLVE the rig credential hooks now.
|
|
1470
1873
|
// sandboxLifecycleHooksForIds throws on an unknown hook name, so a typo'd rig
|
|
@@ -1721,6 +2124,7 @@ export function buildAgentCapabilities(
|
|
|
1721
2124
|
settings: Settings,
|
|
1722
2125
|
packSkills: PackSkill[],
|
|
1723
2126
|
options: {
|
|
2127
|
+
skillLibrarySkills?: PackSkill[];
|
|
1724
2128
|
structuredToolTransport?: boolean;
|
|
1725
2129
|
// EXPLICIT computer-use transport (see BuildAgentOptions.computerToolMode). When
|
|
1726
2130
|
// present, computerUse() is handed the mode directly and its tools() obeys it
|
|
@@ -1728,8 +2132,15 @@ export function buildAgentCapabilities(
|
|
|
1728
2132
|
// imageFunctionResults path (driven by structuredToolTransport) is unchanged.
|
|
1729
2133
|
computerToolMode?: ComputerToolMode;
|
|
1730
2134
|
onComputerUseReady?: (session: SandboxSessionLike) => Promise<void>;
|
|
2135
|
+
turnCancellationSignal?: AbortSignal;
|
|
2136
|
+
onToolCancellationFence?: (fence: TurnToolCancellationFence) => void;
|
|
1731
2137
|
} = {},
|
|
1732
2138
|
): ReturnType<typeof Capabilities.default> {
|
|
2139
|
+
const toolCancellation =
|
|
2140
|
+
options.turnCancellationSignal || options.onToolCancellationFence
|
|
2141
|
+
? createTurnToolCancellationController(options.turnCancellationSignal)
|
|
2142
|
+
: null;
|
|
2143
|
+
if (toolCancellation) options.onToolCancellationFence?.(toolCancellation);
|
|
1733
2144
|
// The `filesystem()` capability picks hosted-vs-function tool variants from the
|
|
1734
2145
|
// bound model instance (supportsApplyPatchTransport / structured tool output).
|
|
1735
2146
|
// When the caller declares the backend does NOT support that structured/hosted
|
|
@@ -1748,9 +2159,13 @@ export function buildAgentCapabilities(
|
|
|
1748
2159
|
}
|
|
1749
2160
|
const caps: ReturnType<typeof Capabilities.default> = [
|
|
1750
2161
|
filesystemCapability,
|
|
1751
|
-
shell({ configureTools: withExecOpCorrelation }),
|
|
2162
|
+
shell({ ...(toolCancellation ? {} : { configureTools: withExecOpCorrelation }) }),
|
|
1752
2163
|
];
|
|
1753
|
-
caps.push(
|
|
2164
|
+
caps.push(
|
|
2165
|
+
skills({
|
|
2166
|
+
lazyFrom: lazySkillSourceWithPackSkills(packSkills, options.skillLibrarySkills ?? []),
|
|
2167
|
+
}),
|
|
2168
|
+
);
|
|
1754
2169
|
// P4.3 computer-use: the agent drives the SAME :0 humans watch (xdotool/XTEST +
|
|
1755
2170
|
// scrot), but only when the desktop tier is ON, computer-use is enabled, and the
|
|
1756
2171
|
// backend is one whose image carries the X stack (descriptorgate — honest about
|
|
@@ -1802,6 +2217,14 @@ export function buildAgentCapabilities(
|
|
|
1802
2217
|
}
|
|
1803
2218
|
caps.push(computerCapability as unknown as ReturnType<typeof Capabilities.default>[number]);
|
|
1804
2219
|
}
|
|
2220
|
+
if (toolCancellation) {
|
|
2221
|
+
for (const capability of caps) {
|
|
2222
|
+
wrapCapabilityToolsForTurnCancellation(
|
|
2223
|
+
capability as unknown as { tools(): Tool<unknown>[] },
|
|
2224
|
+
toolCancellation,
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
1805
2228
|
return caps;
|
|
1806
2229
|
}
|
|
1807
2230
|
|
|
@@ -2023,9 +2446,13 @@ function connectionBrokerFetch(
|
|
|
2023
2446
|
status: "auth_needed",
|
|
2024
2447
|
reason: "expired",
|
|
2025
2448
|
providerDomain: connectionRef.providerDomain,
|
|
2449
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2026
2450
|
connectionId: refreshed.connectionId,
|
|
2027
2451
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2028
2452
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2453
|
+
...(connectionRef.selectedResources
|
|
2454
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2455
|
+
: {}),
|
|
2029
2456
|
},
|
|
2030
2457
|
connectionRef,
|
|
2031
2458
|
);
|
|
@@ -2054,9 +2481,13 @@ async function resolveConnectionForRequest(
|
|
|
2054
2481
|
status: "auth_needed",
|
|
2055
2482
|
reason: "missing_connection",
|
|
2056
2483
|
providerDomain: connectionRef.providerDomain,
|
|
2484
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2057
2485
|
...(connectionRef.connectionId ? { connectionId: connectionRef.connectionId } : {}),
|
|
2058
2486
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2059
2487
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2488
|
+
...(connectionRef.selectedResources
|
|
2489
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2490
|
+
: {}),
|
|
2060
2491
|
};
|
|
2061
2492
|
}
|
|
2062
2493
|
const request: ResolveConnectionCredentialInput = {
|
|
@@ -2074,9 +2505,13 @@ async function resolveConnectionForRequest(
|
|
|
2074
2505
|
status: "auth_needed",
|
|
2075
2506
|
reason: "refresh_failed",
|
|
2076
2507
|
providerDomain: connectionRef.providerDomain,
|
|
2508
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2077
2509
|
...(connectionRef.connectionId ? { connectionId: connectionRef.connectionId } : {}),
|
|
2078
2510
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
2079
2511
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
2512
|
+
...(connectionRef.selectedResources
|
|
2513
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2514
|
+
: {}),
|
|
2080
2515
|
};
|
|
2081
2516
|
}
|
|
2082
2517
|
}
|
|
@@ -2094,6 +2529,7 @@ function insufficientScopeAuth(
|
|
|
2094
2529
|
status: "auth_needed",
|
|
2095
2530
|
reason: "insufficient_scope",
|
|
2096
2531
|
providerDomain: connectionRef.providerDomain,
|
|
2532
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
2097
2533
|
connectionId,
|
|
2098
2534
|
...(challenge.scope?.length
|
|
2099
2535
|
? { scopes: challenge.scope }
|
|
@@ -2105,6 +2541,9 @@ function insufficientScopeAuth(
|
|
|
2105
2541
|
: connectionRef.resource
|
|
2106
2542
|
? { resource: connectionRef.resource }
|
|
2107
2543
|
: {}),
|
|
2544
|
+
...(connectionRef.selectedResources
|
|
2545
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2546
|
+
: {}),
|
|
2108
2547
|
};
|
|
2109
2548
|
}
|
|
2110
2549
|
|
|
@@ -2120,6 +2559,11 @@ async function authNeededFetchResponse(
|
|
|
2120
2559
|
serverId,
|
|
2121
2560
|
toolName: request.toolName ?? null,
|
|
2122
2561
|
providerDomain: auth.providerDomain,
|
|
2562
|
+
...(auth.provider
|
|
2563
|
+
? { provider: auth.provider }
|
|
2564
|
+
: connectionRef.provider
|
|
2565
|
+
? { provider: connectionRef.provider }
|
|
2566
|
+
: {}),
|
|
2123
2567
|
reason: auth.reason,
|
|
2124
2568
|
...(connectionId ? { connectionId } : {}),
|
|
2125
2569
|
...(auth.scopes
|
|
@@ -2132,6 +2576,11 @@ async function authNeededFetchResponse(
|
|
|
2132
2576
|
: connectionRef.resource
|
|
2133
2577
|
? { resource: connectionRef.resource }
|
|
2134
2578
|
: {}),
|
|
2579
|
+
...(auth.selectedResources
|
|
2580
|
+
? { selectedResources: auth.selectedResources }
|
|
2581
|
+
: connectionRef.selectedResources
|
|
2582
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
2583
|
+
: {}),
|
|
2135
2584
|
...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
|
|
2136
2585
|
...(options.subjectId ? { subjectId: options.subjectId } : {}),
|
|
2137
2586
|
});
|
|
@@ -2588,6 +3037,7 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2588
3037
|
// doesn't spam the log when the SDK re-lists across model turns.
|
|
2589
3038
|
private readonly bestEffort: boolean;
|
|
2590
3039
|
private loggedListToolsFailure = false;
|
|
3040
|
+
private listedToolSchemaTokens = 0;
|
|
2591
3041
|
|
|
2592
3042
|
constructor(
|
|
2593
3043
|
private readonly inner: MCPServer,
|
|
@@ -2644,12 +3094,19 @@ class PrefixedMcpServer implements MCPServer {
|
|
|
2644
3094
|
}
|
|
2645
3095
|
return [];
|
|
2646
3096
|
}
|
|
2647
|
-
|
|
3097
|
+
const exposed = tools
|
|
2648
3098
|
.filter((tool) => this.isAllowed(tool.name))
|
|
2649
3099
|
.map((tool) => ({
|
|
2650
3100
|
...tool,
|
|
2651
3101
|
name: prefixedMcpToolName(this.name, tool.name),
|
|
2652
3102
|
}));
|
|
3103
|
+
this.listedToolSchemaTokens = estimateSerializedValueTokens(exposed);
|
|
3104
|
+
return exposed;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/** Latest exact tools/list projection used to build the model request. */
|
|
3108
|
+
modelToolSchemaTokens(): number {
|
|
3109
|
+
return this.listedToolSchemaTokens;
|
|
2653
3110
|
}
|
|
2654
3111
|
|
|
2655
3112
|
async callTool(
|
|
@@ -2813,22 +3270,25 @@ export async function prepareRunInput(
|
|
|
2813
3270
|
...(sandboxSessionState ? { sandboxSessionState } : {}),
|
|
2814
3271
|
};
|
|
2815
3272
|
}
|
|
2816
|
-
// An
|
|
3273
|
+
// An interrupted tool can only be resumed against a real saved run state. If the
|
|
2817
3274
|
// latest blob is the cleared sentinel the awaiting turn was wiped (the API
|
|
2818
3275
|
// refuses clear in requires_action, so this is a defensive guard) — fail with
|
|
2819
3276
|
// an honest message instead of the cryptic SDK "missing schema version".
|
|
2820
3277
|
if (isClearedRunStateBlob(input.serializedRunState)) {
|
|
2821
3278
|
throw new Error(
|
|
2822
|
-
"Cannot resume an
|
|
3279
|
+
"Cannot resume an interrupted tool: the session context was cleared, so the awaiting run state no longer exists.",
|
|
2823
3280
|
);
|
|
2824
3281
|
}
|
|
2825
3282
|
const state = await RunState.fromString(agent, input.serializedRunState);
|
|
2826
3283
|
const interruptions = state.getInterruptions();
|
|
2827
|
-
const
|
|
3284
|
+
const interruptionId = input.kind === "human_input" ? input.toolCallId : input.approvalId;
|
|
3285
|
+
const target = interruptions.find((item: any) => approvalIdentifier(item) === interruptionId);
|
|
2828
3286
|
if (!target) {
|
|
2829
|
-
throw new Error(`
|
|
3287
|
+
throw new Error(`Interrupted tool not found in saved run state: ${interruptionId}`);
|
|
2830
3288
|
}
|
|
2831
|
-
if (input.
|
|
3289
|
+
if (input.kind === "human_input") {
|
|
3290
|
+
state.approve(target as any);
|
|
3291
|
+
} else if (input.decision === "approve") {
|
|
2832
3292
|
state.approve(target as any);
|
|
2833
3293
|
} else {
|
|
2834
3294
|
state.reject(target as any, input.message ? { message: input.message } : undefined);
|
|
@@ -2842,13 +3302,21 @@ export type RunAgentStreamOptions = {
|
|
|
2842
3302
|
sandboxClient?: unknown;
|
|
2843
3303
|
sandboxEnvironment?: Record<string, string>;
|
|
2844
3304
|
onRuntimeEvent?: (event: NormalizedRuntimeEvent) => Promise<void> | void;
|
|
2845
|
-
|
|
3305
|
+
contextCompactionSignal?: () => ProviderContextTokenSignal | null | undefined;
|
|
2846
3306
|
contextCompactionRequested?: () => boolean | Promise<boolean>;
|
|
2847
3307
|
// Host-managed git credential renewal registration. Called only after the
|
|
2848
3308
|
// initial token-file seed completed on a real provisioned box. The worker
|
|
2849
3309
|
// owns the multi-day timer and uses this pinned, un-proxied session to
|
|
2850
3310
|
// atomically replace token files; runtime never mints credentials itself.
|
|
2851
3311
|
onGitCredentialSessionReady?: (session: GitCredentialTokenWriterSession) => Promise<void> | void;
|
|
3312
|
+
// OpenGeni-minted Toolspace token renewal registration. Called only after the
|
|
3313
|
+
// initial token file reached the real sandbox session.
|
|
3314
|
+
onToolspaceTokenSessionReady?: (session: ToolspaceTokenWriterSession) => Promise<void> | void;
|
|
3315
|
+
// Host-owned run material is seeded off-manifest before setup and every
|
|
3316
|
+
// agent-created process sources the active immutable generation. The worker
|
|
3317
|
+
// owns resolution/renewal/fencing; runtime owns sandbox transport.
|
|
3318
|
+
runCredentialSessionId?: string;
|
|
3319
|
+
onRunCredentialSessionReady?: RunCredentialSessionReady;
|
|
2852
3320
|
// OWNERSHIP INVERSION (P1.2): an externally-owned, already-live sandbox
|
|
2853
3321
|
// session resolved by the per-turn resume-by-id path. When present,
|
|
2854
3322
|
// runAgentStream does NOT build (or resume, or discard) a client — it threads
|
|
@@ -2879,6 +3347,13 @@ export type RunAgentStreamOptions = {
|
|
|
2879
3347
|
// after establish, so runAgentStream must not run it eagerly here.
|
|
2880
3348
|
deferredSetup?: boolean;
|
|
2881
3349
|
};
|
|
3350
|
+
/**
|
|
3351
|
+
* The attempt's authoritative physical sandbox-operation fence. Platform
|
|
3352
|
+
* lifecycle commands use its command runner too, so Steer/Pause can interrupt
|
|
3353
|
+
* repository clone, rig setup, file materialization, and credential seeding
|
|
3354
|
+
* before the attempt-quiesced receipt opens queue admission.
|
|
3355
|
+
*/
|
|
3356
|
+
turnToolCancellationFence?: TurnToolCancellationFence;
|
|
2882
3357
|
// A per-turn model-input filter chained AFTER the provider-item-id strip.
|
|
2883
3358
|
// Used by the genesis-title injection to prepend a hidden, NON-PERSISTED
|
|
2884
3359
|
// directive: a callModelInputFilter mutates only `modelData.input` for each
|
|
@@ -2888,7 +3363,7 @@ export type RunAgentStreamOptions = {
|
|
|
2888
3363
|
};
|
|
2889
3364
|
|
|
2890
3365
|
export type ContextRobustnessFilterOptions = {
|
|
2891
|
-
|
|
3366
|
+
contextCompactionSignal?: () => ProviderContextTokenSignal | null | undefined;
|
|
2892
3367
|
contextCompactionRequested?: () => boolean | Promise<boolean>;
|
|
2893
3368
|
throwOnCompactionNeeded?: boolean;
|
|
2894
3369
|
};
|
|
@@ -2934,9 +3409,10 @@ function takeGenesisTitleInputFilter(agent: Agent<any, any>): CallModelInputFilt
|
|
|
2934
3409
|
// Generic substrate prompting for programmatic tool calling (toolspace). Same
|
|
2935
3410
|
// text for every host; gated per-turn by appendToolspaceInstructions on the
|
|
2936
3411
|
// presence of a minted toolspace token, so it only appears when the sandbox
|
|
2937
|
-
//
|
|
3412
|
+
// exposes $OPENGENI_TOOLSPACE_URL/_TOKEN_FILE. Stock images carry ogtool;
|
|
3413
|
+
// custom environments get only an exact deployment-pinned bootstrap hint.
|
|
2938
3414
|
export const TOOLSPACE_PROGRAMMATIC_DIRECTIVE =
|
|
2939
|
-
"Every tool on your MCP surface is also callable programmatically from the sandbox shell, so scripts can invoke tools without a model round trip per call.
|
|
3415
|
+
"Every tool on your MCP surface is also callable programmatically from the sandbox shell, so scripts can invoke tools without a model round trip per call. If `ogtool` is installed, run `ogtool list` to see the available tools and their input schemas (from tools/list), then `ogtool call <tool-name> '<json-args>'`. If it is absent and both npm and $OPENGENI_OGTOOL_PACKAGE_SPEC are available, run the exact deployment-pinned package with `npm exec --yes --package=\"$OPENGENI_OGTOOL_PACKAGE_SPEC\" -- ogtool ...`; never guess a version or install `latest`. Otherwise POST MCP JSON-RPC directly to $OPENGENI_TOOLSPACE_URL with the bearer token read from $OPENGENI_TOOLSPACE_TOKEN_FILE. Prefer programmatic calls for loops, polling, and bulk filtering: their results stay in the sandbox and do not consume your context window. Tools that require human approval must still be invoked normally — called programmatically they return a typed error.";
|
|
2940
3416
|
|
|
2941
3417
|
/**
|
|
2942
3418
|
* callModelInputFilter that removes provider-assigned item ids (rs_/msg_/fc_…)
|
|
@@ -2995,24 +3471,89 @@ export const elideSupersededViewImagesFilter: CallModelInputFilter = ({ modelDat
|
|
|
2995
3471
|
) as unknown as AgentInputItem[],
|
|
2996
3472
|
});
|
|
2997
3473
|
|
|
3474
|
+
/**
|
|
3475
|
+
* Canonical Codex-style tool-result bound at the final model-input seam. The
|
|
3476
|
+
* identical pure normalizer also runs before conversation rows are persisted,
|
|
3477
|
+
* so this is a live-turn defense rather than a request-only alternate history.
|
|
3478
|
+
*/
|
|
3479
|
+
export function boundModelToolOutputsFilterForSettings(settings: Settings): CallModelInputFilter {
|
|
3480
|
+
return ({ modelData }) => ({
|
|
3481
|
+
...modelData,
|
|
3482
|
+
input: boundModelToolOutputItems(
|
|
3483
|
+
modelData.input as unknown as Array<Record<string, unknown>>,
|
|
3484
|
+
settings.modelToolOutputTruncationTokens,
|
|
3485
|
+
) as unknown as AgentInputItem[],
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
function estimateAgentToolSchemaTokens(agent: Agent<any, any>): number {
|
|
3490
|
+
const localTools = Array.isArray((agent as { tools?: unknown }).tools)
|
|
3491
|
+
? ((agent as { tools: unknown[] }).tools ?? [])
|
|
3492
|
+
: [];
|
|
3493
|
+
const localDescriptors = localTools.map((candidate) => {
|
|
3494
|
+
if (!candidate || typeof candidate !== "object") return candidate;
|
|
3495
|
+
const tool = candidate as Record<string, unknown>;
|
|
3496
|
+
return {
|
|
3497
|
+
type: tool.type,
|
|
3498
|
+
name: tool.name,
|
|
3499
|
+
description: tool.description,
|
|
3500
|
+
parameters: tool.parameters,
|
|
3501
|
+
inputSchema: tool.inputSchema,
|
|
3502
|
+
strict: tool.strict,
|
|
3503
|
+
providerData: tool.providerData,
|
|
3504
|
+
};
|
|
3505
|
+
});
|
|
3506
|
+
const mcpServers = Array.isArray((agent as { mcpServers?: unknown }).mcpServers)
|
|
3507
|
+
? ((agent as { mcpServers: unknown[] }).mcpServers ?? [])
|
|
3508
|
+
: [];
|
|
3509
|
+
const mcpTokens = mcpServers.reduce<number>((total, server) => {
|
|
3510
|
+
const getter = (server as { modelToolSchemaTokens?: () => number } | null)
|
|
3511
|
+
?.modelToolSchemaTokens;
|
|
3512
|
+
return total + (typeof getter === "function" ? getter.call(server) : 0);
|
|
3513
|
+
}, 0);
|
|
3514
|
+
return estimateSerializedValueTokens(localDescriptors) + mcpTokens;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
2998
3517
|
export function contextRobustnessFilterForSettings(
|
|
2999
3518
|
settings: Settings,
|
|
3000
3519
|
options: ContextRobustnessFilterOptions = {},
|
|
3001
3520
|
): CallModelInputFilter {
|
|
3002
3521
|
const thresholdTokens = compactionThresholdTokens(settings);
|
|
3003
|
-
|
|
3522
|
+
let previousRequest: { revision: number; footprint: CompleteModelInputFootprint } | null = null;
|
|
3523
|
+
let requestRevision = 0;
|
|
3524
|
+
return async ({ modelData, agent }) => {
|
|
3004
3525
|
const input = modelData.input;
|
|
3005
3526
|
if (options.throwOnCompactionNeeded) {
|
|
3006
|
-
const reported = options.
|
|
3007
|
-
const
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
:
|
|
3527
|
+
const reported = options.contextCompactionSignal?.();
|
|
3528
|
+
const current: CompleteModelInputFootprint = {
|
|
3529
|
+
input: input as unknown as Array<Record<string, unknown>>,
|
|
3530
|
+
instructionsTokens: estimateSerializedValueTokens(modelData.instructions ?? ""),
|
|
3531
|
+
toolSchemaTokens: estimateAgentToolSchemaTokens(agent),
|
|
3532
|
+
};
|
|
3533
|
+
// Stream consumption can lag the SDK's background model loop. A provider
|
|
3534
|
+
// usage signal is safe only when its response revision belongs to the
|
|
3535
|
+
// immediately preceding request. Never attach a delayed response count to
|
|
3536
|
+
// a newer footprint: doing so can hide all model output produced between
|
|
3537
|
+
// them and recreate an under-counting compaction loop.
|
|
3538
|
+
const boundProvider =
|
|
3539
|
+
reported &&
|
|
3540
|
+
previousRequest &&
|
|
3541
|
+
reported.revision === previousRequest.revision &&
|
|
3542
|
+
hasModelGeneratedItem(current.input)
|
|
3543
|
+
? reported
|
|
3544
|
+
: null;
|
|
3545
|
+
const estimate = estimateCompleteModelInput({
|
|
3546
|
+
current,
|
|
3547
|
+
provider: boundProvider,
|
|
3548
|
+
providerRequestFootprint: boundProvider ? (previousRequest?.footprint ?? null) : null,
|
|
3549
|
+
});
|
|
3550
|
+
const signalTokens = estimate.tokens;
|
|
3551
|
+
previousRequest = { revision: ++requestRevision, footprint: current };
|
|
3011
3552
|
if (await options.contextCompactionRequested?.()) {
|
|
3012
3553
|
throw new CompactionNeededError({
|
|
3013
3554
|
signalTokens,
|
|
3014
3555
|
thresholdTokens,
|
|
3015
|
-
signalSource:
|
|
3556
|
+
signalSource: boundProvider ? "provider" : "estimate",
|
|
3016
3557
|
trigger: "operator",
|
|
3017
3558
|
});
|
|
3018
3559
|
}
|
|
@@ -3020,7 +3561,7 @@ export function contextRobustnessFilterForSettings(
|
|
|
3020
3561
|
throw new CompactionNeededError({
|
|
3021
3562
|
signalTokens,
|
|
3022
3563
|
thresholdTokens,
|
|
3023
|
-
signalSource:
|
|
3564
|
+
signalSource: boundProvider ? "provider" : "estimate",
|
|
3024
3565
|
});
|
|
3025
3566
|
}
|
|
3026
3567
|
}
|
|
@@ -3057,6 +3598,7 @@ export function callModelInputFilterForSettings(
|
|
|
3057
3598
|
const filters: CallModelInputFilter[] = [
|
|
3058
3599
|
normalizeComputerCallsFilter,
|
|
3059
3600
|
elideSupersededViewImagesFilter,
|
|
3601
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
3060
3602
|
];
|
|
3061
3603
|
if (settings.openaiProviderItemIds === "strip") {
|
|
3062
3604
|
filters.push(stripProviderItemIdsFilter);
|
|
@@ -3074,7 +3616,11 @@ export async function runAgentStream(
|
|
|
3074
3616
|
const prepared: PreparedAgentInput =
|
|
3075
3617
|
typeof input === "string" || input instanceof RunState ? { input } : input;
|
|
3076
3618
|
const environment = overrides.sandboxEnvironment ?? collectSandboxEnvironment(settings);
|
|
3619
|
+
const toolspaceTokenFile = toolspaceTokenFileForAgent(agent, environment);
|
|
3077
3620
|
const genesisTitleInputFilter = takeGenesisTitleInputFilter(agent);
|
|
3621
|
+
if (overrides.onRunCredentialSessionReady && !overrides.runCredentialSessionId) {
|
|
3622
|
+
throw new Error("runCredentialSessionId is required when run credential setup is enabled");
|
|
3623
|
+
}
|
|
3078
3624
|
|
|
3079
3625
|
// OWNED PATH (P1.2 ownership inversion): the per-turn resume path injected a
|
|
3080
3626
|
// live, externally-owned box. We thread the live `session` straight into
|
|
@@ -3093,11 +3639,24 @@ export async function runAgentStream(
|
|
|
3093
3639
|
// whose per-op pointer re-read could land these execs on a machine swapped in
|
|
3094
3640
|
// mid-turn.
|
|
3095
3641
|
const setupSession = (overrides.ownedSandbox.setupSession ?? session) as SandboxSessionLike;
|
|
3642
|
+
const credentialAgentSession = overrides.runCredentialSessionId
|
|
3643
|
+
? withRunCredentialsSession(session as SandboxSessionLike, overrides.runCredentialSessionId)
|
|
3644
|
+
: (session as SandboxSessionLike);
|
|
3645
|
+
const agentSession = toolspaceTokenFile
|
|
3646
|
+
? withToolspaceTokenSession(credentialAgentSession, toolspaceTokenFile)
|
|
3647
|
+
: credentialAgentSession;
|
|
3648
|
+
const credentialSetupSession = overrides.runCredentialSessionId
|
|
3649
|
+
? withRunCredentialsSession(setupSession, overrides.runCredentialSessionId)
|
|
3650
|
+
: setupSession;
|
|
3651
|
+
const decoratedSetupSession = toolspaceTokenFile
|
|
3652
|
+
? withToolspaceTokenSession(credentialSetupSession, toolspaceTokenFile)
|
|
3653
|
+
: credentialSetupSession;
|
|
3096
3654
|
// Platform setup (manifest-env pin + beforeAgentStart hooks + file downloads)
|
|
3097
3655
|
// against the UN-proxied established box — the ONE-TRUTH helper shared with the
|
|
3098
3656
|
// lazy provisioner. Eager path: runs here, before the run starts (unchanged).
|
|
3099
3657
|
if (!overrides.ownedSandbox.deferredSetup) {
|
|
3100
|
-
await
|
|
3658
|
+
await overrides.onRunCredentialSessionReady?.(session as SandboxSessionLike);
|
|
3659
|
+
await runOwnedSandboxSetup(agent, session as SandboxSessionLike, decoratedSetupSession, {
|
|
3101
3660
|
settings,
|
|
3102
3661
|
environment,
|
|
3103
3662
|
preparedInput: prepared,
|
|
@@ -3105,7 +3664,17 @@ export async function runAgentStream(
|
|
|
3105
3664
|
? { fileDownloadsMaterialized: true }
|
|
3106
3665
|
: {}),
|
|
3107
3666
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3667
|
+
...(overrides.turnToolCancellationFence
|
|
3668
|
+
? {
|
|
3669
|
+
commandRunner: overrides.turnToolCancellationFence.runSandboxCommand.bind(
|
|
3670
|
+
overrides.turnToolCancellationFence,
|
|
3671
|
+
),
|
|
3672
|
+
}
|
|
3673
|
+
: {}),
|
|
3108
3674
|
});
|
|
3675
|
+
if (toolspaceTokenSeedForAgent(agent)) {
|
|
3676
|
+
await overrides.onToolspaceTokenSessionReady?.(agentSession);
|
|
3677
|
+
}
|
|
3109
3678
|
await overrides.onGitCredentialSessionReady?.(setupSession);
|
|
3110
3679
|
}
|
|
3111
3680
|
const runAs = sandboxRunAs(settings);
|
|
@@ -3115,11 +3684,19 @@ export async function runAgentStream(
|
|
|
3115
3684
|
? withSandboxFileDownloads(ownedClient as SandboxClient, fileDownloads, {
|
|
3116
3685
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3117
3686
|
...(runAs ? { runAs } : {}),
|
|
3687
|
+
...(overrides.turnToolCancellationFence
|
|
3688
|
+
? {
|
|
3689
|
+
commandRunner: overrides.turnToolCancellationFence.runSandboxCommand.bind(
|
|
3690
|
+
overrides.turnToolCancellationFence,
|
|
3691
|
+
),
|
|
3692
|
+
}
|
|
3693
|
+
: {}),
|
|
3118
3694
|
})
|
|
3119
3695
|
: (ownedClient as SandboxClient);
|
|
3120
3696
|
// TOKEN-BROKER (B1): the per-turn git token seed, forwarded OFF-MANIFEST so the
|
|
3121
3697
|
// repository-clone hook seeds it to the box's token file before the clone.
|
|
3122
3698
|
const ownedGitTokenSeeds = gitTokenSeedsForAgent(agent);
|
|
3699
|
+
const ownedGitCredentialBindings = gitCredentialBindingsForAgent(agent);
|
|
3123
3700
|
const ownedToolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
|
|
3124
3701
|
const ownedRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3125
3702
|
const ownedHooks = [
|
|
@@ -3132,6 +3709,9 @@ export async function runAgentStream(
|
|
|
3132
3709
|
rigCredentialHooksForAgent(agent),
|
|
3133
3710
|
),
|
|
3134
3711
|
...sandboxToolspaceTokenHooksForAgent(agent),
|
|
3712
|
+
...toolspaceTokenSessionRegistrationHooks(
|
|
3713
|
+
ownedToolspaceTokenSeed ? overrides.onToolspaceTokenSessionReady : undefined,
|
|
3714
|
+
),
|
|
3135
3715
|
...sandboxRepositoryCloneHooksForAgent(agent),
|
|
3136
3716
|
...gitCredentialSessionRegistrationHooks(overrides.onGitCredentialSessionReady),
|
|
3137
3717
|
];
|
|
@@ -3140,31 +3720,49 @@ export async function runAgentStream(
|
|
|
3140
3720
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3141
3721
|
...(runAs ? { runAs } : {}),
|
|
3142
3722
|
...(ownedGitTokenSeeds ? { gitTokenSeeds: ownedGitTokenSeeds } : {}),
|
|
3723
|
+
...(ownedGitCredentialBindings ? { gitCredentialBindings: ownedGitCredentialBindings } : {}),
|
|
3143
3724
|
...(ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {}),
|
|
3725
|
+
...(toolspaceTokenFile ? { toolspaceTokenFile } : {}),
|
|
3144
3726
|
...(ownedRigSetup ? { rigSetup: ownedRigSetup } : {}),
|
|
3145
3727
|
};
|
|
3146
|
-
// Keep
|
|
3147
|
-
//
|
|
3148
|
-
|
|
3728
|
+
// Keep both credential seeding and lifecycle decoration as a safety net for
|
|
3729
|
+
// any session the SDK does create/resume during this run. They are inert for
|
|
3730
|
+
// the provided session, which remains the normal ownership-inverted path.
|
|
3731
|
+
const credentialResourceClient = overrides.runCredentialSessionId
|
|
3732
|
+
? withRunCredentialsClient(
|
|
3733
|
+
resourceClient,
|
|
3734
|
+
overrides.runCredentialSessionId,
|
|
3735
|
+
overrides.onRunCredentialSessionReady,
|
|
3736
|
+
)
|
|
3737
|
+
: resourceClient;
|
|
3738
|
+
const toolspaceResourceClient = toolspaceTokenFile
|
|
3739
|
+
? withToolspaceTokenClient(credentialResourceClient, toolspaceTokenFile)
|
|
3740
|
+
: credentialResourceClient;
|
|
3741
|
+
const decoratedClient = withSandboxLifecycleHooks(
|
|
3742
|
+
toolspaceResourceClient,
|
|
3743
|
+
ownedHooks,
|
|
3744
|
+
ownedHookContext,
|
|
3745
|
+
);
|
|
3149
3746
|
const ownedFilter = composeCallModelInputFilters(
|
|
3150
3747
|
[
|
|
3151
|
-
callModelInputFilterForSettings(settings,
|
|
3748
|
+
callModelInputFilterForSettings(settings),
|
|
3749
|
+
genesisTitleInputFilter,
|
|
3750
|
+
overrides.callModelInputFilter,
|
|
3751
|
+
// A caller filter may synthesize model input. Re-apply the idempotent
|
|
3752
|
+
// canonical bound at the literal final seam before accounting/provider
|
|
3753
|
+
// serialization so no extension can bypass the policy.
|
|
3754
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
3755
|
+
contextRobustnessFilterForSettings(settings, {
|
|
3152
3756
|
throwOnCompactionNeeded: Boolean(
|
|
3153
|
-
overrides.
|
|
3757
|
+
overrides.contextCompactionSignal || overrides.contextCompactionRequested,
|
|
3154
3758
|
),
|
|
3155
|
-
...(overrides.
|
|
3156
|
-
? {
|
|
3157
|
-
contextCompactionSignalTokens: overrides.contextCompactionSignalTokens,
|
|
3158
|
-
}
|
|
3759
|
+
...(overrides.contextCompactionSignal
|
|
3760
|
+
? { contextCompactionSignal: overrides.contextCompactionSignal }
|
|
3159
3761
|
: {}),
|
|
3160
3762
|
...(overrides.contextCompactionRequested
|
|
3161
|
-
? {
|
|
3162
|
-
contextCompactionRequested: overrides.contextCompactionRequested,
|
|
3163
|
-
}
|
|
3763
|
+
? { contextCompactionRequested: overrides.contextCompactionRequested }
|
|
3164
3764
|
: {}),
|
|
3165
3765
|
}),
|
|
3166
|
-
genesisTitleInputFilter,
|
|
3167
|
-
overrides.callModelInputFilter,
|
|
3168
3766
|
].filter((f): f is CallModelInputFilter => Boolean(f)),
|
|
3169
3767
|
);
|
|
3170
3768
|
const ownedRunOptions: Parameters<typeof run>[2] = {
|
|
@@ -3175,7 +3773,7 @@ export async function runAgentStream(
|
|
|
3175
3773
|
};
|
|
3176
3774
|
ownedRunOptions.sandbox = {
|
|
3177
3775
|
client: decoratedClient,
|
|
3178
|
-
session,
|
|
3776
|
+
session: agentSession,
|
|
3179
3777
|
...(sessionState ? { sessionState } : {}),
|
|
3180
3778
|
} as SandboxRunConfig;
|
|
3181
3779
|
return await runScopedRunner(settings).run(agent, prepared.input, ownedRunOptions);
|
|
@@ -3200,14 +3798,27 @@ export async function runAgentStream(
|
|
|
3200
3798
|
...(runAs ? { runAs } : {}),
|
|
3201
3799
|
})
|
|
3202
3800
|
: refreshedClient;
|
|
3801
|
+
const credentialClient =
|
|
3802
|
+
resourceClient && overrides.runCredentialSessionId
|
|
3803
|
+
? withRunCredentialsClient(
|
|
3804
|
+
resourceClient,
|
|
3805
|
+
overrides.runCredentialSessionId,
|
|
3806
|
+
overrides.onRunCredentialSessionReady,
|
|
3807
|
+
)
|
|
3808
|
+
: resourceClient;
|
|
3809
|
+
const toolspaceClient =
|
|
3810
|
+
credentialClient && toolspaceTokenFile
|
|
3811
|
+
? withToolspaceTokenClient(credentialClient, toolspaceTokenFile)
|
|
3812
|
+
: credentialClient;
|
|
3203
3813
|
// TOKEN-BROKER (B1): the per-turn git token seed, forwarded OFF-MANIFEST so the
|
|
3204
3814
|
// repository-clone hook seeds it to the box's token file before the clone.
|
|
3205
3815
|
const gitTokenSeeds = gitTokenSeedsForAgent(agent);
|
|
3816
|
+
const gitCredentialBindings = gitCredentialBindingsForAgent(agent);
|
|
3206
3817
|
const toolspaceTokenSeed = toolspaceTokenSeedForAgent(agent);
|
|
3207
3818
|
const legacyRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3208
|
-
const client =
|
|
3819
|
+
const client = toolspaceClient
|
|
3209
3820
|
? withSandboxLifecycleHooks(
|
|
3210
|
-
|
|
3821
|
+
toolspaceClient,
|
|
3211
3822
|
[
|
|
3212
3823
|
// M3: same rig-setup-first ordering + credential-hook union as the owned
|
|
3213
3824
|
// path (this legacy create/resume decoration path is byte-for-byte today
|
|
@@ -3218,6 +3829,9 @@ export async function runAgentStream(
|
|
|
3218
3829
|
rigCredentialHooksForAgent(agent),
|
|
3219
3830
|
),
|
|
3220
3831
|
...sandboxToolspaceTokenHooksForAgent(agent),
|
|
3832
|
+
...toolspaceTokenSessionRegistrationHooks(
|
|
3833
|
+
toolspaceTokenSeed ? overrides.onToolspaceTokenSessionReady : undefined,
|
|
3834
|
+
),
|
|
3221
3835
|
...sandboxRepositoryCloneHooksForAgent(agent),
|
|
3222
3836
|
...gitCredentialSessionRegistrationHooks(overrides.onGitCredentialSessionReady),
|
|
3223
3837
|
],
|
|
@@ -3226,7 +3840,9 @@ export async function runAgentStream(
|
|
|
3226
3840
|
...(overrides.onRuntimeEvent ? { onRuntimeEvent: overrides.onRuntimeEvent } : {}),
|
|
3227
3841
|
...(runAs ? { runAs } : {}),
|
|
3228
3842
|
...(gitTokenSeeds ? { gitTokenSeeds } : {}),
|
|
3843
|
+
...(gitCredentialBindings ? { gitCredentialBindings } : {}),
|
|
3229
3844
|
...(toolspaceTokenSeed ? { toolspaceTokenSeed } : {}),
|
|
3845
|
+
...(toolspaceTokenFile ? { toolspaceTokenFile } : {}),
|
|
3230
3846
|
...(legacyRigSetup ? { rigSetup: legacyRigSetup } : {}),
|
|
3231
3847
|
},
|
|
3232
3848
|
)
|
|
@@ -3239,21 +3855,21 @@ export async function runAgentStream(
|
|
|
3239
3855
|
// OpenGeni's durable conversation truth is still reconciled explicitly below.
|
|
3240
3856
|
const callModelInputFilter = composeCallModelInputFilters(
|
|
3241
3857
|
[
|
|
3242
|
-
callModelInputFilterForSettings(settings,
|
|
3858
|
+
callModelInputFilterForSettings(settings),
|
|
3859
|
+
genesisTitleInputFilter,
|
|
3860
|
+
overrides.callModelInputFilter,
|
|
3861
|
+
boundModelToolOutputsFilterForSettings(settings),
|
|
3862
|
+
contextRobustnessFilterForSettings(settings, {
|
|
3243
3863
|
throwOnCompactionNeeded: Boolean(
|
|
3244
|
-
overrides.
|
|
3864
|
+
overrides.contextCompactionSignal || overrides.contextCompactionRequested,
|
|
3245
3865
|
),
|
|
3246
|
-
...(overrides.
|
|
3247
|
-
? {
|
|
3248
|
-
contextCompactionSignalTokens: overrides.contextCompactionSignalTokens,
|
|
3249
|
-
}
|
|
3866
|
+
...(overrides.contextCompactionSignal
|
|
3867
|
+
? { contextCompactionSignal: overrides.contextCompactionSignal }
|
|
3250
3868
|
: {}),
|
|
3251
3869
|
...(overrides.contextCompactionRequested
|
|
3252
3870
|
? { contextCompactionRequested: overrides.contextCompactionRequested }
|
|
3253
3871
|
: {}),
|
|
3254
3872
|
}),
|
|
3255
|
-
genesisTitleInputFilter,
|
|
3256
|
-
overrides.callModelInputFilter,
|
|
3257
3873
|
].filter((f): f is CallModelInputFilter => Boolean(f)),
|
|
3258
3874
|
);
|
|
3259
3875
|
const runOptions: Parameters<typeof run>[2] = {
|
|
@@ -3656,6 +4272,9 @@ export async function runOwnedSandboxSetup(
|
|
|
3656
4272
|
onRuntimeEvent?: SandboxLifecycleHookContext["onRuntimeEvent"];
|
|
3657
4273
|
gitTokenSeedsOverride?: GitTokenSeeds;
|
|
3658
4274
|
gitTokenSeedOverride?: string;
|
|
4275
|
+
gitCredentialBindingsOverride?: GitCredentialBindingSeed[];
|
|
4276
|
+
toolspaceTokenSeedOverride?: string;
|
|
4277
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
3659
4278
|
},
|
|
3660
4279
|
): Promise<void> {
|
|
3661
4280
|
const { settings, environment } = opts;
|
|
@@ -3677,7 +4296,11 @@ export async function runOwnedSandboxSetup(
|
|
|
3677
4296
|
...(opts.gitTokenSeedOverride ? { github: opts.gitTokenSeedOverride } : {}),
|
|
3678
4297
|
...(opts.gitTokenSeedsOverride ?? {}),
|
|
3679
4298
|
} satisfies GitTokenSeeds;
|
|
3680
|
-
const
|
|
4299
|
+
const ownedGitCredentialBindings =
|
|
4300
|
+
opts.gitCredentialBindingsOverride ?? gitCredentialBindingsForAgent(agent);
|
|
4301
|
+
const ownedToolspaceTokenSeed =
|
|
4302
|
+
opts.toolspaceTokenSeedOverride ?? toolspaceTokenSeedForAgent(agent);
|
|
4303
|
+
const ownedToolspaceTokenFile = toolspaceTokenFileForAgent(agent, environment);
|
|
3681
4304
|
const ownedRigSetup = rigSetupDescriptorForAgent(agent);
|
|
3682
4305
|
const ownedHooks = [
|
|
3683
4306
|
// M3: rig setup runs FIRST so any tooling it installs is present for the
|
|
@@ -3699,8 +4322,11 @@ export async function runOwnedSandboxSetup(
|
|
|
3699
4322
|
...(opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {}),
|
|
3700
4323
|
...(runAs ? { runAs } : {}),
|
|
3701
4324
|
...(Object.keys(ownedGitTokenSeeds).length > 0 ? { gitTokenSeeds: ownedGitTokenSeeds } : {}),
|
|
4325
|
+
...(ownedGitCredentialBindings ? { gitCredentialBindings: ownedGitCredentialBindings } : {}),
|
|
3702
4326
|
...(ownedToolspaceTokenSeed ? { toolspaceTokenSeed: ownedToolspaceTokenSeed } : {}),
|
|
4327
|
+
...(ownedToolspaceTokenFile ? { toolspaceTokenFile: ownedToolspaceTokenFile } : {}),
|
|
3703
4328
|
...(ownedRigSetup ? { rigSetup: ownedRigSetup } : {}),
|
|
4329
|
+
...(opts.commandRunner ? { commandRunner: opts.commandRunner } : {}),
|
|
3704
4330
|
};
|
|
3705
4331
|
// OWNED-PATH HOOKS: run the beforeAgentStart hooks directly against the provided
|
|
3706
4332
|
// box, once per turn, BEFORE the run starts (repository-clone hook seeds the B1
|
|
@@ -3720,6 +4346,7 @@ export async function runOwnedSandboxSetup(
|
|
|
3720
4346
|
const materialized = await materializeSandboxFileDownloads(setupSession, fileDownloads, {
|
|
3721
4347
|
...(opts.onRuntimeEvent ? { onRuntimeEvent: opts.onRuntimeEvent } : {}),
|
|
3722
4348
|
...(runAs ? { runAs } : {}),
|
|
4349
|
+
...(opts.commandRunner ? { commandRunner: opts.commandRunner } : {}),
|
|
3723
4350
|
});
|
|
3724
4351
|
if (opts.preparedInput) {
|
|
3725
4352
|
appendSandboxFileDownloadFailureNote(opts.preparedInput, materialized.failures);
|
|
@@ -3753,7 +4380,7 @@ function mergePathGrants(
|
|
|
3753
4380
|
export function withSandboxFileDownloads(
|
|
3754
4381
|
client: SandboxClient,
|
|
3755
4382
|
downloads: SandboxFileDownload[],
|
|
3756
|
-
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs"> = {},
|
|
4383
|
+
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs" | "commandRunner"> = {},
|
|
3757
4384
|
): SandboxClient {
|
|
3758
4385
|
const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
|
|
3759
4386
|
if (normalizedDownloads.length === 0) {
|
|
@@ -3819,7 +4446,7 @@ export function withSandboxFileDownloads(
|
|
|
3819
4446
|
export async function materializeSandboxFileDownloads(
|
|
3820
4447
|
session: SandboxSessionLike,
|
|
3821
4448
|
downloads: SandboxFileDownload[],
|
|
3822
|
-
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs"> = {},
|
|
4449
|
+
context: Pick<SandboxLifecycleHookContext, "onRuntimeEvent" | "runAs" | "commandRunner"> = {},
|
|
3823
4450
|
): Promise<SandboxFileDownloadMaterializationResult> {
|
|
3824
4451
|
const normalizedDownloads = normalizeSandboxFileDownloads(downloads);
|
|
3825
4452
|
if (normalizedDownloads.length === 0) {
|
|
@@ -3857,27 +4484,24 @@ export async function materializeSandboxFileDownloads(
|
|
|
3857
4484
|
}
|
|
3858
4485
|
let result: unknown;
|
|
3859
4486
|
try {
|
|
3860
|
-
result =
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
3872
|
-
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
3873
|
-
maxOutputTokens: 20_000,
|
|
3874
|
-
});
|
|
4487
|
+
result = await runSandboxLifecycleCommand(
|
|
4488
|
+
session,
|
|
4489
|
+
{
|
|
4490
|
+
cmd: sandboxFileDownloadCommand(download, targetPath),
|
|
4491
|
+
workdir: "/workspace",
|
|
4492
|
+
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
4493
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
4494
|
+
maxOutputTokens: 20_000,
|
|
4495
|
+
},
|
|
4496
|
+
context.commandRunner,
|
|
4497
|
+
);
|
|
3875
4498
|
assertSandboxCommandSucceeded(result, `Sandbox file resource download ${download.fileId}`);
|
|
3876
4499
|
await context.onRuntimeEvent?.({
|
|
3877
4500
|
type: "sandbox.operation.completed",
|
|
3878
4501
|
payload: { name: "file-resource-download", ...payload },
|
|
3879
4502
|
});
|
|
3880
4503
|
} catch (error) {
|
|
4504
|
+
if (error instanceof TurnSandboxCommandCancelledError) throw error;
|
|
3881
4505
|
const failure = sandboxFileDownloadFailure(download, targetPath, error, result);
|
|
3882
4506
|
failures.push(failure);
|
|
3883
4507
|
await context.onRuntimeEvent?.({
|
|
@@ -3954,86 +4578,87 @@ function ensureManifest(
|
|
|
3954
4578
|
});
|
|
3955
4579
|
}
|
|
3956
4580
|
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
4581
|
+
function base64DecodedByteLength(value: string): number {
|
|
4582
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
4583
|
+
return Math.max(0, Math.floor((value.length * 3) / 4) - padding);
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
/** Determine image byte length without allocating another binary/base64 copy. */
|
|
4587
|
+
function imageDataByteLength(data: unknown): number | null {
|
|
4588
|
+
if (ArrayBuffer.isView(data)) return data.byteLength;
|
|
4589
|
+
if (data instanceof ArrayBuffer) return data.byteLength;
|
|
3965
4590
|
if (Array.isArray(data)) {
|
|
3966
|
-
return data.every((
|
|
4591
|
+
return data.every((value) => typeof value === "number") ? data.length : null;
|
|
3967
4592
|
}
|
|
3968
|
-
if (data
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
}
|
|
4593
|
+
if (!data || typeof data !== "object") return null;
|
|
4594
|
+
const record = data as Record<string, unknown>;
|
|
4595
|
+
if (record.type === "Buffer" && Array.isArray(record.data)) {
|
|
4596
|
+
return record.data.every((value) => typeof value === "number") ? record.data.length : null;
|
|
3973
4597
|
}
|
|
3974
|
-
|
|
4598
|
+
const keys = Object.keys(record);
|
|
4599
|
+
return keys.length > 0 &&
|
|
4600
|
+
keys.every((key) => /^\d+$/.test(key) && typeof record[key] === "number")
|
|
4601
|
+
? keys.length
|
|
4602
|
+
: null;
|
|
3975
4603
|
}
|
|
3976
4604
|
|
|
3977
|
-
/**
|
|
3978
|
-
*
|
|
3979
|
-
*
|
|
3980
|
-
*
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
if (
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
url
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4605
|
+
/**
|
|
4606
|
+
* Convert one image-shaped tool result into a content-free audit fact. This is
|
|
4607
|
+
* intentionally different from model history: the model keeps its structured
|
|
4608
|
+
* image item, while `session_events` never becomes an implicit image blob store.
|
|
4609
|
+
*/
|
|
4610
|
+
function toolOutputMediaPreview(value: unknown): SessionEventMediaPreview | null {
|
|
4611
|
+
if (typeof value === "string") {
|
|
4612
|
+
return sessionEventMediaPreviewFromDataUrl(value);
|
|
4613
|
+
}
|
|
4614
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4615
|
+
const record = value as Record<string, unknown>;
|
|
4616
|
+
if (record.type === "input_image") {
|
|
4617
|
+
const source = record.image ?? record.image_url ?? record.imageUrl;
|
|
4618
|
+
const url =
|
|
4619
|
+
typeof source === "string"
|
|
4620
|
+
? source
|
|
4621
|
+
: source && typeof source === "object"
|
|
4622
|
+
? (source as Record<string, unknown>).url
|
|
4623
|
+
: null;
|
|
4624
|
+
if (typeof url !== "string" || url.length === 0) return null;
|
|
4625
|
+
return sessionEventMediaPreviewFromDataUrl(url) ?? sessionEventMediaPreview("image/*", null);
|
|
4626
|
+
}
|
|
4627
|
+
if (record.type !== "image" || !record.image || typeof record.image !== "object") return null;
|
|
4628
|
+
const image = record.image as Record<string, unknown>;
|
|
4001
4629
|
const mediaType =
|
|
4002
4630
|
typeof image.mediaType === "string" && image.mediaType.length > 0
|
|
4003
4631
|
? image.mediaType
|
|
4004
4632
|
: "image/png";
|
|
4633
|
+
if (typeof image.url === "string" && image.url.length > 0) {
|
|
4634
|
+
return (
|
|
4635
|
+
sessionEventMediaPreviewFromDataUrl(image.url) ?? sessionEventMediaPreview(mediaType, null)
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4005
4638
|
if (typeof image.data === "string") {
|
|
4006
|
-
return
|
|
4639
|
+
return (
|
|
4640
|
+
sessionEventMediaPreviewFromDataUrl(image.data) ??
|
|
4641
|
+
sessionEventMediaPreview(mediaType, base64DecodedByteLength(image.data))
|
|
4642
|
+
);
|
|
4007
4643
|
}
|
|
4008
|
-
const
|
|
4009
|
-
return
|
|
4644
|
+
const byteLength = imageDataByteLength(image.data);
|
|
4645
|
+
return byteLength === null ? null : sessionEventMediaPreview(mediaType, byteLength);
|
|
4010
4646
|
}
|
|
4011
4647
|
|
|
4012
4648
|
/**
|
|
4013
|
-
*
|
|
4014
|
-
*
|
|
4015
|
-
*
|
|
4016
|
-
*
|
|
4017
|
-
* per screenshot in session_events — ~10x the base64 form). This mirrors the desktop
|
|
4018
|
-
* screenshot to the SAME compact `data:<mediaType>;base64,…` STRING the HOSTED
|
|
4019
|
-
* `computer_call` event already carries (agents-core sets its output to that data-URL),
|
|
4020
|
-
* so both computer-use transports emit one representation. The full data-URL is kept
|
|
4021
|
-
* (not truncated) because the web timeline RENDERS the screenshot from this event
|
|
4022
|
-
* payload — packages/react/src/timeline/tool-renderers.tsx ComputerCallRenderer
|
|
4023
|
-
* (`out.startsWith("data:image")` → <ScreenshotFigure src={out}/>) and ViewImageRenderer.
|
|
4024
|
-
* Non-image outputs (text strings, MCP `{isError,content}` objects, hosted computer_call
|
|
4025
|
-
* data-URL strings) pass through unchanged.
|
|
4649
|
+
* Normalize a tool-call output for the lossy `agent.toolCall.output` audit event.
|
|
4650
|
+
* Inline image bytes/data URLs become a compact `media_preview` with exact byte
|
|
4651
|
+
* length where knowable and `fullOutputAvailable:false`. The model-facing output
|
|
4652
|
+
* is not changed here, and mixed arrays retain their non-image text/error facts.
|
|
4026
4653
|
*/
|
|
4027
4654
|
export function normalizeToolOutputForEvent(output: unknown): unknown {
|
|
4028
|
-
const single =
|
|
4655
|
+
const single = toolOutputMediaPreview(output);
|
|
4029
4656
|
if (single !== null) {
|
|
4030
4657
|
return single;
|
|
4031
4658
|
}
|
|
4032
4659
|
if (Array.isArray(output)) {
|
|
4033
|
-
const normalized = output.map((el) =>
|
|
4034
|
-
|
|
4035
|
-
// image renderers expect; a mixed/multi array keeps its (now-compact) shape.
|
|
4036
|
-
if (normalized.length === 1 && typeof normalized[0] === "string") {
|
|
4660
|
+
const normalized = output.map((el) => toolOutputMediaPreview(el) ?? el);
|
|
4661
|
+
if (normalized.length === 1 && normalized[0]?.type === "media_preview") {
|
|
4037
4662
|
return normalized[0];
|
|
4038
4663
|
}
|
|
4039
4664
|
return normalized;
|
|
@@ -4090,8 +4715,8 @@ export function normalizeSdkEvent(event: RunStreamEvent): NormalizedRuntimeEvent
|
|
|
4090
4715
|
type: "agent.toolCall.output",
|
|
4091
4716
|
payload: {
|
|
4092
4717
|
id: item.rawItem?.callId ?? item.id ?? null,
|
|
4093
|
-
//
|
|
4094
|
-
//
|
|
4718
|
+
// Inline media becomes a content-free audit fact. Model history keeps
|
|
4719
|
+
// the provider's real structured image output on its separate path.
|
|
4095
4720
|
output: normalizeToolOutputForEvent(item.output),
|
|
4096
4721
|
},
|
|
4097
4722
|
});
|
|
@@ -4245,17 +4870,45 @@ function outputTokenDetailsProp(
|
|
|
4245
4870
|
}
|
|
4246
4871
|
|
|
4247
4872
|
export function serializeApprovals(interruptions: unknown[]): unknown[] {
|
|
4248
|
-
return interruptions
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4873
|
+
return interruptions
|
|
4874
|
+
.filter((item) => interruptionToolName(item) !== HUMAN_INPUT_TOOL_NAME)
|
|
4875
|
+
.map((item: any) => {
|
|
4876
|
+
if (typeof item?.toJSON === "function") {
|
|
4877
|
+
return item.toJSON();
|
|
4878
|
+
}
|
|
4879
|
+
return {
|
|
4880
|
+
id: approvalIdentifier(item) ?? "approval",
|
|
4881
|
+
name: item?.name ?? item?.rawItem?.name ?? "tool",
|
|
4882
|
+
arguments: item?.arguments ?? item?.rawItem?.arguments ?? null,
|
|
4883
|
+
raw: item,
|
|
4884
|
+
};
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
export function serializeHumanInputRequests(
|
|
4889
|
+
interruptions: unknown[],
|
|
4890
|
+
): SerializedHumanInputInterruption[] {
|
|
4891
|
+
return interruptions
|
|
4892
|
+
.filter((item) => interruptionToolName(item) === HUMAN_INPUT_TOOL_NAME)
|
|
4893
|
+
.map((item: any) => {
|
|
4894
|
+
const rawArguments = item?.arguments ?? item?.rawItem?.arguments;
|
|
4895
|
+
let parsedArguments: unknown = rawArguments;
|
|
4896
|
+
if (typeof rawArguments === "string") {
|
|
4897
|
+
try {
|
|
4898
|
+
parsedArguments = JSON.parse(rawArguments);
|
|
4899
|
+
} catch {
|
|
4900
|
+
throw new Error("Human-input interruption contains invalid JSON arguments");
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
const toolCallId = approvalIdentifier(item);
|
|
4904
|
+
if (!toolCallId) {
|
|
4905
|
+
throw new Error("Human-input interruption is missing a stable tool-call identity");
|
|
4906
|
+
}
|
|
4907
|
+
return {
|
|
4908
|
+
toolCallId,
|
|
4909
|
+
input: RequestHumanInputToolInput.parse(parsedArguments),
|
|
4910
|
+
};
|
|
4911
|
+
});
|
|
4259
4912
|
}
|
|
4260
4913
|
|
|
4261
4914
|
export function buildManifest(
|
|
@@ -4264,6 +4917,7 @@ export function buildManifest(
|
|
|
4264
4917
|
environment = collectSandboxEnvironment(settings),
|
|
4265
4918
|
fileResourceDownloads: SandboxFileDownload[] = [],
|
|
4266
4919
|
): Manifest {
|
|
4920
|
+
assertUniqueResourceMountPaths(resources);
|
|
4267
4921
|
const entries: Record<string, any> = {};
|
|
4268
4922
|
const downloadsByFileId = new Map(
|
|
4269
4923
|
normalizeSandboxFileDownloads(fileResourceDownloads).map((download) => [
|
|
@@ -4274,9 +4928,9 @@ export function buildManifest(
|
|
|
4274
4928
|
for (const resource of resources) {
|
|
4275
4929
|
if (resource.kind === "repository") {
|
|
4276
4930
|
const url = new URL(resource.uri);
|
|
4277
|
-
const host = url.
|
|
4931
|
+
const host = url.host.toLowerCase();
|
|
4278
4932
|
const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
4279
|
-
const mountPath =
|
|
4933
|
+
const mountPath = resourceMountPath(resource);
|
|
4280
4934
|
if (repositoryUsesSandboxClone(settings, resource)) {
|
|
4281
4935
|
entries[mountPath] = dir();
|
|
4282
4936
|
continue;
|
|
@@ -4285,12 +4939,12 @@ export function buildManifest(
|
|
|
4285
4939
|
host,
|
|
4286
4940
|
repo,
|
|
4287
4941
|
ref: resource.ref,
|
|
4288
|
-
...(resource.subpath ? { subpath:
|
|
4942
|
+
...(resource.subpath ? { subpath: normalizeRepositorySubpath(resource.subpath) } : {}),
|
|
4289
4943
|
});
|
|
4290
4944
|
continue;
|
|
4291
4945
|
}
|
|
4292
4946
|
if (resource.kind === "file") {
|
|
4293
|
-
const mountPath =
|
|
4947
|
+
const mountPath = resourceMountPath(resource);
|
|
4294
4948
|
const download = downloadsByFileId.get(resource.fileId);
|
|
4295
4949
|
entries[mountPath] = download
|
|
4296
4950
|
? sandboxDownloadDirectory(download, mountPath)
|
|
@@ -4450,11 +5104,7 @@ function parseAzureConnectionString(value: string): Record<string, string> {
|
|
|
4450
5104
|
}
|
|
4451
5105
|
|
|
4452
5106
|
function normalizeManifestPath(path: string): string {
|
|
4453
|
-
|
|
4454
|
-
if (!normalized || normalized.includes("..")) {
|
|
4455
|
-
throw new Error(`Invalid sandbox resource path: ${path}`);
|
|
4456
|
-
}
|
|
4457
|
-
return normalized;
|
|
5107
|
+
return normalizeResourceMountPath(path);
|
|
4458
5108
|
}
|
|
4459
5109
|
|
|
4460
5110
|
function normalizeSandboxFileDownloads(downloads: SandboxFileDownload[]): SandboxFileDownload[] {
|
|
@@ -4517,6 +5167,11 @@ function shellQuote(value: string): string {
|
|
|
4517
5167
|
|
|
4518
5168
|
export type SandboxLifecycleHookPhase = "beforeAgentStart";
|
|
4519
5169
|
|
|
5170
|
+
export type SandboxLifecycleCommandRunner = (
|
|
5171
|
+
session: TurnSandboxCommandSession,
|
|
5172
|
+
args: TurnSandboxCommandArgs,
|
|
5173
|
+
) => Promise<unknown>;
|
|
5174
|
+
|
|
4520
5175
|
export type SandboxLifecycleHookContext = {
|
|
4521
5176
|
environment: Record<string, string>;
|
|
4522
5177
|
onRuntimeEvent?: (event: NormalizedRuntimeEvent) => Promise<void> | void;
|
|
@@ -4528,12 +5183,28 @@ export type SandboxLifecycleHookContext = {
|
|
|
4528
5183
|
// NEVER the box/agent manifest env (validateNoEnvironmentDelta must never see
|
|
4529
5184
|
// rotating values).
|
|
4530
5185
|
gitTokenSeeds?: GitTokenSeeds;
|
|
5186
|
+
gitCredentialBindings?: GitCredentialBindingSeed[];
|
|
4531
5187
|
toolspaceTokenSeed?: string;
|
|
5188
|
+
toolspaceTokenFile?: string;
|
|
4532
5189
|
// M3: the rig setup descriptor for the rig-setup hook (the script + marker
|
|
4533
5190
|
// version id + the rig's own timeout). Present only on a rig-bound turn.
|
|
4534
5191
|
rigSetup?: RigSetupDescriptor;
|
|
5192
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
4535
5193
|
};
|
|
4536
5194
|
|
|
5195
|
+
async function runSandboxLifecycleCommand(
|
|
5196
|
+
session: SandboxSessionLike,
|
|
5197
|
+
args: TurnSandboxCommandArgs,
|
|
5198
|
+
commandRunner?: SandboxLifecycleCommandRunner,
|
|
5199
|
+
): Promise<unknown> {
|
|
5200
|
+
if (commandRunner) {
|
|
5201
|
+
return await commandRunner(session as TurnSandboxCommandSession, args);
|
|
5202
|
+
}
|
|
5203
|
+
if (session.exec) return await session.exec(args);
|
|
5204
|
+
if (session.execCommand) return await session.execCommand(args);
|
|
5205
|
+
throw new Error("Sandbox session does not support command execution");
|
|
5206
|
+
}
|
|
5207
|
+
|
|
4537
5208
|
// M3: everything the rig-setup hook needs to run the frozen rig version's setup
|
|
4538
5209
|
// script exactly once per box. `versionId` keys the idempotence marker
|
|
4539
5210
|
// (/var/opengeni/rig-setup-<versionId>.done); `timeoutMs` is the rig-specific
|
|
@@ -4683,10 +5354,32 @@ function gitTokenSeedsForAgent(agent: Agent<any, any>): GitTokenSeeds | undefine
|
|
|
4683
5354
|
return agentGitTokenSeeds.get(agent);
|
|
4684
5355
|
}
|
|
4685
5356
|
|
|
5357
|
+
function gitCredentialBindingsForAgent(
|
|
5358
|
+
agent: Agent<any, any>,
|
|
5359
|
+
): GitCredentialBindingSeed[] | undefined {
|
|
5360
|
+
return agentGitCredentialBindings.get(agent);
|
|
5361
|
+
}
|
|
5362
|
+
|
|
4686
5363
|
function toolspaceTokenSeedForAgent(agent: Agent<any, any>): string | undefined {
|
|
4687
5364
|
return agentToolspaceTokenSeed.get(agent);
|
|
4688
5365
|
}
|
|
4689
5366
|
|
|
5367
|
+
function toolspaceTokenSessionIdForAgent(agent: Agent<any, any>): string | undefined {
|
|
5368
|
+
return agentToolspaceTokenSessionId.get(agent);
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
function toolspaceTokenFileForAgent(
|
|
5372
|
+
agent: Agent<any, any>,
|
|
5373
|
+
environment: Readonly<Record<string, string>>,
|
|
5374
|
+
): string | undefined {
|
|
5375
|
+
if (!toolspaceTokenSeedForAgent(agent)) return undefined;
|
|
5376
|
+
const sessionId = toolspaceTokenSessionIdForAgent(agent);
|
|
5377
|
+
if (!sessionId) {
|
|
5378
|
+
throw new Error("Toolspace token seed is missing its session identity");
|
|
5379
|
+
}
|
|
5380
|
+
return toolspaceTokenFileFromEnvironment(environment, sessionId);
|
|
5381
|
+
}
|
|
5382
|
+
|
|
4690
5383
|
function sandboxToolspaceTokenHooksForAgent(agent: Agent<any, any>): SandboxLifecycleHook[] {
|
|
4691
5384
|
return toolspaceTokenSeedForAgent(agent)
|
|
4692
5385
|
? [
|
|
@@ -4756,6 +5449,22 @@ function gitCredentialSessionRegistrationHooks(
|
|
|
4756
5449
|
: [];
|
|
4757
5450
|
}
|
|
4758
5451
|
|
|
5452
|
+
function toolspaceTokenSessionRegistrationHooks(
|
|
5453
|
+
callback: RunAgentStreamOptions["onToolspaceTokenSessionReady"],
|
|
5454
|
+
): SandboxLifecycleHook[] {
|
|
5455
|
+
return callback
|
|
5456
|
+
? [
|
|
5457
|
+
{
|
|
5458
|
+
id: "toolspace-token-renewal-registration",
|
|
5459
|
+
phase: "beforeAgentStart",
|
|
5460
|
+
run: async (session) => {
|
|
5461
|
+
await callback(session);
|
|
5462
|
+
},
|
|
5463
|
+
},
|
|
5464
|
+
]
|
|
5465
|
+
: [];
|
|
5466
|
+
}
|
|
5467
|
+
|
|
4759
5468
|
function sandboxRepositoryCloneHooks(
|
|
4760
5469
|
settings: Settings,
|
|
4761
5470
|
resources: ResourceRef[],
|
|
@@ -4823,6 +5532,20 @@ function gitProviderSeedEnv(provider: GitCredentialProvider): string {
|
|
|
4823
5532
|
return `OPENGENI_GIT_${provider.toUpperCase()}_TOKEN_SEED`;
|
|
4824
5533
|
}
|
|
4825
5534
|
|
|
5535
|
+
export function gitCredentialBindingHash(credentialBindingId: string): string {
|
|
5536
|
+
return createHash("sha256").update(credentialBindingId, "utf8").digest("hex").slice(0, 32);
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
function gitBindingSeedEnv(binding: GitCredentialBindingSeed): string {
|
|
5540
|
+
return `OPENGENI_GIT_BINDING_${gitCredentialBindingHash(binding.credentialBindingId).toUpperCase()}_TOKEN_SEED`;
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
function gitCredentialBindingSeedExportPrefix(bindings: GitCredentialBindingSeed[]): string {
|
|
5544
|
+
return bindings
|
|
5545
|
+
.map((binding) => `export ${gitBindingSeedEnv(binding)}=${shellQuote(binding.token)}`)
|
|
5546
|
+
.join("\n");
|
|
5547
|
+
}
|
|
5548
|
+
|
|
4826
5549
|
function gitTokenSeedExportPrefix(seeds: GitTokenSeeds): string {
|
|
4827
5550
|
const lines: string[] = [];
|
|
4828
5551
|
for (const provider of GIT_CREDENTIAL_PROVIDERS) {
|
|
@@ -4838,37 +5561,116 @@ function gitTokenSeedExportPrefix(seeds: GitTokenSeeds): string {
|
|
|
4838
5561
|
return lines.join("\n");
|
|
4839
5562
|
}
|
|
4840
5563
|
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
5564
|
+
type RuntimeGitBindingDescriptor = {
|
|
5565
|
+
provider: GitCredentialProvider;
|
|
5566
|
+
credentialBindingId: string;
|
|
5567
|
+
bindingHash: string;
|
|
5568
|
+
protocol: string;
|
|
5569
|
+
host: string;
|
|
5570
|
+
path: string;
|
|
5571
|
+
uri: string;
|
|
5572
|
+
};
|
|
5573
|
+
|
|
5574
|
+
function runtimeGitBindingDescriptors(
|
|
5575
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5576
|
+
): RuntimeGitBindingDescriptor[] {
|
|
5577
|
+
const remoteBindings = new Map<string, string>();
|
|
5578
|
+
const bindingProviders = new Map<string, GitCredentialProvider>();
|
|
5579
|
+
return resources.map((resource) => {
|
|
5580
|
+
const url = new URL(resource.uri);
|
|
5581
|
+
const credentialProvider = gitCredentialProviderForRepository(resource);
|
|
5582
|
+
// Provider-less public/legacy resources retain the historical GitHub
|
|
5583
|
+
// askpass fallback, but credential-bound resources derive through the
|
|
5584
|
+
// shared contracts helper used by the worker and core.
|
|
5585
|
+
const provider = credentialProvider ?? "github";
|
|
5586
|
+
const credentialBindingId =
|
|
5587
|
+
gitCredentialBindingIdForRepository(resource, credentialProvider) ?? provider;
|
|
5588
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "");
|
|
5589
|
+
const remote = `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}/${path.replace(/\.git$/, "")}`;
|
|
5590
|
+
const bindingKey = `${provider}\u0000${credentialBindingId}`;
|
|
5591
|
+
const boundProvider = bindingProviders.get(credentialBindingId);
|
|
5592
|
+
if (boundProvider && boundProvider !== provider) {
|
|
5593
|
+
throw new Error(
|
|
5594
|
+
`credential binding ${credentialBindingId} is assigned to multiple Git providers`,
|
|
5595
|
+
);
|
|
5596
|
+
}
|
|
5597
|
+
bindingProviders.set(credentialBindingId, provider);
|
|
5598
|
+
const claimed = remoteBindings.get(remote);
|
|
5599
|
+
if (claimed && claimed !== bindingKey) {
|
|
5600
|
+
throw new Error(
|
|
5601
|
+
`repository remote ${resource.uri} is claimed by multiple credential bindings`,
|
|
5602
|
+
);
|
|
5603
|
+
}
|
|
5604
|
+
remoteBindings.set(remote, bindingKey);
|
|
5605
|
+
return {
|
|
5606
|
+
provider,
|
|
5607
|
+
credentialBindingId,
|
|
5608
|
+
bindingHash: gitCredentialBindingHash(credentialBindingId),
|
|
5609
|
+
protocol: url.protocol.replace(/:$/, "").toLowerCase(),
|
|
5610
|
+
host: url.host.toLowerCase(),
|
|
5611
|
+
path,
|
|
5612
|
+
uri: resource.uri,
|
|
5613
|
+
};
|
|
5614
|
+
});
|
|
5615
|
+
}
|
|
5616
|
+
|
|
5617
|
+
function gitUsernameForProvider(provider: GitCredentialProvider): string {
|
|
5618
|
+
if (provider === "github") return "x-access-token";
|
|
5619
|
+
if (provider === "gitlab") return "oauth2";
|
|
5620
|
+
return "opengeni";
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
function gitCredentialHelperBindingCaseLines(
|
|
5624
|
+
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
5625
|
+
): string[] {
|
|
5626
|
+
return runtimeGitBindingDescriptors(resources).flatMap((descriptor) => {
|
|
5627
|
+
const paths = new Set([
|
|
5628
|
+
descriptor.path,
|
|
5629
|
+
descriptor.path.replace(/\.git$/, ""),
|
|
5630
|
+
`${descriptor.path.replace(/\.git$/, "")}.git`,
|
|
5631
|
+
]);
|
|
5632
|
+
return [...paths].map(
|
|
5633
|
+
(path) =>
|
|
5634
|
+
` ${shellQuote(`${descriptor.protocol}|${descriptor.host}|${path}`)}) username=${shellQuote(gitUsernameForProvider(descriptor.provider))}; token_file="$credential_dir/${descriptor.bindingHash}-token" ;;`,
|
|
5635
|
+
);
|
|
5636
|
+
});
|
|
4845
5637
|
}
|
|
4846
5638
|
|
|
4847
5639
|
function gitAskpassHostProviderCaseLines(
|
|
4848
5640
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
4849
5641
|
): string[] {
|
|
4850
|
-
const hosts = new Map<string, GitCredentialProvider>();
|
|
4851
|
-
for (const
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
} catch {
|
|
4859
|
-
// Resource validation catches invalid URIs before normal runtime use. Keep
|
|
4860
|
-
// helper generation tolerant so tests for clone failure can still build.
|
|
4861
|
-
}
|
|
5642
|
+
const hosts = new Map<string, { provider: GitCredentialProvider; bindings: Set<string> }>();
|
|
5643
|
+
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
5644
|
+
const entry = hosts.get(descriptor.host) ?? {
|
|
5645
|
+
provider: descriptor.provider,
|
|
5646
|
+
bindings: new Set<string>(),
|
|
5647
|
+
};
|
|
5648
|
+
entry.bindings.add(`${descriptor.provider}\u0000${descriptor.credentialBindingId}`);
|
|
5649
|
+
hosts.set(descriptor.host, entry);
|
|
4862
5650
|
}
|
|
4863
5651
|
return [...hosts.entries()]
|
|
5652
|
+
.filter(([, entry]) => entry.bindings.size === 1)
|
|
4864
5653
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
4865
5654
|
.map(
|
|
4866
|
-
([hostname,
|
|
4867
|
-
` ${shellQuote(hostname)}) printf '%s\\n' ${provider}; return 0 ;;`,
|
|
5655
|
+
([hostname, entry]) =>
|
|
5656
|
+
` ${shellQuote(hostname)}) printf '%s\\n' ${entry.provider}; return 0 ;;`,
|
|
4868
5657
|
);
|
|
4869
5658
|
}
|
|
4870
5659
|
|
|
4871
|
-
function gitCredentialTokenWriterCommandLines(): string[] {
|
|
5660
|
+
function gitCredentialTokenWriterCommandLines(bindings: GitCredentialBindingSeed[] = []): string[] {
|
|
5661
|
+
const bindingWrites = bindings.flatMap((binding) => {
|
|
5662
|
+
const hash = gitCredentialBindingHash(binding.credentialBindingId);
|
|
5663
|
+
const seedEnv = gitBindingSeedEnv(binding);
|
|
5664
|
+
const count = Math.max(1, binding.providerBindingCount ?? 1);
|
|
5665
|
+
const provider = binding.provider;
|
|
5666
|
+
const lines = [`write_git_binding_token ${shellQuote(hash)} "\${${seedEnv}:-}"`];
|
|
5667
|
+
if (count === 1) {
|
|
5668
|
+
lines.push(`write_git_provider_token ${shellQuote(provider)} "\${${seedEnv}:-}"`);
|
|
5669
|
+
} else {
|
|
5670
|
+
lines.push(`remove_git_provider_token ${shellQuote(provider)}`);
|
|
5671
|
+
}
|
|
5672
|
+
return lines;
|
|
5673
|
+
});
|
|
4872
5674
|
return [
|
|
4873
5675
|
// TOKEN-BROKER (B1/B2): seed run-scoped provider tokens into stable files and
|
|
4874
5676
|
// atomically replace each provider file. Token VALUES are supplied only by
|
|
@@ -4882,6 +5684,23 @@ function gitCredentialTokenWriterCommandLines(): string[] {
|
|
|
4882
5684
|
" *) printf '%s\\n' \"${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$provider-token\" ;;",
|
|
4883
5685
|
" esac",
|
|
4884
5686
|
"}",
|
|
5687
|
+
"write_git_binding_token() {",
|
|
5688
|
+
' binding_hash="$1"',
|
|
5689
|
+
' token="$2"',
|
|
5690
|
+
' [ -n "$token" ] || return 0',
|
|
5691
|
+
' credential_dir="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}"',
|
|
5692
|
+
' mkdir -p "$credential_dir"',
|
|
5693
|
+
' token_file="$credential_dir/$binding_hash-token"',
|
|
5694
|
+
' printf \'%s\' "$token" > "$token_file.tmp.$$"',
|
|
5695
|
+
' mv -f "$token_file.tmp.$$" "$token_file"',
|
|
5696
|
+
"}",
|
|
5697
|
+
"remove_git_provider_token() {",
|
|
5698
|
+
' provider="$1"',
|
|
5699
|
+
' rm -f "$(git_provider_token_file "$provider")"',
|
|
5700
|
+
' if [ "$provider" = github ]; then',
|
|
5701
|
+
' rm -f "${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/github-token"',
|
|
5702
|
+
" fi",
|
|
5703
|
+
"}",
|
|
4885
5704
|
"write_git_provider_token() {",
|
|
4886
5705
|
' provider="$1"',
|
|
4887
5706
|
' token="$2"',
|
|
@@ -4902,16 +5721,53 @@ function gitCredentialTokenWriterCommandLines(): string[] {
|
|
|
4902
5721
|
'write_git_provider_token github "${OPENGENI_GIT_GITHUB_TOKEN_SEED:-${OPENGENI_GIT_TOKEN_SEED:-}}"',
|
|
4903
5722
|
'write_git_provider_token gitlab "${OPENGENI_GIT_GITLAB_TOKEN_SEED:-}"',
|
|
4904
5723
|
'write_git_provider_token azure_devops "${OPENGENI_GIT_AZURE_DEVOPS_TOKEN_SEED:-}"',
|
|
5724
|
+
...bindingWrites,
|
|
4905
5725
|
'umask "$seed_umask"',
|
|
4906
5726
|
];
|
|
4907
5727
|
}
|
|
4908
5728
|
|
|
4909
5729
|
function gitCredentialHelperCommandLines(
|
|
4910
5730
|
resources: Extract<ResourceRef, { kind: "repository" }>[] = [],
|
|
5731
|
+
bindings: GitCredentialBindingSeed[] = [],
|
|
4911
5732
|
): string[] {
|
|
4912
5733
|
const hostProviderCases = gitAskpassHostProviderCaseLines(resources);
|
|
5734
|
+
const bindingCases = gitCredentialHelperBindingCaseLines(resources);
|
|
5735
|
+
const descriptors = runtimeGitBindingDescriptors(resources);
|
|
5736
|
+
const wrapperDescriptors = bindings.length > 0 ? descriptors : [];
|
|
5737
|
+
const bindingProviders = new Map<GitCredentialProvider, Set<string>>();
|
|
5738
|
+
for (const descriptor of runtimeGitBindingDescriptors(resources)) {
|
|
5739
|
+
const ids = bindingProviders.get(descriptor.provider) ?? new Set<string>();
|
|
5740
|
+
ids.add(descriptor.credentialBindingId);
|
|
5741
|
+
bindingProviders.set(descriptor.provider, ids);
|
|
5742
|
+
}
|
|
5743
|
+
const strictAskpass = [...bindingProviders.values()].some((ids) => ids.size > 1);
|
|
5744
|
+
const allowedWrapperHashes = [
|
|
5745
|
+
...new Set(wrapperDescriptors.map((item) => `${item.provider}|${item.bindingHash}`)),
|
|
5746
|
+
].map((key) => ` ${shellQuote(key)}) return 0 ;;`);
|
|
5747
|
+
const originWrapperHashes = wrapperDescriptors.flatMap((item) => {
|
|
5748
|
+
const base = item.uri.replace(/\.git$/, "");
|
|
5749
|
+
return [...new Set([item.uri, base, `${base}.git`])].map(
|
|
5750
|
+
(uri) =>
|
|
5751
|
+
` ${shellQuote(`${item.provider}|${uri}`)}) printf '%s\\n' ${shellQuote(item.bindingHash)}; return 0 ;;`,
|
|
5752
|
+
);
|
|
5753
|
+
});
|
|
5754
|
+
const soleWrapperHashes = [...bindingProviders.entries()].flatMap(([provider, ids]) => {
|
|
5755
|
+
if (ids.size !== 1) return [];
|
|
5756
|
+
const descriptor = wrapperDescriptors.find((item) => item.provider === provider);
|
|
5757
|
+
return descriptor
|
|
5758
|
+
? [
|
|
5759
|
+
` ${shellQuote(provider)}) printf '%s\\n' ${shellQuote(descriptor.bindingHash)}; return 0 ;;`,
|
|
5760
|
+
]
|
|
5761
|
+
: [];
|
|
5762
|
+
});
|
|
5763
|
+
const multiWrapperProviders =
|
|
5764
|
+
bindings.length > 0
|
|
5765
|
+
? [...bindingProviders.entries()]
|
|
5766
|
+
.filter(([, ids]) => ids.size > 1)
|
|
5767
|
+
.map(([provider]) => provider)
|
|
5768
|
+
: [];
|
|
4913
5769
|
return [
|
|
4914
|
-
...gitCredentialTokenWriterCommandLines(),
|
|
5770
|
+
...gitCredentialTokenWriterCommandLines(bindings),
|
|
4915
5771
|
// Provision git/provider-CLI helpers at SETUP (runtime) before any clone
|
|
4916
5772
|
// runs. Renewal updates only token files and deliberately leaves these
|
|
4917
5773
|
// repository-specific host mappings intact.
|
|
@@ -4928,8 +5784,8 @@ function gitCredentialHelperCommandLines(
|
|
|
4928
5784
|
' rest="${prompt_lower#*://}"',
|
|
4929
5785
|
' rest="${rest#*@}"',
|
|
4930
5786
|
' host="${rest%%/*}"',
|
|
4931
|
-
' host="${host%%:*}"',
|
|
4932
5787
|
' host="$(printf \'%s\\n\' "$host" | tr -d "\'")"',
|
|
5788
|
+
' host="${host%:}"',
|
|
4933
5789
|
" printf '%s\\n' \"$host\"",
|
|
4934
5790
|
"}",
|
|
4935
5791
|
"provider_for_prompt() {",
|
|
@@ -4941,7 +5797,7 @@ function gitCredentialHelperCommandLines(
|
|
|
4941
5797
|
" *github.com*|*githubusercontent.com*) printf '%s\\n' github ;;",
|
|
4942
5798
|
" *gitlab*) printf '%s\\n' gitlab ;;",
|
|
4943
5799
|
" *dev.azure.com*|*.visualstudio.com*) printf '%s\\n' azure_devops ;;",
|
|
4944
|
-
" *) printf '%s\\n' github ;;",
|
|
5800
|
+
strictAskpass ? " *) printf '\\n' ;;" : " *) printf '%s\\n' github ;;",
|
|
4945
5801
|
" esac",
|
|
4946
5802
|
"}",
|
|
4947
5803
|
"token_file_for_provider() {",
|
|
@@ -4967,6 +5823,40 @@ function gitCredentialHelperCommandLines(
|
|
|
4967
5823
|
"ASKPASS_EOF",
|
|
4968
5824
|
'chmod 0755 "$git_askpass.tmp.$$"',
|
|
4969
5825
|
'mv -f "$git_askpass.tmp.$$" "$git_askpass"',
|
|
5826
|
+
'git_credential_helper="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/helper"',
|
|
5827
|
+
'mkdir -p "$(dirname "$git_credential_helper")"',
|
|
5828
|
+
"cat > \"$git_credential_helper.tmp.$$\" <<'GIT_CREDENTIAL_HELPER_EOF'",
|
|
5829
|
+
"#!/usr/bin/env sh",
|
|
5830
|
+
"set -eu",
|
|
5831
|
+
'[ "${1:-get}" = get ] || exit 0',
|
|
5832
|
+
"protocol= host= path=",
|
|
5833
|
+
"while IFS='=' read -r key value; do",
|
|
5834
|
+
' case "$key" in',
|
|
5835
|
+
" protocol) protocol=\"$(printf '%s' \"$value\" | tr '[:upper:]' '[:lower:]')\" ;;",
|
|
5836
|
+
" host) host=\"$(printf '%s' \"$value\" | tr '[:upper:]' '[:lower:]')\" ;;",
|
|
5837
|
+
' path) path="${value#/}" ;;',
|
|
5838
|
+
" esac",
|
|
5839
|
+
"done",
|
|
5840
|
+
'credential_dir="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}"',
|
|
5841
|
+
"username= token_file=",
|
|
5842
|
+
'case "$protocol|$host|$path" in',
|
|
5843
|
+
...bindingCases,
|
|
5844
|
+
" *) exit 0 ;;",
|
|
5845
|
+
"esac",
|
|
5846
|
+
'[ -r "$token_file" ] || exit 0',
|
|
5847
|
+
'password="$(cat "$token_file" 2>/dev/null || true)"',
|
|
5848
|
+
'[ -n "$password" ] || exit 0',
|
|
5849
|
+
'printf \'username=%s\\npassword=%s\\n\' "$username" "$password"',
|
|
5850
|
+
"GIT_CREDENTIAL_HELPER_EOF",
|
|
5851
|
+
'chmod 0755 "$git_credential_helper.tmp.$$"',
|
|
5852
|
+
'mv -f "$git_credential_helper.tmp.$$" "$git_credential_helper"',
|
|
5853
|
+
// Empty helper resets lower-priority/system helpers; our exact path-aware
|
|
5854
|
+
// helper returns no credential for an unbound remote, so multi-binding
|
|
5855
|
+
// sessions fail closed instead of falling through to ambient credentials.
|
|
5856
|
+
"git config --global --unset-all credential.helper >/dev/null 2>&1 || true",
|
|
5857
|
+
"git config --global --add credential.helper ''",
|
|
5858
|
+
'git config --global --add credential.helper "$git_credential_helper"',
|
|
5859
|
+
"git config --global credential.useHttpPath true",
|
|
4970
5860
|
'wrapper_dir="${OPENGENI_GIT_CLI_WRAPPER_DIR:-$HOME/.opengeni/bin}"',
|
|
4971
5861
|
'mkdir -p "$wrapper_dir"',
|
|
4972
5862
|
"for opengeni_git_cli_tool in gh glab az; do",
|
|
@@ -4981,11 +5871,53 @@ function gitCredentialHelperCommandLines(
|
|
|
4981
5871
|
" az) provider=azure_devops; token_env=AZURE_DEVOPS_EXT_PAT ;;",
|
|
4982
5872
|
" *) provider=; token_env= ;;",
|
|
4983
5873
|
"esac",
|
|
4984
|
-
|
|
5874
|
+
"hash_binding_id() {",
|
|
5875
|
+
" if command -v sha256sum >/dev/null 2>&1; then printf '%s' \"$1\" | sha256sum | cut -c1-32; return; fi",
|
|
5876
|
+
" if command -v shasum >/dev/null 2>&1; then printf '%s' \"$1\" | shasum -a 256 | cut -c1-32; return; fi",
|
|
5877
|
+
" if command -v openssl >/dev/null 2>&1; then printf '%s' \"$1\" | openssl dgst -sha256 | sed 's/^.*= //' | cut -c1-32; return; fi",
|
|
5878
|
+
" printf '%s\\n' 'No SHA-256 utility is available to select OPENGENI_GIT_BINDING' >&2",
|
|
5879
|
+
" return 127",
|
|
5880
|
+
"}",
|
|
5881
|
+
"binding_hash_allowed() {",
|
|
5882
|
+
' case "$provider|$1" in',
|
|
5883
|
+
...allowedWrapperHashes,
|
|
5884
|
+
" *) return 1 ;;",
|
|
5885
|
+
" esac",
|
|
5886
|
+
"}",
|
|
5887
|
+
"binding_hash_for_origin() {",
|
|
5888
|
+
' case "$provider|$1" in',
|
|
5889
|
+
...originWrapperHashes,
|
|
5890
|
+
" *) return 1 ;;",
|
|
5891
|
+
" esac",
|
|
5892
|
+
"}",
|
|
5893
|
+
"sole_binding_hash() {",
|
|
4985
5894
|
' case "$provider" in',
|
|
4986
|
-
|
|
4987
|
-
|
|
5895
|
+
...soleWrapperHashes,
|
|
5896
|
+
" *) return 1 ;;",
|
|
4988
5897
|
" esac",
|
|
5898
|
+
"}",
|
|
5899
|
+
`multi_binding_providers=${shellQuote(multiWrapperProviders.join(" "))}`,
|
|
5900
|
+
'if [ -n "$provider" ]; then',
|
|
5901
|
+
" binding_hash=",
|
|
5902
|
+
' if [ -n "${OPENGENI_GIT_BINDING:-}" ]; then',
|
|
5903
|
+
' binding_hash="$(hash_binding_id "$OPENGENI_GIT_BINDING")"',
|
|
5904
|
+
' binding_hash_allowed "$binding_hash" || { printf \'%s\\n\' "OPENGENI_GIT_BINDING does not select a $provider credential attached to this session" >&2; exit 2; }',
|
|
5905
|
+
" elif command -v git >/dev/null 2>&1; then",
|
|
5906
|
+
' origin="$(git config --get remote.origin.url 2>/dev/null || true)"',
|
|
5907
|
+
' [ -z "$origin" ] || binding_hash="$(binding_hash_for_origin "$origin" 2>/dev/null || true)"',
|
|
5908
|
+
" fi",
|
|
5909
|
+
' [ -n "$binding_hash" ] || binding_hash="$(sole_binding_hash 2>/dev/null || true)"',
|
|
5910
|
+
' if [ -n "$binding_hash" ]; then',
|
|
5911
|
+
' token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$binding_hash-token"',
|
|
5912
|
+
" else",
|
|
5913
|
+
' case " $multi_binding_providers " in',
|
|
5914
|
+
' *" $provider "*) printf \'%s\\n\' "Unable to select one of multiple $provider credentials; run inside an attached repository or set OPENGENI_GIT_BINDING" >&2; exit 2 ;;',
|
|
5915
|
+
" esac",
|
|
5916
|
+
' case "$provider" in',
|
|
5917
|
+
' github) token_file="${OPENGENI_GIT_TOKEN_FILE:-$HOME/.opengeni/git-token}" ;;',
|
|
5918
|
+
' *) token_file="${OPENGENI_GIT_CREDENTIALS_DIR:-$HOME/.opengeni/git-credentials}/$provider-token" ;;',
|
|
5919
|
+
" esac",
|
|
5920
|
+
" fi",
|
|
4989
5921
|
' if [ -f "$token_file" ]; then',
|
|
4990
5922
|
' token="$(cat "$token_file" 2>/dev/null || true)"',
|
|
4991
5923
|
' if [ -n "$token" ]; then',
|
|
@@ -5040,10 +5972,23 @@ export function gitProviderTokenRefreshCommand(seeds: GitTokenSeeds): string {
|
|
|
5040
5972
|
].join("\n");
|
|
5041
5973
|
}
|
|
5042
5974
|
|
|
5975
|
+
export function gitCredentialBindingTokenRefreshCommand(
|
|
5976
|
+
bindings: GitCredentialBindingSeed[],
|
|
5977
|
+
): string {
|
|
5978
|
+
const seedPrefix = gitCredentialBindingSeedExportPrefix(bindings);
|
|
5979
|
+
if (!seedPrefix) return "";
|
|
5980
|
+
return [
|
|
5981
|
+
seedPrefix,
|
|
5982
|
+
"set -eu",
|
|
5983
|
+
'export HOME="${HOME:-/workspace}"',
|
|
5984
|
+
...gitCredentialTokenWriterCommandLines(bindings),
|
|
5985
|
+
].join("\n");
|
|
5986
|
+
}
|
|
5987
|
+
|
|
5043
5988
|
export async function refreshGitProviderTokenFiles(
|
|
5044
5989
|
session: GitCredentialTokenWriterSession,
|
|
5045
5990
|
seeds: GitTokenSeeds,
|
|
5046
|
-
options: { runAs?: string } = {},
|
|
5991
|
+
options: { runAs?: string; commandRunner?: SandboxLifecycleCommandRunner } = {},
|
|
5047
5992
|
): Promise<void> {
|
|
5048
5993
|
const command = gitProviderTokenRefreshCommand(seeds);
|
|
5049
5994
|
if (!command) {
|
|
@@ -5056,23 +6001,41 @@ export async function refreshGitProviderTokenFiles(
|
|
|
5056
6001
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5057
6002
|
maxOutputTokens: 4_000,
|
|
5058
6003
|
};
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
6004
|
+
assertSandboxCommandSucceeded(
|
|
6005
|
+
await runSandboxLifecycleCommand(session, args, options.commandRunner),
|
|
6006
|
+
"Git credential refresh",
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
export async function refreshGitCredentialBindingTokenFiles(
|
|
6011
|
+
session: GitCredentialTokenWriterSession,
|
|
6012
|
+
bindings: GitCredentialBindingSeed[],
|
|
6013
|
+
options: { runAs?: string; commandRunner?: SandboxLifecycleCommandRunner } = {},
|
|
6014
|
+
): Promise<void> {
|
|
6015
|
+
const command = gitCredentialBindingTokenRefreshCommand(bindings);
|
|
6016
|
+
if (!command) return;
|
|
6017
|
+
const args = {
|
|
6018
|
+
cmd: command,
|
|
6019
|
+
workdir: "/workspace",
|
|
6020
|
+
...(options.runAs ? { runAs: options.runAs } : {}),
|
|
6021
|
+
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
6022
|
+
maxOutputTokens: 4_000,
|
|
6023
|
+
};
|
|
6024
|
+
assertSandboxCommandSucceeded(
|
|
6025
|
+
await runSandboxLifecycleCommand(session, args, options.commandRunner),
|
|
6026
|
+
"Git credential binding refresh",
|
|
6027
|
+
);
|
|
5066
6028
|
}
|
|
5067
6029
|
|
|
5068
6030
|
export function repositoryCloneCommand(
|
|
5069
6031
|
resources: Extract<ResourceRef, { kind: "repository" }>[],
|
|
6032
|
+
bindings: GitCredentialBindingSeed[] = [],
|
|
5070
6033
|
): string {
|
|
6034
|
+
assertUniqueResourceMountPaths(resources);
|
|
5071
6035
|
const commands = [
|
|
5072
6036
|
"set -eu",
|
|
5073
6037
|
'export HOME="${HOME:-/workspace}"',
|
|
5074
6038
|
'export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"',
|
|
5075
|
-
...gitCredentialHelperCommandLines(resources),
|
|
5076
6039
|
"ensure_git() {",
|
|
5077
6040
|
" if command -v git >/dev/null 2>&1; then",
|
|
5078
6041
|
" return 0",
|
|
@@ -5088,6 +6051,7 @@ export function repositoryCloneCommand(
|
|
|
5088
6051
|
" exit 127",
|
|
5089
6052
|
"}",
|
|
5090
6053
|
"ensure_git",
|
|
6054
|
+
...gitCredentialHelperCommandLines(resources, bindings),
|
|
5091
6055
|
"clone_repository() {",
|
|
5092
6056
|
' target="$1"',
|
|
5093
6057
|
' uri="$2"',
|
|
@@ -5160,33 +6124,41 @@ export function repositoryCloneCommand(
|
|
|
5160
6124
|
"}",
|
|
5161
6125
|
];
|
|
5162
6126
|
for (const resource of resources) {
|
|
5163
|
-
const
|
|
5164
|
-
const repo = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
5165
|
-
const mountPath = normalizeManifestPath(resource.mountPath ?? `repos/${repo}`);
|
|
6127
|
+
const mountPath = resourceMountPath(resource);
|
|
5166
6128
|
commands.push(
|
|
5167
6129
|
[
|
|
5168
6130
|
"clone_repository",
|
|
5169
6131
|
shellQuote(posixPath.join("/workspace", mountPath)),
|
|
5170
6132
|
shellQuote(resource.uri),
|
|
5171
6133
|
shellQuote(resource.ref),
|
|
5172
|
-
shellQuote(resource.subpath ?
|
|
6134
|
+
shellQuote(resource.subpath ? normalizeRepositorySubpath(resource.subpath) : ""),
|
|
5173
6135
|
].join(" "),
|
|
5174
6136
|
);
|
|
5175
6137
|
}
|
|
5176
6138
|
return commands.join("\n");
|
|
5177
6139
|
}
|
|
5178
6140
|
|
|
5179
|
-
export function toolspaceTokenSeedCommand(
|
|
6141
|
+
export function toolspaceTokenSeedCommand(
|
|
6142
|
+
options: { tokenFile?: string; legacyTokenFile?: string } = {},
|
|
6143
|
+
): string {
|
|
5180
6144
|
return [
|
|
5181
6145
|
"set -eu",
|
|
5182
6146
|
'export HOME="${HOME:-/workspace}"',
|
|
5183
6147
|
'if [ -n "${OPENGENI_TOOLSPACE_TOKEN_SEED:-}" ]; then',
|
|
5184
6148
|
' seed_umask="$(umask)"',
|
|
5185
6149
|
" umask 077",
|
|
5186
|
-
|
|
6150
|
+
options.tokenFile
|
|
6151
|
+
? ` token_file=${shellQuote(options.tokenFile)}`
|
|
6152
|
+
: ' token_file="${OPENGENI_TOOLSPACE_TOKEN_FILE:-$HOME/.opengeni/toolspace-token}"',
|
|
6153
|
+
options.legacyTokenFile
|
|
6154
|
+
? ` legacy_token_file=${shellQuote(options.legacyTokenFile)}`
|
|
6155
|
+
: ' legacy_token_file=""',
|
|
5187
6156
|
' mkdir -p "$(dirname "$token_file")"',
|
|
5188
6157
|
' printf \'%s\' "$OPENGENI_TOOLSPACE_TOKEN_SEED" > "$token_file.tmp.$$"',
|
|
5189
6158
|
' mv -f "$token_file.tmp.$$" "$token_file"',
|
|
6159
|
+
' if [ -n "$legacy_token_file" ] && [ "$legacy_token_file" != "$token_file" ]; then',
|
|
6160
|
+
' rm -f -- "$legacy_token_file"',
|
|
6161
|
+
" fi",
|
|
5190
6162
|
' umask "$seed_umask"',
|
|
5191
6163
|
"fi",
|
|
5192
6164
|
].join("\n");
|
|
@@ -5199,28 +6171,56 @@ export async function runToolspaceTokenSeedHook(
|
|
|
5199
6171
|
if (!context.toolspaceTokenSeed) {
|
|
5200
6172
|
return;
|
|
5201
6173
|
}
|
|
5202
|
-
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(context.toolspaceTokenSeed)}\n${toolspaceTokenSeedCommand(
|
|
5203
|
-
|
|
5204
|
-
|
|
6174
|
+
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(context.toolspaceTokenSeed)}\n${toolspaceTokenSeedCommand(
|
|
6175
|
+
{
|
|
6176
|
+
...(context.toolspaceTokenFile ? { tokenFile: context.toolspaceTokenFile } : {}),
|
|
6177
|
+
...(context.toolspaceTokenFile && context.environment.OPENGENI_TOOLSPACE_TOKEN_FILE
|
|
6178
|
+
? { legacyTokenFile: context.environment.OPENGENI_TOOLSPACE_TOKEN_FILE }
|
|
6179
|
+
: {}),
|
|
6180
|
+
},
|
|
6181
|
+
)}`;
|
|
6182
|
+
const result = await runSandboxLifecycleCommand(
|
|
6183
|
+
session,
|
|
6184
|
+
{
|
|
5205
6185
|
cmd: command,
|
|
5206
6186
|
workdir: "/workspace",
|
|
5207
6187
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5208
6188
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5209
6189
|
maxOutputTokens: 4_000,
|
|
5210
|
-
}
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
6190
|
+
},
|
|
6191
|
+
context.commandRunner,
|
|
6192
|
+
);
|
|
6193
|
+
assertSandboxCommandSucceeded(result, "Toolspace token seed hook");
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
export async function refreshToolspaceTokenFile(
|
|
6197
|
+
session: ToolspaceTokenWriterSession,
|
|
6198
|
+
token: string,
|
|
6199
|
+
options: {
|
|
6200
|
+
runAs?: string;
|
|
6201
|
+
commandRunner?: SandboxLifecycleCommandRunner;
|
|
6202
|
+
tokenFile?: string;
|
|
6203
|
+
legacyTokenFile?: string;
|
|
6204
|
+
} = {},
|
|
6205
|
+
): Promise<void> {
|
|
6206
|
+
const command = `export OPENGENI_TOOLSPACE_TOKEN_SEED=${shellQuote(token)}\n${toolspaceTokenSeedCommand(
|
|
6207
|
+
{
|
|
6208
|
+
...(options.tokenFile ? { tokenFile: options.tokenFile } : {}),
|
|
6209
|
+
...(options.legacyTokenFile ? { legacyTokenFile: options.legacyTokenFile } : {}),
|
|
6210
|
+
},
|
|
6211
|
+
)}`;
|
|
6212
|
+
const result = await runSandboxLifecycleCommand(
|
|
6213
|
+
session,
|
|
6214
|
+
{
|
|
5214
6215
|
cmd: command,
|
|
5215
6216
|
workdir: "/workspace",
|
|
5216
|
-
...(
|
|
6217
|
+
...(options.runAs ? { runAs: options.runAs } : {}),
|
|
5217
6218
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5218
6219
|
maxOutputTokens: 4_000,
|
|
5219
|
-
}
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
}
|
|
6220
|
+
},
|
|
6221
|
+
options.commandRunner,
|
|
6222
|
+
);
|
|
6223
|
+
assertSandboxCommandSucceeded(result, "Toolspace token refresh");
|
|
5224
6224
|
}
|
|
5225
6225
|
|
|
5226
6226
|
// Bounds the setup output tail carried on a rig.setup failure event/error so a
|
|
@@ -5329,13 +6329,7 @@ export async function runRigSetupHook(
|
|
|
5329
6329
|
};
|
|
5330
6330
|
let result: unknown;
|
|
5331
6331
|
try {
|
|
5332
|
-
|
|
5333
|
-
result = await session.exec(execArgs);
|
|
5334
|
-
} else if (session.execCommand) {
|
|
5335
|
-
result = await session.execCommand(execArgs);
|
|
5336
|
-
} else {
|
|
5337
|
-
throw new Error("Sandbox session does not support command execution");
|
|
5338
|
-
}
|
|
6332
|
+
result = await runSandboxLifecycleCommand(session, execArgs, context.commandRunner);
|
|
5339
6333
|
} catch (error) {
|
|
5340
6334
|
const message = error instanceof Error ? error.message : String(error);
|
|
5341
6335
|
await context.onRuntimeEvent?.({
|
|
@@ -5414,31 +6408,27 @@ export async function runRepositoryCloneHook(
|
|
|
5414
6408
|
...(context.gitTokenSeeds ?? {}),
|
|
5415
6409
|
...(context.gitTokenSeed ? { github: context.gitTokenSeed } : {}),
|
|
5416
6410
|
} satisfies GitTokenSeeds;
|
|
5417
|
-
const
|
|
5418
|
-
const
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
assertSandboxCommandSucceeded(result, "Repository clone hook");
|
|
5430
|
-
} else if (session.execCommand) {
|
|
5431
|
-
const result = await session.execCommand({
|
|
6411
|
+
const gitCredentialBindings = context.gitCredentialBindings ?? [];
|
|
6412
|
+
const seedPrefix = [
|
|
6413
|
+
gitTokenSeedExportPrefix(gitTokenSeeds),
|
|
6414
|
+
gitCredentialBindingSeedExportPrefix(gitCredentialBindings),
|
|
6415
|
+
]
|
|
6416
|
+
.filter(Boolean)
|
|
6417
|
+
.join("\n");
|
|
6418
|
+
const cloneCommand = repositoryCloneCommand(resources, gitCredentialBindings);
|
|
6419
|
+
const command = seedPrefix ? `${seedPrefix}\n${cloneCommand}` : cloneCommand;
|
|
6420
|
+
const result = await runSandboxLifecycleCommand(
|
|
6421
|
+
session,
|
|
6422
|
+
{
|
|
5432
6423
|
cmd: command,
|
|
5433
6424
|
workdir: "/workspace",
|
|
5434
6425
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5435
6426
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5436
6427
|
maxOutputTokens: 20_000,
|
|
5437
|
-
}
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
}
|
|
6428
|
+
},
|
|
6429
|
+
context.commandRunner,
|
|
6430
|
+
);
|
|
6431
|
+
assertSandboxCommandSucceeded(result, "Repository clone hook");
|
|
5442
6432
|
await context.onRuntimeEvent?.({
|
|
5443
6433
|
type: "sandbox.operation.completed",
|
|
5444
6434
|
payload,
|
|
@@ -5564,27 +6554,18 @@ export async function runAzureCliLoginHook(
|
|
|
5564
6554
|
payload,
|
|
5565
6555
|
});
|
|
5566
6556
|
try {
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
workdir: "/workspace",
|
|
5571
|
-
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5572
|
-
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5573
|
-
maxOutputTokens: 20_000,
|
|
5574
|
-
});
|
|
5575
|
-
assertSandboxCommandSucceeded(result, "Azure CLI login hook");
|
|
5576
|
-
} else if (session.execCommand) {
|
|
5577
|
-
const result = await session.execCommand({
|
|
6557
|
+
const result = await runSandboxLifecycleCommand(
|
|
6558
|
+
session,
|
|
6559
|
+
{
|
|
5578
6560
|
cmd: azureCliLoginCommand(),
|
|
5579
6561
|
workdir: "/workspace",
|
|
5580
6562
|
...(context.runAs ? { runAs: context.runAs } : {}),
|
|
5581
6563
|
yieldTimeMs: SANDBOX_LIFECYCLE_COMMAND_TIMEOUT_MS,
|
|
5582
6564
|
maxOutputTokens: 20_000,
|
|
5583
|
-
}
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
}
|
|
6565
|
+
},
|
|
6566
|
+
context.commandRunner,
|
|
6567
|
+
);
|
|
6568
|
+
assertSandboxCommandSucceeded(result, "Azure CLI login hook");
|
|
5588
6569
|
await context.onRuntimeEvent?.({
|
|
5589
6570
|
type: "sandbox.operation.completed",
|
|
5590
6571
|
payload,
|
|
@@ -5674,26 +6655,46 @@ function isPathWithin(root: string, candidate: string): boolean {
|
|
|
5674
6655
|
}
|
|
5675
6656
|
|
|
5676
6657
|
/**
|
|
5677
|
-
* The skill source fed to the SDK Skills capability. Without pack
|
|
5678
|
-
* is the plain bundled local-dir source, byte-for-byte the
|
|
5679
|
-
* With
|
|
5680
|
-
* skill directories (as local_dir
|
|
5681
|
-
*
|
|
5682
|
-
* index, one `## Skills` instruction section, lazy
|
|
5683
|
-
* them. A pack skill shadows a bundled
|
|
6658
|
+
* The skill source fed to the SDK Skills capability. Without pack or curated
|
|
6659
|
+
* skills this is the plain bundled local-dir source, byte-for-byte the
|
|
6660
|
+
* pre-pack behavior. With either selected source it becomes a single
|
|
6661
|
+
* in-memory dir source combining bundled skill directories (as local_dir
|
|
6662
|
+
* entries the SDK materializes lazily) with selected in-memory skill
|
|
6663
|
+
* directories — one skill index, one `## Skills` instruction section, lazy
|
|
6664
|
+
* `load_skill` for all of them. A pack skill shadows a bundled or curated
|
|
6665
|
+
* skill with the same directory name, case-insensitively.
|
|
5684
6666
|
*/
|
|
5685
|
-
export function lazySkillSourceWithPackSkills(
|
|
6667
|
+
export function lazySkillSourceWithPackSkills(
|
|
6668
|
+
packSkills: PackSkill[],
|
|
6669
|
+
skillLibrarySkills: PackSkill[] = [],
|
|
6670
|
+
): LocalDirLazySkillSource {
|
|
5686
6671
|
const bundledDir = bundledSkillsDir();
|
|
5687
6672
|
const bundled = localDirLazySkillSource({ src: bundledDir });
|
|
5688
|
-
if (packSkills.length === 0) {
|
|
6673
|
+
if (packSkills.length === 0 && skillLibrarySkills.length === 0) {
|
|
5689
6674
|
return bundled;
|
|
5690
6675
|
}
|
|
5691
6676
|
const children: Record<string, Entry> = {};
|
|
5692
6677
|
for (const name of bundledSkillDirNames(bundledDir)) {
|
|
5693
6678
|
children[name] = localDir({ src: join(bundledDir, name) });
|
|
5694
6679
|
}
|
|
6680
|
+
const libraryIndex: SkillIndexEntry[] = [];
|
|
6681
|
+
const libraryNameKeys = new Set<string>();
|
|
6682
|
+
for (const skill of skillLibrarySkills) {
|
|
6683
|
+
assertSafePackSkillName(skill.name);
|
|
6684
|
+
const key = skill.name.toLowerCase();
|
|
6685
|
+
if (libraryNameKeys.has(key)) {
|
|
6686
|
+
throw new Error(`Duplicate curated skill name: ${skill.name}`);
|
|
6687
|
+
}
|
|
6688
|
+
libraryNameKeys.add(key);
|
|
6689
|
+
removeSkillChildByNameKey(children, key);
|
|
6690
|
+
children[skill.name] = packSkillDirEntry(skill);
|
|
6691
|
+
libraryIndex.push({
|
|
6692
|
+
name: skill.name,
|
|
6693
|
+
description: packSkillDescription(skill),
|
|
6694
|
+
path: skill.name,
|
|
6695
|
+
});
|
|
6696
|
+
}
|
|
5695
6697
|
const packIndex: SkillIndexEntry[] = [];
|
|
5696
|
-
const packNames = new Set<string>();
|
|
5697
6698
|
const packNameKeys = new Set<string>();
|
|
5698
6699
|
for (const skill of packSkills) {
|
|
5699
6700
|
assertSafePackSkillName(skill.name);
|
|
@@ -5701,7 +6702,7 @@ export function lazySkillSourceWithPackSkills(packSkills: PackSkill[]): LocalDir
|
|
|
5701
6702
|
throw new Error(`Duplicate pack skill name: ${skill.name}`);
|
|
5702
6703
|
}
|
|
5703
6704
|
packNameKeys.add(skill.name.toLowerCase());
|
|
5704
|
-
|
|
6705
|
+
removeSkillChildByNameKey(children, skill.name.toLowerCase());
|
|
5705
6706
|
children[skill.name] = packSkillDirEntry(skill);
|
|
5706
6707
|
packIndex.push({
|
|
5707
6708
|
name: skill.name,
|
|
@@ -5713,13 +6714,62 @@ export function lazySkillSourceWithPackSkills(packSkills: PackSkill[]): LocalDir
|
|
|
5713
6714
|
source: dir({ children }),
|
|
5714
6715
|
getIndex: (manifest, skillsPath) => [
|
|
5715
6716
|
...(bundled.getIndex?.(manifest, skillsPath) ?? []).filter(
|
|
5716
|
-
(entry) =>
|
|
6717
|
+
(entry) =>
|
|
6718
|
+
!packNameKeys.has((entry.path ?? entry.name).toLowerCase()) &&
|
|
6719
|
+
!libraryNameKeys.has((entry.path ?? entry.name).toLowerCase()),
|
|
6720
|
+
),
|
|
6721
|
+
...libraryIndex.filter(
|
|
6722
|
+
(entry) => !packNameKeys.has((entry.path ?? entry.name).toLowerCase()),
|
|
5717
6723
|
),
|
|
5718
6724
|
...packIndex,
|
|
5719
6725
|
],
|
|
5720
6726
|
};
|
|
5721
6727
|
}
|
|
5722
6728
|
|
|
6729
|
+
function effectiveSkillSelections(
|
|
6730
|
+
librarySelections: readonly EffectiveSkillSelection[],
|
|
6731
|
+
librarySkills: readonly PackSkill[],
|
|
6732
|
+
packSkills: readonly PackSkill[],
|
|
6733
|
+
): readonly EffectiveSkillSelection[] {
|
|
6734
|
+
const defaultSelections = bundledSkillDirNames(bundledSkillsDir()).map((name) => ({
|
|
6735
|
+
id: `bundled:${name}`,
|
|
6736
|
+
name,
|
|
6737
|
+
source: "bundled" as const,
|
|
6738
|
+
version: null,
|
|
6739
|
+
contentSha256: null,
|
|
6740
|
+
reason: "deployment default skill bundle",
|
|
6741
|
+
}));
|
|
6742
|
+
const libraryNameKeys = new Set(librarySkills.map((skill) => skill.name.toLowerCase()));
|
|
6743
|
+
const packNameKeys = new Set(packSkills.map((skill) => skill.name.toLowerCase()));
|
|
6744
|
+
const selected = librarySelections.filter((selection) =>
|
|
6745
|
+
libraryNameKeys.has(selection.name.toLowerCase()),
|
|
6746
|
+
);
|
|
6747
|
+
return [
|
|
6748
|
+
...defaultSelections.filter(
|
|
6749
|
+
(selection) =>
|
|
6750
|
+
!libraryNameKeys.has(selection.name.toLowerCase()) &&
|
|
6751
|
+
!packNameKeys.has(selection.name.toLowerCase()),
|
|
6752
|
+
),
|
|
6753
|
+
...selected.filter((selection) => !packNameKeys.has(selection.name.toLowerCase())),
|
|
6754
|
+
...packSkills.map((skill) => ({
|
|
6755
|
+
id: `pack:${skill.name}`,
|
|
6756
|
+
name: skill.name,
|
|
6757
|
+
source: "pack" as const,
|
|
6758
|
+
version: null,
|
|
6759
|
+
contentSha256: null,
|
|
6760
|
+
reason: "enabled capability pack",
|
|
6761
|
+
})),
|
|
6762
|
+
];
|
|
6763
|
+
}
|
|
6764
|
+
|
|
6765
|
+
function removeSkillChildByNameKey(children: Record<string, Entry>, nameKey: string): void {
|
|
6766
|
+
for (const name of Object.keys(children)) {
|
|
6767
|
+
if (name.toLowerCase() === nameKey) {
|
|
6768
|
+
delete children[name];
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
|
|
5723
6773
|
function bundledSkillDirNames(root: string): string[] {
|
|
5724
6774
|
return readdirSync(root, { withFileTypes: true })
|
|
5725
6775
|
.filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "SKILL.md")))
|
|
@@ -5863,7 +6913,12 @@ function sortJson(value: unknown): unknown {
|
|
|
5863
6913
|
}
|
|
5864
6914
|
return value;
|
|
5865
6915
|
}
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
6916
|
+
function interruptionToolName(item: unknown): string {
|
|
6917
|
+
const candidate = item as {
|
|
6918
|
+
toolName?: unknown;
|
|
6919
|
+
name?: unknown;
|
|
6920
|
+
rawItem?: { name?: unknown };
|
|
6921
|
+
};
|
|
6922
|
+
const name = candidate?.toolName ?? candidate?.name ?? candidate?.rawItem?.name;
|
|
6923
|
+
return typeof name === "string" ? name : "";
|
|
5869
6924
|
}
|