@tangle-network/sandbox 0.30.1 → 0.30.2-develop.20260820122029.869ebfa
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/README.md +1 -0
- package/dist/abort-signal-si1WMfJb.js +31 -0
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +3 -3
- package/dist/agent-run-outcome-CM7xD7Mg.d.ts +27 -0
- package/dist/auth/index.d.ts +1 -1
- package/dist/browser-interactive-control-controller-B476gcui.d.ts +96 -0
- package/dist/browser-interactive-control-controller-wXQ7W5pT.js +276 -0
- package/dist/{client-Bg9vBEfL.js → client-BfnJ44gF.js} +29 -6
- package/dist/{client-DRV5bzRc.d.ts → client-Do_HS671.d.ts} +28 -3
- package/dist/collaboration/index.d.ts +1 -1
- package/dist/collaboration/index.js +1 -1
- package/dist/core.d.ts +4 -4
- package/dist/core.js +2 -2
- package/dist/{index-ldDRk8Jh.d.ts → index-ebsVOlUc.d.ts} +2 -2
- package/dist/index.d.ts +11 -8
- package/dist/index.js +7 -6
- package/dist/interactive-control.d.ts +2 -0
- package/dist/interactive-control.js +2 -0
- package/dist/openai/index.d.ts +24 -2
- package/dist/openai/index.js +147 -67
- package/dist/{runtime-api-CK-4S7oA.js → runtime-api-B0tx7NJM.js} +389 -7
- package/dist/runtime.d.ts +4 -3
- package/dist/runtime.js +3 -2
- package/dist/{sandbox-4F9CNqdR.js → sandbox-AUHcCJe1.js} +1435 -784
- package/dist/{sandbox-B49ZGQv7.d.ts → sandbox-Cr-4qzaA.d.ts} +139 -194
- package/dist/tangle/index.d.ts +1 -1
- package/dist/tangle/index.js +1 -1
- package/dist/{tangle-CHpXtqlI.js → tangle-iDk_s8vt.js} +1 -1
- package/dist/terminal-stream-4KNJ6SmL.d.ts +256 -0
- package/dist/{types-Qn3x7R8O.d.ts → types-BwYLVqxX.d.ts} +24 -256
- package/package.json +9 -4
- /package/dist/{collaboration-COCXdRrk.js → collaboration-DQD9kCdN.js} +0 -0
- /package/dist/{errors-Bdy5jXzc.d.ts → errors-CQOvGddH.d.ts} +0 -0
- /package/dist/{index-DtRFVx5U.d.ts → index-Bq_z_u6A.d.ts} +0 -0
- /package/dist/{index-Cp1KYanB.d.ts → index-CMLW3u5K.d.ts} +0 -0
- /package/dist/{local-cli-auth-D4u7fsnH.js → local-cli-auth-CJmSpTjk.js} +0 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { a as uploadChunked, c as
|
|
1
|
+
import { a as uploadChunked, c as encodePromptForWire, d as assertCurrentBackendType, f as normalizeRuntimeBackendConfig, i as sessionHeaders, m as deriveAgentResultOutcome, n as normalizeSessionInfo, o as SANDBOX_PROXY_REQUEST_MAX_BYTES, p as createAgentRunOutcomeTracker, r as appendSessionIdQuery, t as SandboxRuntimeApi, u as applyRequestedModel } from "./runtime-api-B0tx7NJM.js";
|
|
2
2
|
import { a as NetworkError, c as QuotaError, d as StateError, f as TimeoutError, l as SandboxError, m as parseErrorResponse, n as CapabilityError, o as NotFoundError, p as ValidationError, t as AuthError, u as ServerError } from "./errors-C6kn-3zt.js";
|
|
3
|
-
import {
|
|
3
|
+
import { c as createInteractiveControlHolderId, d as interactiveSessionControlReleaseResponseSchema, f as interactiveSessionStatusResponseSchema, i as createBrowserInteractiveSessionControlTransport, o as InteractiveSessionControlController, r as browserInteractiveTerminalEndpoint, s as assertInteractiveSessionRef, u as interactiveSessionControlClaimResponseSchema } from "./browser-interactive-control-controller-wXQ7W5pT.js";
|
|
4
|
+
import { n as combineAbortSignals, t as awaitWithAbort } from "./abort-signal-si1WMfJb.js";
|
|
5
|
+
import { AgentExactRunControlRefSchema, AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionControlClaimSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStartSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, InteractionAcknowledgementSchema, InteractionBindingSchema, InteractionRequestSchema, InteractionResponseCommandSchema, InteractionResponseSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimIsNewer, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionPromptRequestDigest, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionStatusMatchesRef, agentInteractiveSessionStopAcknowledgementMatchesCommand, agentRunCancellationAcknowledgementMatchesRequest, canonicalCandidateDigest, exactAgentInteractiveSessionStart, sha256DigestSchema } from "@tangle-network/agent-interface";
|
|
4
6
|
import { z } from "zod";
|
|
7
|
+
import { AgentInteractiveSessionControlClaimSchema as AgentInteractiveSessionControlClaimSchema$1, AgentInteractiveSessionPromptAcknowledgementSchema as AgentInteractiveSessionPromptAcknowledgementSchema$1, AgentInteractiveSessionRefSchema as AgentInteractiveSessionRefSchema$1, AgentInteractiveSessionStatusSchema, AgentInteractiveSessionStopAcknowledgementSchema as AgentInteractiveSessionStopAcknowledgementSchema$1 } from "@tangle-network/agent-interface/environment-interactive-control";
|
|
5
8
|
import { readTokenCostUsd, readTokenUsage } from "@tangle-network/agent-core";
|
|
6
9
|
//#region ../../../packages/sse-core/dist/index.js
|
|
7
10
|
/**
|
|
@@ -475,9 +478,9 @@ function readFlagGroup(record, group, keys) {
|
|
|
475
478
|
* - Anything else (non-object, missing or non-numeric `schema`) throws:
|
|
476
479
|
* that is not a capability document at all.
|
|
477
480
|
*/
|
|
478
|
-
function parseSandboxRuntimeCapabilities(
|
|
479
|
-
if (
|
|
480
|
-
const record =
|
|
481
|
+
function parseSandboxRuntimeCapabilities(raw) {
|
|
482
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) invalidCapabilities("a non-object document");
|
|
483
|
+
const record = raw;
|
|
481
484
|
if (typeof record.schema === "number" && Number.isInteger(record.schema) && record.schema > 1) return null;
|
|
482
485
|
if (record.schema !== 1) invalidCapabilities(`schema version ${JSON.stringify(record.schema)} — this SDK understands 1`);
|
|
483
486
|
if (typeof record.agentInterface !== "string" || typeof record.sidecarVersion !== "string" || typeof record.image !== "string") invalidCapabilities("a document missing its identity fields");
|
|
@@ -485,6 +488,19 @@ function parseSandboxRuntimeCapabilities(value) {
|
|
|
485
488
|
if (interactionsValue === null || typeof interactionsValue !== "object" || Array.isArray(interactionsValue)) invalidCapabilities("a malformed 'interactions' group");
|
|
486
489
|
const interactionsRecord = interactionsValue;
|
|
487
490
|
if ("responseDedupe" in interactionsRecord && typeof interactionsRecord.responseDedupe !== "boolean") invalidCapabilities("a non-boolean 'interactions.responseDedupe' flag");
|
|
491
|
+
const interactiveAgentValue = record.interactiveAgent;
|
|
492
|
+
let interactiveAgent;
|
|
493
|
+
if (interactiveAgentValue !== void 0) interactiveAgent = readFlagGroup(record, "interactiveAgent", [
|
|
494
|
+
"start",
|
|
495
|
+
"status",
|
|
496
|
+
"attach",
|
|
497
|
+
"control",
|
|
498
|
+
"reattach",
|
|
499
|
+
"sendPrompt",
|
|
500
|
+
"input",
|
|
501
|
+
"resize",
|
|
502
|
+
"stop"
|
|
503
|
+
]);
|
|
488
504
|
return {
|
|
489
505
|
schema: 1,
|
|
490
506
|
agentInterface: record.agentInterface,
|
|
@@ -497,7 +513,8 @@ function parseSandboxRuntimeCapabilities(value) {
|
|
|
497
513
|
"idempotent"
|
|
498
514
|
]),
|
|
499
515
|
runs: readFlagGroup(record, "runs", ["executionScopedStatus", "eventReplay"]),
|
|
500
|
-
interactions: typeof interactionsRecord.responseDedupe === "boolean" ? { responseDedupe: interactionsRecord.responseDedupe } : {}
|
|
516
|
+
interactions: typeof interactionsRecord.responseDedupe === "boolean" ? { responseDedupe: interactionsRecord.responseDedupe } : {},
|
|
517
|
+
...interactiveAgent === void 0 ? {} : { interactiveAgent }
|
|
501
518
|
};
|
|
502
519
|
}
|
|
503
520
|
/**
|
|
@@ -603,11 +620,19 @@ function validateAcceptedDispatchRunRef(raw, expected, sandboxId, sessionId, exe
|
|
|
603
620
|
return accepted;
|
|
604
621
|
}
|
|
605
622
|
//#endregion
|
|
606
|
-
//#region
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
623
|
+
//#region ../../../packages/runtime-contracts/dist/interaction-response.js
|
|
624
|
+
/** Durable states reported for a recorded interaction response. */
|
|
625
|
+
const interactionResolutionDeliveryStateSchema = z.enum([
|
|
626
|
+
"pending",
|
|
627
|
+
"delivering",
|
|
628
|
+
"delivered",
|
|
629
|
+
"delivery_failed",
|
|
630
|
+
"backend_lost",
|
|
631
|
+
"effect_unknown"
|
|
632
|
+
]);
|
|
633
|
+
/** Canonical result returned after a response is recorded by Sidecar. */
|
|
634
|
+
const interactionResolutionAcknowledgementSchema = z.strictObject({
|
|
635
|
+
status: z.enum(["accepted", "already_resolved_same"]),
|
|
611
636
|
operationId: z.string().min(1),
|
|
612
637
|
responseDigest: sha256DigestSchema,
|
|
613
638
|
interactionId: z.string().min(1),
|
|
@@ -617,14 +642,19 @@ const InteractionResolutionSchema = z.strictObject({
|
|
|
617
642
|
"declined",
|
|
618
643
|
"cancelled"
|
|
619
644
|
]),
|
|
645
|
+
deliveryState: interactionResolutionDeliveryStateSchema,
|
|
620
646
|
delivered: z.boolean(),
|
|
621
647
|
resolvedAt: z.string().min(1),
|
|
622
648
|
binding: InteractionBindingSchema.optional()
|
|
623
649
|
});
|
|
624
|
-
|
|
650
|
+
/** Canonical success envelope for the public session interaction endpoint. */
|
|
651
|
+
const interactionResolutionResponseSchema = z.strictObject({
|
|
625
652
|
success: z.literal(true),
|
|
626
|
-
resolution:
|
|
653
|
+
resolution: interactionResolutionAcknowledgementSchema
|
|
627
654
|
});
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/interaction-response.ts
|
|
657
|
+
const REFUSED_INTERACTION_STATUS = z.enum(["already_resolved_different", "binding_mismatch"]);
|
|
628
658
|
const InteractionConflictEnvelopeSchema = z.object({
|
|
629
659
|
success: z.literal(false),
|
|
630
660
|
currentState: REFUSED_INTERACTION_STATUS,
|
|
@@ -667,7 +697,7 @@ function assertResolutionMatches(resolution, sessionId, submitted, response, end
|
|
|
667
697
|
async function readInteractionResult(response, endpoint, sessionId, submitted) {
|
|
668
698
|
const body = await response.text();
|
|
669
699
|
if (response.ok) {
|
|
670
|
-
const parsed =
|
|
700
|
+
const parsed = interactionResolutionResponseSchema.safeParse(parseJson(body, response, endpoint));
|
|
671
701
|
if (!parsed.success) throw invalidRuntimeResponse("Sidecar returned an invalid interaction resolution", response, endpoint);
|
|
672
702
|
assertResolutionMatches(parsed.data.resolution, sessionId, submitted, response, endpoint);
|
|
673
703
|
return parsed.data.resolution;
|
|
@@ -840,7 +870,7 @@ async function submitSandboxInteractionCommand(transport, sessionId, command, op
|
|
|
840
870
|
/**
|
|
841
871
|
* The text carried by a single event, or `undefined` when the event carries no
|
|
842
872
|
* text payload. Mirrors the event shapes the sidecar/OpenCode runtime emits:
|
|
843
|
-
* - `result` — the
|
|
873
|
+
* - `result` — the final response summary; text under `finalText`/`response`/
|
|
844
874
|
* `output`/`content`/`text`.
|
|
845
875
|
* - `token` — an incremental token; text under `value`/`delta`/`text`/
|
|
846
876
|
* `content`.
|
|
@@ -1037,277 +1067,558 @@ function collectAgentFinalMessageText(events) {
|
|
|
1037
1067
|
return composed.trim().length > 0 ? composed : void 0;
|
|
1038
1068
|
}
|
|
1039
1069
|
//#endregion
|
|
1040
|
-
//#region
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1070
|
+
//#region ../../../packages/runtime-contracts/dist/interactive-session-http.js
|
|
1071
|
+
const runningStatusSchema = AgentInteractiveSessionStatusSchema.options[0];
|
|
1072
|
+
const exitedStatusSchema = AgentInteractiveSessionStatusSchema.options[1];
|
|
1073
|
+
/** Start result for one exact interactive process. */
|
|
1074
|
+
const interactiveSessionStartDataSchema = z.discriminatedUnion("state", [runningStatusSchema.extend({
|
|
1075
|
+
control: AgentInteractiveSessionControlClaimSchema$1,
|
|
1076
|
+
streamUrl: z.string().min(1)
|
|
1077
|
+
}).strict(), exitedStatusSchema.extend({ control: AgentInteractiveSessionControlClaimSchema$1 }).strict()]);
|
|
1078
|
+
const interactiveSessionStartResponseSchema = z.object({
|
|
1079
|
+
success: z.literal(true),
|
|
1080
|
+
data: interactiveSessionStartDataSchema
|
|
1081
|
+
}).strict();
|
|
1082
|
+
z.object({
|
|
1083
|
+
success: z.literal(false),
|
|
1084
|
+
error: z.object({
|
|
1085
|
+
code: z.string().min(1),
|
|
1086
|
+
message: z.string().min(1)
|
|
1087
|
+
}).strict()
|
|
1088
|
+
}).strict();
|
|
1089
|
+
const interactiveSessionPromptResponseSchema = z.object({
|
|
1090
|
+
success: z.literal(true),
|
|
1091
|
+
data: AgentInteractiveSessionPromptAcknowledgementSchema$1
|
|
1092
|
+
}).strict();
|
|
1093
|
+
const interactiveSessionStopResponseSchema = z.object({
|
|
1094
|
+
success: z.literal(true),
|
|
1095
|
+
data: AgentInteractiveSessionStopAcknowledgementSchema$1
|
|
1096
|
+
}).strict();
|
|
1097
|
+
z.object({ ref: AgentInteractiveSessionRefSchema$1 }).strict();
|
|
1098
|
+
z.object({ incarnationId: AgentInteractiveSessionRefSchema$1.shape.incarnationId }).strict();
|
|
1099
|
+
//#endregion
|
|
1100
|
+
//#region src/sandbox-interactive-operations.ts
|
|
1101
|
+
function assertInteractiveRefBelongsToSandbox(ref, sandboxId) {
|
|
1102
|
+
if (ref.run.environmentId !== sandboxId) throw new ValidationError("Interactive session reference does not belong to this sandbox", { "ref.run.environmentId": `Expected ${sandboxId}` });
|
|
1103
|
+
}
|
|
1104
|
+
var SandboxInteractiveOperations = class {
|
|
1105
|
+
constructor(host) {
|
|
1048
1106
|
this.host = host;
|
|
1049
|
-
this.sessionId = sessionId;
|
|
1050
1107
|
}
|
|
1051
|
-
/**
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1108
|
+
/** @internal — invoked by InteractiveSessionHandle.start(). */
|
|
1109
|
+
async _startInteractive(id, options, requestOptions) {
|
|
1110
|
+
requestOptions?.signal?.throwIfAborted();
|
|
1111
|
+
const exactStart = exactAgentInteractiveSessionStart(options);
|
|
1112
|
+
if (!exactStart.profile.harness) throw new ValidationError("Interactive profile harness is required");
|
|
1113
|
+
assertCurrentBackendType(exactStart.profile.harness, "profile.harness");
|
|
1114
|
+
if (exactStart.run.sessionId !== id) throw new ValidationError("Interactive start request does not belong to the session");
|
|
1115
|
+
if (exactStart.run.environmentId !== this.host.id) throw new ValidationError("Interactive start request does not belong to this sandbox");
|
|
1116
|
+
await this.host.ensureRunning(requestOptions?.signal);
|
|
1117
|
+
const response = await this.host.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, {
|
|
1118
|
+
method: "POST",
|
|
1119
|
+
body: JSON.stringify(exactStart),
|
|
1120
|
+
...requestOptions?.signal === void 0 ? {} : { signal: requestOptions.signal }
|
|
1121
|
+
});
|
|
1122
|
+
if (!response.ok) {
|
|
1123
|
+
const body = await response.text();
|
|
1124
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1125
|
+
}
|
|
1126
|
+
const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive`;
|
|
1127
|
+
const parsed = interactiveSessionStartResponseSchema.safeParse(await response.json());
|
|
1128
|
+
if (!parsed.success) throw new ServerError("Interactive session response has an invalid exact identity", 502, {
|
|
1129
|
+
endpoint,
|
|
1130
|
+
origin: "runtime"
|
|
1131
|
+
});
|
|
1132
|
+
const result = parsed.data.data;
|
|
1133
|
+
if (!agentInteractiveSessionRefMatchesStart(exactStart, result.ref) || !agentInteractiveSessionControlClaimMatchesRef(result.ref, result.control)) throw new ServerError("Interactive session response has an invalid exact identity", 502, {
|
|
1134
|
+
endpoint,
|
|
1135
|
+
origin: "runtime"
|
|
1136
|
+
});
|
|
1137
|
+
return result;
|
|
1060
1138
|
}
|
|
1061
|
-
/**
|
|
1062
|
-
|
|
1063
|
-
|
|
1139
|
+
/** @internal — invoked by InteractiveSessionHandle.sendPrompt(). */
|
|
1140
|
+
async _sendInteractivePrompt(id, command, options) {
|
|
1141
|
+
options?.signal?.throwIfAborted();
|
|
1142
|
+
const exactCommand = AgentInteractiveSessionPromptCommandSchema.parse(command);
|
|
1143
|
+
if (exactCommand.ref.run.sessionId !== id) throw new ValidationError("Interactive prompt command does not belong to the session");
|
|
1144
|
+
await this.host.ensureRunning(options?.signal);
|
|
1145
|
+
const status = await this._interactiveStatus(id, exactCommand.ref.incarnationId, options);
|
|
1146
|
+
if (!status) throw new NotFoundError("Interactive session", id, {
|
|
1147
|
+
origin: "runtime",
|
|
1148
|
+
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
|
|
1149
|
+
});
|
|
1150
|
+
if (canonicalCandidateDigest(status.ref) !== canonicalCandidateDigest(exactCommand.ref)) throw new ValidationError("Interactive prompt ref or control does not match the session");
|
|
1151
|
+
const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive/prompt`;
|
|
1152
|
+
const response = await this.host.runtimeFetch(endpoint, {
|
|
1153
|
+
method: "POST",
|
|
1154
|
+
body: JSON.stringify(exactCommand),
|
|
1155
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1156
|
+
});
|
|
1157
|
+
if (!response.ok) {
|
|
1158
|
+
const body = await response.text();
|
|
1159
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1160
|
+
}
|
|
1161
|
+
const parsed = interactiveSessionPromptResponseSchema.safeParse(await response.json());
|
|
1162
|
+
if (!parsed.success) throw new ServerError("Interactive prompt acknowledgement is invalid", 502, {
|
|
1163
|
+
endpoint,
|
|
1164
|
+
origin: "runtime"
|
|
1165
|
+
});
|
|
1166
|
+
const acknowledgement = parsed.data.data;
|
|
1167
|
+
if (!agentInteractiveSessionPromptAcknowledgementMatchesCommand(exactCommand, acknowledgement)) throw new ServerError("Interactive prompt acknowledgement does not match the command", 502, {
|
|
1168
|
+
endpoint,
|
|
1169
|
+
origin: "runtime"
|
|
1170
|
+
});
|
|
1171
|
+
return acknowledgement;
|
|
1064
1172
|
}
|
|
1065
|
-
/**
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1173
|
+
/** @internal — invoked by InteractiveSessionHandle.claimControl(). */
|
|
1174
|
+
async _claimInteractiveControl(id, request, options) {
|
|
1175
|
+
options?.signal?.throwIfAborted();
|
|
1176
|
+
const exactRequest = AgentInteractiveSessionControlClaimRequestSchema.parse(request);
|
|
1177
|
+
if (exactRequest.ref.run.sessionId !== id) throw new ValidationError("Interactive control claim does not belong to the session");
|
|
1178
|
+
assertInteractiveRefBelongsToSandbox(exactRequest.ref, this.host.id);
|
|
1179
|
+
await this.host.ensureRunning(options?.signal);
|
|
1180
|
+
options?.signal?.throwIfAborted();
|
|
1181
|
+
const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive/control`;
|
|
1182
|
+
const response = await this.host.runtimeFetch(endpoint, {
|
|
1183
|
+
method: "POST",
|
|
1184
|
+
body: JSON.stringify(exactRequest),
|
|
1185
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1186
|
+
});
|
|
1187
|
+
if (!response.ok) {
|
|
1188
|
+
const body = await response.text();
|
|
1189
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1190
|
+
}
|
|
1191
|
+
const parsed = interactiveSessionControlClaimResponseSchema.safeParse(await response.json());
|
|
1192
|
+
if (!parsed.success) throw new ServerError("Interactive control claim acknowledgement is invalid", 502, {
|
|
1193
|
+
endpoint,
|
|
1194
|
+
origin: "runtime"
|
|
1195
|
+
});
|
|
1196
|
+
const acknowledgement = parsed.data.data;
|
|
1197
|
+
if (!agentInteractiveSessionControlClaimAcknowledgementMatchesRequest(exactRequest, acknowledgement)) throw new ServerError("Interactive control claim acknowledgement does not match the request", 502, {
|
|
1198
|
+
endpoint,
|
|
1199
|
+
origin: "runtime"
|
|
1200
|
+
});
|
|
1201
|
+
return acknowledgement;
|
|
1080
1202
|
}
|
|
1081
|
-
/**
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
if (!
|
|
1089
|
-
|
|
1203
|
+
/** @internal — invoked by the interactive attach controller on cleanup. */
|
|
1204
|
+
async _releaseInteractiveControl(id, ref, control, options) {
|
|
1205
|
+
options?.signal?.throwIfAborted();
|
|
1206
|
+
const exactRef = AgentInteractiveSessionRefSchema.parse(ref);
|
|
1207
|
+
const exactControl = AgentInteractiveSessionControlClaimSchema.parse(control);
|
|
1208
|
+
if (exactRef.run.sessionId !== id) throw new ValidationError("Interactive control release does not belong to the session");
|
|
1209
|
+
assertInteractiveRefBelongsToSandbox(exactRef, this.host.id);
|
|
1210
|
+
if (!agentInteractiveSessionControlClaimMatchesRef(exactRef, exactControl)) throw new ValidationError("Interactive control release does not match the session ref");
|
|
1211
|
+
const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive/control/release`;
|
|
1212
|
+
const response = await this.host.runtimeFetch(endpoint, {
|
|
1213
|
+
method: "POST",
|
|
1214
|
+
body: JSON.stringify({
|
|
1215
|
+
ref: exactRef,
|
|
1216
|
+
control: exactControl
|
|
1217
|
+
}),
|
|
1218
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1219
|
+
});
|
|
1220
|
+
if (!response.ok) {
|
|
1221
|
+
const body = await response.text();
|
|
1222
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1223
|
+
}
|
|
1224
|
+
if (!interactiveSessionControlReleaseResponseSchema.safeParse(await response.json()).success) throw new ServerError("Interactive control release response is invalid", 502, {
|
|
1225
|
+
endpoint,
|
|
1226
|
+
origin: "runtime"
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
/** @internal — invoked by InteractiveSessionHandle.status(). */
|
|
1230
|
+
async _interactiveStatus(id, incarnationId, options) {
|
|
1231
|
+
options?.signal?.throwIfAborted();
|
|
1232
|
+
await this.host.ensureRunning(options?.signal);
|
|
1233
|
+
const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive`;
|
|
1234
|
+
const response = await this.host.runtimeFetch(`${endpoint}?incarnationId=${encodeURIComponent(incarnationId)}`, {
|
|
1235
|
+
method: "GET",
|
|
1236
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1237
|
+
});
|
|
1238
|
+
if (response.status === 404) return null;
|
|
1239
|
+
if (!response.ok) {
|
|
1240
|
+
const body = await response.text();
|
|
1241
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1242
|
+
}
|
|
1243
|
+
const parsed = interactiveSessionStatusResponseSchema.safeParse(await response.json());
|
|
1244
|
+
if (!parsed.success) throw new ServerError("Interactive status response has an invalid exact identity", 502, {
|
|
1245
|
+
endpoint,
|
|
1246
|
+
origin: "runtime"
|
|
1247
|
+
});
|
|
1248
|
+
const status = parsed.data.data.status;
|
|
1249
|
+
if (status.ref.run.sessionId !== id || status.ref.incarnationId !== incarnationId) throw new ServerError("Interactive status response has an invalid exact identity", 502, {
|
|
1250
|
+
endpoint,
|
|
1251
|
+
origin: "runtime"
|
|
1252
|
+
});
|
|
1253
|
+
assertInteractiveRefBelongsToSandbox(status.ref, this.host.id);
|
|
1254
|
+
return status;
|
|
1255
|
+
}
|
|
1256
|
+
/** @internal — invoked by InteractiveSessionHandle.stop(). */
|
|
1257
|
+
async _stopInteractive(id, command, options) {
|
|
1258
|
+
options?.signal?.throwIfAborted();
|
|
1259
|
+
const exactCommand = AgentInteractiveSessionStopCommandSchema.parse(command);
|
|
1260
|
+
if (exactCommand.ref.run.sessionId !== id) throw new ValidationError("Interactive stop command does not belong to the session");
|
|
1261
|
+
assertInteractiveRefBelongsToSandbox(exactCommand.ref, this.host.id);
|
|
1262
|
+
await this.host.ensureRunning(options?.signal);
|
|
1263
|
+
const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive`;
|
|
1264
|
+
const response = await this.host.runtimeFetch(endpoint, {
|
|
1265
|
+
method: "DELETE",
|
|
1266
|
+
body: JSON.stringify(exactCommand),
|
|
1267
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1268
|
+
});
|
|
1269
|
+
if (!response.ok) {
|
|
1270
|
+
const body = await response.text();
|
|
1271
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1272
|
+
}
|
|
1273
|
+
const parsed = interactiveSessionStopResponseSchema.safeParse(await response.json());
|
|
1274
|
+
if (!parsed.success) throw new ServerError("Interactive stop acknowledgement is invalid", 502, {
|
|
1275
|
+
endpoint,
|
|
1276
|
+
origin: "runtime"
|
|
1277
|
+
});
|
|
1278
|
+
const acknowledgement = parsed.data.data;
|
|
1279
|
+
if (!agentInteractiveSessionStopAcknowledgementMatchesCommand(exactCommand, acknowledgement)) throw new ServerError("Interactive stop acknowledgement does not match the command", 502, {
|
|
1280
|
+
endpoint,
|
|
1281
|
+
origin: "runtime"
|
|
1282
|
+
});
|
|
1283
|
+
return acknowledgement;
|
|
1090
1284
|
}
|
|
1091
|
-
/**
|
|
1092
|
-
|
|
1093
|
-
|
|
1285
|
+
/** @internal — lifecycle authority for exact provider teardown. */
|
|
1286
|
+
async _stopInteractiveLifecycle(id, ref, options) {
|
|
1287
|
+
options?.signal?.throwIfAborted();
|
|
1288
|
+
const exactRef = AgentInteractiveSessionRefSchema.parse(ref);
|
|
1289
|
+
if (exactRef.run.sessionId !== id) throw new ValidationError("Interactive lifecycle stop reference does not belong to the session");
|
|
1290
|
+
assertInteractiveRefBelongsToSandbox(exactRef, this.host.id);
|
|
1291
|
+
await this.host.ensureRunning(options?.signal);
|
|
1292
|
+
const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive/lifecycle`;
|
|
1293
|
+
const response = await this.host.runtimeFetch(endpoint, {
|
|
1294
|
+
method: "DELETE",
|
|
1295
|
+
body: JSON.stringify({ ref: exactRef }),
|
|
1296
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1297
|
+
});
|
|
1298
|
+
if (!response.ok) {
|
|
1299
|
+
const body = await response.text();
|
|
1300
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1301
|
+
}
|
|
1302
|
+
const parsed = interactiveSessionStopResponseSchema.safeParse(await response.json());
|
|
1303
|
+
if (!parsed.success) throw new ServerError("Interactive lifecycle stop acknowledgement is invalid", 502, {
|
|
1304
|
+
endpoint,
|
|
1305
|
+
origin: "runtime"
|
|
1306
|
+
});
|
|
1307
|
+
const acknowledgement = parsed.data.data;
|
|
1308
|
+
if (canonicalCandidateDigest(acknowledgement.ref) !== canonicalCandidateDigest(exactRef)) throw new ServerError("Interactive lifecycle stop acknowledgement does not match the exact ref", 502, {
|
|
1309
|
+
endpoint,
|
|
1310
|
+
origin: "runtime"
|
|
1311
|
+
});
|
|
1312
|
+
return acknowledgement;
|
|
1094
1313
|
}
|
|
1095
1314
|
};
|
|
1096
1315
|
//#endregion
|
|
1097
|
-
//#region src/
|
|
1098
|
-
/**
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1316
|
+
//#region src/scoped-token.ts
|
|
1317
|
+
/** Maximum time for the exact interactive terminal-token request. */
|
|
1318
|
+
const SCOPED_TOKEN_REQUEST_TIMEOUT_MS = 5e3;
|
|
1319
|
+
const scopedTokenResponseSchema = z.object({
|
|
1320
|
+
token: z.string().min(1),
|
|
1321
|
+
expiresAt: z.number().finite().nonnegative(),
|
|
1322
|
+
runtimeUrl: z.string().min(1),
|
|
1323
|
+
sidecarProxyUrl: z.string().min(1),
|
|
1324
|
+
scope: z.enum([
|
|
1325
|
+
"session",
|
|
1326
|
+
"session-runtime",
|
|
1327
|
+
"project",
|
|
1328
|
+
"read-only"
|
|
1329
|
+
])
|
|
1330
|
+
});
|
|
1331
|
+
/** Reject a scoped credential that the SDK must not send to a consumer. */
|
|
1332
|
+
function assertScopedTokenActive(token, expectedScope) {
|
|
1333
|
+
if (expectedScope !== void 0 && token.scope !== expectedScope) throw new ValidationError(`Scoped token must use the ${expectedScope} scope`);
|
|
1334
|
+
if (typeof token.token !== "string" || !token.token.trim() || !(token.expiresAt instanceof Date) || !Number.isFinite(token.expiresAt.getTime()) || token.expiresAt.getTime() <= Date.now()) throw new ValidationError("Scoped token is expired or invalid");
|
|
1335
|
+
}
|
|
1336
|
+
function parseScopedTokenResponse(raw, expectedScope) {
|
|
1337
|
+
const parsed = scopedTokenResponseSchema.safeParse(raw);
|
|
1338
|
+
if (!parsed.success) throw new ServerError("Sandbox API returned an invalid scoped token response.", 502, { origin: "sandbox-api" });
|
|
1339
|
+
if (parsed.data.scope !== expectedScope) throw new ServerError(`Sandbox API returned a ${parsed.data.scope} token for a ${expectedScope} request.`, 502, { origin: "sandbox-api" });
|
|
1340
|
+
const token = {
|
|
1341
|
+
token: parsed.data.token,
|
|
1342
|
+
expiresAt: /* @__PURE__ */ new Date(parsed.data.expiresAt * 1e3),
|
|
1343
|
+
runtimeUrl: parsed.data.runtimeUrl,
|
|
1344
|
+
sidecarProxyUrl: parsed.data.sidecarProxyUrl,
|
|
1345
|
+
scope: parsed.data.scope
|
|
1346
|
+
};
|
|
1347
|
+
try {
|
|
1348
|
+
assertScopedTokenActive(token, expectedScope);
|
|
1349
|
+
} catch {
|
|
1350
|
+
throw new ServerError("Sandbox API returned an expired or invalid scoped token.", 502, { origin: "sandbox-api" });
|
|
1109
1351
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1352
|
+
return token;
|
|
1353
|
+
}
|
|
1354
|
+
async function requestScopedToken(client, sandboxId, options, requestOptions) {
|
|
1355
|
+
if ((options.scope === "session" || options.scope === "session-runtime") && !options.sessionId) throw new ValidationError(`${options.scope} scope requires a sessionId`);
|
|
1356
|
+
if (options.scope === "session" && !options.runtimeSessionId) throw new ValidationError("session scope requires the actual runtimeSessionId");
|
|
1357
|
+
const response = await client.fetch(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/scoped-token`, {
|
|
1358
|
+
method: "POST",
|
|
1359
|
+
headers: { "Content-Type": "application/json" },
|
|
1360
|
+
body: JSON.stringify({
|
|
1361
|
+
scope: options.scope,
|
|
1362
|
+
...options.sessionId ? { sessionId: options.sessionId } : {},
|
|
1363
|
+
...options.scope === "session" ? { runtimeSessionId: options.runtimeSessionId } : {},
|
|
1364
|
+
...options.ttlMinutes ? { ttlMinutes: options.ttlMinutes } : {}
|
|
1365
|
+
}),
|
|
1366
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1367
|
+
}, requestOptions);
|
|
1368
|
+
if (!response.ok) {
|
|
1369
|
+
const body = await response.text();
|
|
1370
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1120
1371
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
* The async iterator terminates when the session reaches a terminal
|
|
1127
|
-
* state (`completed`, `failed`, `cancelled`) and the corresponding
|
|
1128
|
-
* terminal event has been yielded, OR when the caller's signal aborts.
|
|
1129
|
-
*/
|
|
1130
|
-
events(opts) {
|
|
1131
|
-
return this.box._sessionEvents(this.id, opts);
|
|
1372
|
+
let raw;
|
|
1373
|
+
try {
|
|
1374
|
+
raw = await response.json();
|
|
1375
|
+
} catch {
|
|
1376
|
+
throw new ServerError("Sandbox API returned invalid scoped token JSON.", 502, { origin: "sandbox-api" });
|
|
1132
1377
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1378
|
+
return parseScopedTokenResponse(raw, options.scope);
|
|
1379
|
+
}
|
|
1380
|
+
function parseInteractiveSessionTokenRef(ref) {
|
|
1381
|
+
const parsed = AgentInteractiveSessionRefSchema.safeParse(ref);
|
|
1382
|
+
if (!parsed.success) throw new ValidationError("Interactive session ref is invalid.", { ref: parsed.error.issues[0]?.message ?? "invalid ref" });
|
|
1383
|
+
return parsed.data;
|
|
1384
|
+
}
|
|
1385
|
+
function interactiveSessionTokenOptions(ref, options = {}) {
|
|
1386
|
+
const exactRef = parseInteractiveSessionTokenRef(ref);
|
|
1387
|
+
return {
|
|
1388
|
+
sandboxId: exactRef.run.environmentId,
|
|
1389
|
+
request: {
|
|
1390
|
+
scope: "session-runtime",
|
|
1391
|
+
sessionId: exactRef.run.sessionId,
|
|
1392
|
+
...options.ttlMinutes === void 0 ? {} : { ttlMinutes: options.ttlMinutes },
|
|
1393
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function requestInteractiveSessionToken(client, ref, options) {
|
|
1398
|
+
const exact = interactiveSessionTokenOptions(ref, options);
|
|
1399
|
+
return requestScopedToken(client, exact.sandboxId, exact.request, { timeoutMs: SCOPED_TOKEN_REQUEST_TIMEOUT_MS });
|
|
1400
|
+
}
|
|
1401
|
+
//#endregion
|
|
1402
|
+
//#region src/sandbox-interactive-terminal.ts
|
|
1403
|
+
var SandboxInteractiveTerminalOperations = class {
|
|
1404
|
+
constructor(host) {
|
|
1405
|
+
this.host = host;
|
|
1143
1406
|
}
|
|
1144
1407
|
/**
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1408
|
+
* Resolve a sandbox-relative terminal stream path against the browser-safe
|
|
1409
|
+
* sidecar proxy base returned by the scoped-token mint. Concatenation, not
|
|
1410
|
+
* URL resolution: the proxy base carries a path prefix
|
|
1411
|
+
* (`/v1/sidecar-proxy/{id}`) that `new URL(absolutePath, base)` would drop.
|
|
1147
1412
|
*/
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
sessionId: this.id
|
|
1152
|
-
});
|
|
1413
|
+
resolveTerminalStreamUrl(sidecarProxyUrl, streamPath) {
|
|
1414
|
+
if (!streamPath.startsWith("/")) throw new ServerError(`Terminal stream path must be sandbox-relative (got "${streamPath}")`, 502, { origin: "runtime" });
|
|
1415
|
+
return `${sidecarProxyUrl.replace(/\/+$/, "")}${streamPath}`;
|
|
1153
1416
|
}
|
|
1154
|
-
/**
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1417
|
+
/** @internal — invoked by InteractiveSessionHandle.attach(). */
|
|
1418
|
+
async _attachInteractive(id, ref, request, options, reconnectAuthority) {
|
|
1419
|
+
options?.signal?.throwIfAborted();
|
|
1420
|
+
const exactRef = AgentInteractiveSessionRefSchema.parse(ref);
|
|
1421
|
+
if (exactRef.run.sessionId !== id) throw new ValidationError("Interactive attach ref does not belong to the session");
|
|
1422
|
+
assertInteractiveRefBelongsToSandbox(exactRef, this.host.id);
|
|
1423
|
+
const { handlers, ...attachRequest } = request;
|
|
1424
|
+
const exactRequest = AgentInteractiveSessionAttachSchema.parse(attachRequest);
|
|
1425
|
+
if (!agentInteractiveSessionControlClaimMatchesRef(exactRef, exactRequest.control)) throw new ValidationError("Interactive attach control does not match the session ref");
|
|
1426
|
+
const status = await this.host.interactiveStatus(id, exactRef.incarnationId, options);
|
|
1427
|
+
if (!status) throw new NotFoundError("Interactive session", id, {
|
|
1428
|
+
origin: "runtime",
|
|
1429
|
+
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
|
|
1430
|
+
});
|
|
1431
|
+
if (status.state !== "running") throw new StateError(`Interactive session ${id} has exited; there is no live PTY to attach`, status.state, "running", {
|
|
1432
|
+
origin: "runtime",
|
|
1433
|
+
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
|
|
1434
|
+
}, "INTERACTIVE_SESSION_EXITED");
|
|
1435
|
+
if (canonicalCandidateDigest(status.ref) !== canonicalCandidateDigest(exactRef)) throw new ValidationError("Interactive attach ref does not match the current session");
|
|
1436
|
+
const scoped = await this.host.mintScopedToken({
|
|
1437
|
+
scope: "session-runtime",
|
|
1438
|
+
sessionId: id,
|
|
1439
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
1440
|
+
});
|
|
1441
|
+
const stream = await this.host.terminalStreamConnect({
|
|
1442
|
+
url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, `/terminals/${encodeURIComponent(id)}/ws`),
|
|
1443
|
+
token: this.terminalTokenProvider(scoped, id),
|
|
1444
|
+
connectionId: id,
|
|
1445
|
+
cols: exactRequest.cols,
|
|
1446
|
+
rows: exactRequest.rows,
|
|
1447
|
+
incarnationId: exactRef.incarnationId,
|
|
1448
|
+
control: exactRequest.control,
|
|
1449
|
+
handlers,
|
|
1450
|
+
signal: options?.signal,
|
|
1451
|
+
...reconnectAuthority === void 0 ? {} : { reconnectAuthority }
|
|
1163
1452
|
});
|
|
1453
|
+
if (options?.signal?.aborted) {
|
|
1454
|
+
await stream.close();
|
|
1455
|
+
options.signal.throwIfAborted();
|
|
1456
|
+
}
|
|
1457
|
+
return stream;
|
|
1164
1458
|
}
|
|
1165
1459
|
/**
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
* are
|
|
1460
|
+
* Per-dial credential for a terminal attach. The first dial consumes the
|
|
1461
|
+
* token minted alongside the proxy URL (no second mint); every later dial
|
|
1462
|
+
* (a `reconnect()` after the socket dropped or the detach window) re-mints,
|
|
1463
|
+
* because scoped terminal tokens are clamped to a <= 15-minute TTL and the
|
|
1464
|
+
* original is likely dead by then.
|
|
1170
1465
|
*/
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1466
|
+
terminalTokenProvider(initialToken, sessionId) {
|
|
1467
|
+
let unused = initialToken;
|
|
1468
|
+
return async (signal) => {
|
|
1469
|
+
signal?.throwIfAborted();
|
|
1470
|
+
if (unused !== null) {
|
|
1471
|
+
const token = unused;
|
|
1472
|
+
unused = null;
|
|
1473
|
+
assertScopedTokenActive(token, "session-runtime");
|
|
1474
|
+
return token;
|
|
1475
|
+
}
|
|
1476
|
+
const scoped = await this.host.mintScopedToken({
|
|
1477
|
+
scope: "session-runtime",
|
|
1478
|
+
sessionId,
|
|
1479
|
+
signal
|
|
1480
|
+
});
|
|
1481
|
+
assertScopedTokenActive(scoped, "session-runtime");
|
|
1482
|
+
return scoped;
|
|
1483
|
+
};
|
|
1177
1484
|
}
|
|
1178
|
-
/**
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
return {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
*/
|
|
1196
|
-
interactive() {
|
|
1197
|
-
return new InteractiveSessionHandle(this.box, this.id);
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Resolve a pending permission interaction the agent raised mid-turn,
|
|
1201
|
-
* forwarding `allow`/`deny` to the blocked agent through the unified
|
|
1202
|
-
* interaction channel. Throws if no matching permission is outstanding.
|
|
1203
|
-
*/
|
|
1204
|
-
async respondToPermission(permissionID, options) {
|
|
1205
|
-
return this.box._respondToPermission(this.id, permissionID, options);
|
|
1206
|
-
}
|
|
1207
|
-
/** Return every interaction that still awaits a response on this session. */
|
|
1208
|
-
async interactions(options) {
|
|
1209
|
-
return this.box._listInteractions(this.id, options);
|
|
1210
|
-
}
|
|
1211
|
-
/** Respond to one exact interaction with a retry-safe command. */
|
|
1212
|
-
async respondToInteraction(command, options) {
|
|
1213
|
-
return this.box._respondToInteraction(this.id, command, options);
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Interrupt an execution without deleting the session. Pass `executionId`
|
|
1217
|
-
* to make the cancellation exact: a newer run on the same session is never
|
|
1218
|
-
* touched. Without it, the current execution is selected. Returns
|
|
1219
|
-
* `{ cancelled: false }` when the selected execution is not running.
|
|
1220
|
-
*/
|
|
1221
|
-
async interrupt(options) {
|
|
1222
|
-
return options ? this.box._interrupt(this.id, options) : this.box._interrupt(this.id);
|
|
1223
|
-
}
|
|
1224
|
-
/**
|
|
1225
|
-
* Cancel one exact run through the durable operation contract. Reusing the
|
|
1226
|
-
* same request after a lost response returns the sidecar's replayed
|
|
1227
|
-
* acknowledgement instead of issuing a second cancellation.
|
|
1228
|
-
*/
|
|
1229
|
-
async cancelRun(request, options) {
|
|
1230
|
-
return this.box._cancelRun(this.id, request, options);
|
|
1231
|
-
}
|
|
1232
|
-
/** Answer one exact question without selecting another outstanding ask. */
|
|
1233
|
-
async answer(interactionId, answers, options) {
|
|
1234
|
-
return this.box._answerQuestion(this.id, interactionId, answers, options);
|
|
1235
|
-
}
|
|
1236
|
-
/** Return the newest plan awaiting a decision for this session. */
|
|
1237
|
-
async plan() {
|
|
1238
|
-
return this.box._currentPlan(this.id);
|
|
1239
|
-
}
|
|
1240
|
-
/** Approve a durable plan and enqueue its separate execution turn. */
|
|
1241
|
-
async approvePlan(planId) {
|
|
1242
|
-
const id = planId ?? (await this.requireCurrentPlan()).id;
|
|
1243
|
-
return this.box._decidePlan(this.id, id, { outcome: "approved" });
|
|
1244
|
-
}
|
|
1245
|
-
/** Reject a durable plan with required feedback and enqueue a revision turn. */
|
|
1246
|
-
async rejectPlan(feedback, planId) {
|
|
1247
|
-
if (!feedback.trim()) throw new Error("Plan rejection feedback is required");
|
|
1248
|
-
const id = planId ?? (await this.requireCurrentPlan()).id;
|
|
1249
|
-
return this.box._decidePlan(this.id, id, {
|
|
1250
|
-
outcome: "rejected",
|
|
1251
|
-
feedback: feedback.trim()
|
|
1485
|
+
/** @internal — invoked by `box.terminals.attach()`. */
|
|
1486
|
+
async _attachTerminal(connectionId, options = {}) {
|
|
1487
|
+
if (!connectionId.trim()) throw new ValidationError("Terminal connectionId is required");
|
|
1488
|
+
const scoped = await this.host.mintScopedToken({
|
|
1489
|
+
scope: "session-runtime",
|
|
1490
|
+
sessionId: connectionId
|
|
1491
|
+
});
|
|
1492
|
+
return this.host.terminalStreamConnect({
|
|
1493
|
+
url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, `/terminals/${encodeURIComponent(connectionId)}/ws`),
|
|
1494
|
+
token: this.terminalTokenProvider(scoped, connectionId),
|
|
1495
|
+
connectionId,
|
|
1496
|
+
cols: options.cols,
|
|
1497
|
+
rows: options.rows,
|
|
1498
|
+
command: options.command,
|
|
1499
|
+
cwd: options.cwd,
|
|
1500
|
+
env: options.env,
|
|
1501
|
+
handlers: options.handlers
|
|
1252
1502
|
});
|
|
1253
|
-
}
|
|
1254
|
-
async requireCurrentPlan() {
|
|
1255
|
-
const plan = await this.plan();
|
|
1256
|
-
if (!plan) throw new Error("No durable plan is awaiting a decision");
|
|
1257
|
-
return plan;
|
|
1258
1503
|
}
|
|
1259
1504
|
};
|
|
1505
|
+
const TERMINAL_WS_MAX_DIMENSION = 4096;
|
|
1506
|
+
/** Input chunk size that leaves room for JSON escaping inside one frame. */
|
|
1507
|
+
const TERMINAL_WS_INPUT_FRAME_CHUNK_CHARS = 128 * 1024;
|
|
1508
|
+
/** WebSocket authentication envelope prefixes. */
|
|
1509
|
+
const TERMINAL_WS_EDGE_BEARER_SUBPROTOCOL_PREFIX = "bearer.";
|
|
1510
|
+
const TERMINAL_WS_SIDECAR_BEARER_SUBPROTOCOL_PREFIX = "tangle.bearer.";
|
|
1511
|
+
/** Non-credential protocol selected during a successful terminal handshake. */
|
|
1512
|
+
const TERMINAL_WS_ECHO_SUBPROTOCOL = "tangle.terminal.v1";
|
|
1513
|
+
const terminalGeometry = {
|
|
1514
|
+
cols: z.number().int().min(1).max(TERMINAL_WS_MAX_DIMENSION),
|
|
1515
|
+
rows: z.number().int().min(1).max(TERMINAL_WS_MAX_DIMENSION)
|
|
1516
|
+
};
|
|
1517
|
+
const terminalInitMessageSchema = z.object({
|
|
1518
|
+
type: z.literal("init"),
|
|
1519
|
+
connectionId: z.string().optional(),
|
|
1520
|
+
/** Required for an interactive-agent PTY; absent for generic terminals. */
|
|
1521
|
+
incarnationId: z.string().uuid().optional(),
|
|
1522
|
+
...terminalGeometry,
|
|
1523
|
+
command: z.string().min(1).optional(),
|
|
1524
|
+
cwd: z.string().min(1).optional(),
|
|
1525
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
1526
|
+
sessionId: z.string().min(1).optional(),
|
|
1527
|
+
/** Canonical control claim for an interactive-agent PTY. */
|
|
1528
|
+
control: AgentInteractiveSessionControlClaimSchema.optional()
|
|
1529
|
+
}).superRefine((message, refinement) => {
|
|
1530
|
+
if (message.incarnationId !== void 0 && message.control === void 0) refinement.addIssue({
|
|
1531
|
+
code: "custom",
|
|
1532
|
+
path: ["control"],
|
|
1533
|
+
message: "Interactive session attach requires a control claim"
|
|
1534
|
+
});
|
|
1535
|
+
if (message.incarnationId === void 0 && message.control !== void 0) refinement.addIssue({
|
|
1536
|
+
code: "custom",
|
|
1537
|
+
path: ["control"],
|
|
1538
|
+
message: "Generic terminal attach cannot carry an interactive control claim"
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
const terminalInputMessageSchema = z.object({
|
|
1542
|
+
type: z.literal("input"),
|
|
1543
|
+
data: z.string()
|
|
1544
|
+
});
|
|
1545
|
+
const terminalResizeMessageSchema = z.object({
|
|
1546
|
+
type: z.literal("resize"),
|
|
1547
|
+
...terminalGeometry
|
|
1548
|
+
});
|
|
1549
|
+
const terminalRenewMessageSchema = z.object({ type: z.literal("renew") });
|
|
1550
|
+
const terminalCloseMessageSchema = z.object({ type: z.literal("close") });
|
|
1551
|
+
z.union([
|
|
1552
|
+
terminalInitMessageSchema,
|
|
1553
|
+
terminalInputMessageSchema,
|
|
1554
|
+
terminalResizeMessageSchema,
|
|
1555
|
+
terminalRenewMessageSchema,
|
|
1556
|
+
terminalCloseMessageSchema
|
|
1557
|
+
]);
|
|
1558
|
+
const terminalReadyMessageSchema = z.object({
|
|
1559
|
+
type: z.literal("ready"),
|
|
1560
|
+
connectionId: z.string(),
|
|
1561
|
+
sessionId: z.string(),
|
|
1562
|
+
restored: z.boolean(),
|
|
1563
|
+
detachTimeoutMs: z.number().int().positive()
|
|
1564
|
+
});
|
|
1565
|
+
const terminalExitMessageSchema = z.object({
|
|
1566
|
+
type: z.literal("exit"),
|
|
1567
|
+
exitCode: z.number().optional(),
|
|
1568
|
+
exitSignal: z.string().optional()
|
|
1569
|
+
});
|
|
1570
|
+
const terminalRenewedMessageSchema = z.object({
|
|
1571
|
+
type: z.literal("renewed"),
|
|
1572
|
+
sessionId: z.string()
|
|
1573
|
+
});
|
|
1574
|
+
const terminalErrorMessageSchema = z.object({
|
|
1575
|
+
type: z.literal("error"),
|
|
1576
|
+
code: z.string(),
|
|
1577
|
+
message: z.string()
|
|
1578
|
+
});
|
|
1579
|
+
const terminalServerMessageSchema = z.union([
|
|
1580
|
+
terminalReadyMessageSchema,
|
|
1581
|
+
terminalExitMessageSchema,
|
|
1582
|
+
terminalRenewedMessageSchema,
|
|
1583
|
+
terminalErrorMessageSchema
|
|
1584
|
+
]);
|
|
1260
1585
|
//#endregion
|
|
1261
|
-
//#region src/
|
|
1262
|
-
/** hono's default unmatched-route body. */
|
|
1263
|
-
const HONO_NOT_FOUND_BODY = "404 Not Found";
|
|
1586
|
+
//#region src/terminal-stream.ts
|
|
1264
1587
|
/**
|
|
1265
|
-
*
|
|
1588
|
+
* TerminalStream — typed client for the sandbox terminal WebSocket protocol.
|
|
1266
1589
|
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*/
|
|
1273
|
-
function isRouterNotFound(body) {
|
|
1274
|
-
return body === HONO_NOT_FOUND_BODY;
|
|
1275
|
-
}
|
|
1276
|
-
/**
|
|
1277
|
-
* Request a TEE surface, refusing by name when the deployment cannot serve it.
|
|
1590
|
+
* Drives a live PTY session (an interactive harness TUI or a plain shell)
|
|
1591
|
+
* from the caller's own program: binary frames stream raw PTY output bytes,
|
|
1592
|
+
* TEXT JSON frames carry the control protocol. The browser-safe wire contract
|
|
1593
|
+
* is defined by the shared browser-safe runtime contract and shared with the
|
|
1594
|
+
* sidecar:
|
|
1278
1595
|
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1596
|
+
* client → server (TEXT JSON):
|
|
1597
|
+
* init {connectionId?, cols, rows, command?, cwd?, env?} — MUST be first
|
|
1598
|
+
* input {data}
|
|
1599
|
+
* resize {cols, rows}
|
|
1600
|
+
*
|
|
1601
|
+
* Interactive-agent init frames add the exact process authority:
|
|
1602
|
+
* {incarnationId, control: {refDigest, generation, leaseId, holderId, expiresAt}}
|
|
1603
|
+
* Subsequent input and resize frames carry only their operation fields; the
|
|
1604
|
+
* server binds authority to the init for the socket lifetime.
|
|
1605
|
+
* server → client (TEXT JSON):
|
|
1606
|
+
* ready {connectionId, sessionId, restored, detachTimeoutMs}
|
|
1607
|
+
* exit {exitCode?, exitSignal?}
|
|
1608
|
+
* error {code, message}
|
|
1609
|
+
* rec / rec.done — recording tee frames for the platform's session
|
|
1610
|
+
* capture consumer; not part of the client surface
|
|
1611
|
+
* server → client (BINARY): raw PTY output bytes
|
|
1612
|
+
*
|
|
1613
|
+
* Detach/reconnect: the server keeps the PTY alive for `detachTimeoutMs`
|
|
1614
|
+
* (5 minutes today) after the socket closes; re-dialing with the SAME
|
|
1615
|
+
* connectionId reattaches and replays the buffered output history, so a
|
|
1616
|
+
* fresh client repaints the current screen.
|
|
1617
|
+
*
|
|
1618
|
+
* Browser-safe: uses the global `WebSocket` (browsers, Workers, Node >= 22)
|
|
1619
|
+
* and never imports `ws` — the same environment contract as
|
|
1620
|
+
* `session-gateway/client.ts`.
|
|
1282
1621
|
*/
|
|
1283
|
-
async function fetchTeeSurface(client, sandboxId, surface, init) {
|
|
1284
|
-
const path = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/${surface}`;
|
|
1285
|
-
const response = await client.fetch(path, init);
|
|
1286
|
-
if (response.ok) return response;
|
|
1287
|
-
const body = await response.text();
|
|
1288
|
-
if (response.status === 404 && isRouterNotFound(body)) throw new CapabilityError(`TEE ${surface} is not served by this deployment. The /tee paths are answered by a Tangle confidential operator that intercepts them before the sandbox API; a sandbox reached through the public API has no such route. Point the client at the operator, or create the sandbox with driver.type "tangle" on a deployment that fronts it.`, "TEE_UNSUPPORTED", 404, void 0, "tee");
|
|
1289
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1290
|
-
}
|
|
1291
|
-
//#endregion
|
|
1292
|
-
//#region src/task-session.ts
|
|
1293
|
-
/** A background task session with isolated changeset operations. */
|
|
1294
|
-
var SandboxTaskSession = class extends SandboxSession {
|
|
1295
|
-
/** @internal Apps should call `box.taskSession(id)`. */
|
|
1296
|
-
constructor(taskBox, id) {
|
|
1297
|
-
super(taskBox, id);
|
|
1298
|
-
this.taskBox = taskBox;
|
|
1299
|
-
}
|
|
1300
|
-
/** Read the task's isolated file changes. */
|
|
1301
|
-
async changes() {
|
|
1302
|
-
return this.taskBox._taskSessionChanges(this.id);
|
|
1303
|
-
}
|
|
1304
|
-
/** Apply all or a selected subset of the task's isolated file changes. */
|
|
1305
|
-
async commit(options) {
|
|
1306
|
-
return this.taskBox._taskSessionCommit(this.id, options);
|
|
1307
|
-
}
|
|
1308
|
-
};
|
|
1309
|
-
//#endregion
|
|
1310
|
-
//#region src/terminal-stream.ts
|
|
1311
1622
|
/** A protocol `error` control frame or a transport failure. */
|
|
1312
1623
|
var TerminalStreamError = class extends Error {
|
|
1313
1624
|
/** Protocol error code (e.g. `INIT_FAILED`) or `TRANSPORT` for socket faults. */
|
|
@@ -1319,38 +1630,17 @@ var TerminalStreamError = class extends Error {
|
|
|
1319
1630
|
}
|
|
1320
1631
|
};
|
|
1321
1632
|
/**
|
|
1322
|
-
* The sidecar caps WebSocket frames at 1 MiB
|
|
1323
|
-
* `
|
|
1633
|
+
* The sidecar caps WebSocket frames at 1 MiB
|
|
1634
|
+
* (`TERMINAL_WS_MAX_PAYLOAD_BYTES` in the shared runtime contract) and drops
|
|
1635
|
+
* the connection on
|
|
1324
1636
|
* larger frames. `write()` chunks input so every serialized `input` frame
|
|
1325
1637
|
* stays under that cap: 128 Ki characters per chunk × a worst-case JSON
|
|
1326
1638
|
* escape expansion of 6 bytes per character (`` etc.) ≈ 768 KiB,
|
|
1327
1639
|
* comfortably below the limit including the frame's JSON envelope.
|
|
1328
1640
|
*/
|
|
1329
|
-
const INPUT_FRAME_CHUNK_CHARS = 128 * 1024;
|
|
1330
1641
|
const DEFAULT_COLS = 80;
|
|
1331
1642
|
const DEFAULT_ROWS = 24;
|
|
1332
1643
|
const DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
|
|
1333
|
-
/** Geometry bounds mirrored from the sidecar's init/resize schemas. */
|
|
1334
|
-
const MIN_DIMENSION = 1;
|
|
1335
|
-
const MAX_DIMENSION = 4096;
|
|
1336
|
-
const EDGE_BEARER_SUBPROTOCOL_PREFIX = "bearer.";
|
|
1337
|
-
const SIDECAR_BEARER_SUBPROTOCOL_PREFIX = "tangle.bearer.";
|
|
1338
|
-
/**
|
|
1339
|
-
* Fixed non-credential marker offered on EVERY dial (both auth formats),
|
|
1340
|
-
* giving a spec-compliant server something safe to select into the 101's
|
|
1341
|
-
* `Sec-WebSocket-Protocol`. Two reasons it exists:
|
|
1342
|
-
*
|
|
1343
|
-
* 1. RFC 6455 §4.1 says a client that offered subprotocols MUST fail the
|
|
1344
|
-
* connection when the server selects none. Node's WebSocket (undici,
|
|
1345
|
-
* per the WHATWG spec) enforces this strictly; browsers are lenient and
|
|
1346
|
-
* tolerate a 101 with no selected subprotocol — which is why the
|
|
1347
|
-
* browser-only reference client got away with offering just the
|
|
1348
|
-
* credential.
|
|
1349
|
-
* 2. The server must never echo the token-bearing offer back (that would
|
|
1350
|
-
* leak the live credential into a response header) — so it needs a
|
|
1351
|
-
* non-credential protocol it CAN select.
|
|
1352
|
-
*/
|
|
1353
|
-
const TERMINAL_WS_ECHO_SUBPROTOCOL = "tangle.terminal.v1";
|
|
1354
1644
|
/**
|
|
1355
1645
|
* base64url-encode a UTF-8 string without padding — the exact envelope the
|
|
1356
1646
|
* edge's `parseTerminalWsSubprotocolToken` decodes (its strict round-trip
|
|
@@ -1364,19 +1654,56 @@ function base64UrlEncodeUtf8(value) {
|
|
|
1364
1654
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1365
1655
|
}
|
|
1366
1656
|
function buildAuthSubprotocols(format, token) {
|
|
1367
|
-
if (format === "sidecar-direct") return [`${
|
|
1368
|
-
return [`${
|
|
1657
|
+
if (format === "sidecar-direct") return [`${TERMINAL_WS_SIDECAR_BEARER_SUBPROTOCOL_PREFIX}${token}`, TERMINAL_WS_ECHO_SUBPROTOCOL];
|
|
1658
|
+
return [`${TERMINAL_WS_EDGE_BEARER_SUBPROTOCOL_PREFIX}${base64UrlEncodeUtf8(token)}`, TERMINAL_WS_ECHO_SUBPROTOCOL];
|
|
1369
1659
|
}
|
|
1370
1660
|
/**
|
|
1371
1661
|
* Resolve the dial credential: invoke a provider (fresh token per dial) or
|
|
1372
1662
|
* pass a string through. Fails loud on an empty result — a blank credential
|
|
1373
1663
|
* would dial and be rejected with an opaque 401 downstream.
|
|
1374
1664
|
*/
|
|
1375
|
-
async function resolveTerminalToken(token) {
|
|
1376
|
-
const value = typeof token === "function" ? await token() : token;
|
|
1665
|
+
async function resolveTerminalToken(token, signal) {
|
|
1666
|
+
const value = typeof token === "function" ? await token(signal) : token;
|
|
1667
|
+
if (typeof value === "object" && value !== null) {
|
|
1668
|
+
try {
|
|
1669
|
+
assertScopedTokenActive(value, "session-runtime");
|
|
1670
|
+
} catch {
|
|
1671
|
+
throw new TerminalStreamError("Terminal stream scoped token is expired or invalid", "INVALID_TOKEN");
|
|
1672
|
+
}
|
|
1673
|
+
return value.token;
|
|
1674
|
+
}
|
|
1377
1675
|
if (typeof value !== "string" || !value.trim()) throw new TerminalStreamError(typeof token === "function" ? "Terminal stream token provider returned an empty token" : "Terminal stream token must not be empty", "INVALID_TOKEN");
|
|
1378
1676
|
return value;
|
|
1379
1677
|
}
|
|
1678
|
+
function terminalAbortReason(signal) {
|
|
1679
|
+
return signal.reason instanceof Error ? signal.reason : new DOMException("Terminal attach was aborted", "AbortError");
|
|
1680
|
+
}
|
|
1681
|
+
function awaitTerminalOperation(promise, signal) {
|
|
1682
|
+
if (!signal) return promise;
|
|
1683
|
+
signal.throwIfAborted();
|
|
1684
|
+
return new Promise((resolve, reject) => {
|
|
1685
|
+
let settled = false;
|
|
1686
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1687
|
+
const onAbort = () => {
|
|
1688
|
+
if (settled) return;
|
|
1689
|
+
settled = true;
|
|
1690
|
+
cleanup();
|
|
1691
|
+
reject(terminalAbortReason(signal));
|
|
1692
|
+
};
|
|
1693
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1694
|
+
promise.then((value) => {
|
|
1695
|
+
if (settled) return;
|
|
1696
|
+
settled = true;
|
|
1697
|
+
cleanup();
|
|
1698
|
+
resolve(value);
|
|
1699
|
+
}, (cause) => {
|
|
1700
|
+
if (settled) return;
|
|
1701
|
+
settled = true;
|
|
1702
|
+
cleanup();
|
|
1703
|
+
reject(cause);
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1380
1707
|
function toWebSocketUrl(url) {
|
|
1381
1708
|
let parsed;
|
|
1382
1709
|
try {
|
|
@@ -1390,7 +1717,48 @@ function toWebSocketUrl(url) {
|
|
|
1390
1717
|
return parsed.toString();
|
|
1391
1718
|
}
|
|
1392
1719
|
function assertDimension(name, value) {
|
|
1393
|
-
if (!Number.isInteger(value) || value <
|
|
1720
|
+
if (!Number.isInteger(value) || value < 1 || value > 4096) throw new TerminalStreamError(`${name} must be an integer in [1, ${TERMINAL_WS_MAX_DIMENSION}], got ${value}`, "INVALID_DIMENSION");
|
|
1721
|
+
}
|
|
1722
|
+
function normalizeInteractiveSessionIdentity(incarnationId, control) {
|
|
1723
|
+
if (incarnationId === void 0 && control === void 0) return void 0;
|
|
1724
|
+
if (typeof incarnationId !== "string" || incarnationId.length === 0 || incarnationId.trim() !== incarnationId) throw new TerminalStreamError("Interactive terminal attach requires an exact incarnationId", "INVALID_INTERACTIVE_IDENTITY");
|
|
1725
|
+
if (control === void 0) throw new TerminalStreamError("Interactive terminal attach requires an exact control claim", "INVALID_INTERACTIVE_CONTROL");
|
|
1726
|
+
const parsedControl = AgentInteractiveSessionControlClaimSchema.safeParse(control);
|
|
1727
|
+
if (!parsedControl.success) throw new TerminalStreamError("Interactive terminal attach requires an exact control claim", "INVALID_INTERACTIVE_CONTROL");
|
|
1728
|
+
const expiresAt = Date.parse(parsedControl.data.expiresAt);
|
|
1729
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) throw new TerminalStreamError("Interactive terminal control claim is expired", "INTERACTIVE_CONTROL_EXPIRED");
|
|
1730
|
+
return {
|
|
1731
|
+
incarnationId,
|
|
1732
|
+
control: parsedControl.data
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
function interactiveInitFields(identity) {
|
|
1736
|
+
if (identity === void 0) return {};
|
|
1737
|
+
return {
|
|
1738
|
+
incarnationId: identity.incarnationId,
|
|
1739
|
+
control: identity.control
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function buildTerminalInitMessage(options, identity) {
|
|
1743
|
+
if (typeof options.connectionId !== "string" || !options.connectionId.trim()) throw new TerminalStreamError("Terminal stream connectionId must not be empty", "INVALID_CONNECTION_ID");
|
|
1744
|
+
if (options.cols !== void 0) assertDimension("cols", options.cols);
|
|
1745
|
+
if (options.rows !== void 0) assertDimension("rows", options.rows);
|
|
1746
|
+
const parsed = terminalInitMessageSchema.safeParse({
|
|
1747
|
+
type: "init",
|
|
1748
|
+
connectionId: options.connectionId,
|
|
1749
|
+
cols: options.cols ?? DEFAULT_COLS,
|
|
1750
|
+
rows: options.rows ?? DEFAULT_ROWS,
|
|
1751
|
+
...options.command !== void 0 ? { command: options.command } : {},
|
|
1752
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1753
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
1754
|
+
...interactiveInitFields(identity)
|
|
1755
|
+
});
|
|
1756
|
+
if (!parsed.success) throw new TerminalStreamError("Terminal stream init does not match the shared terminal protocol", "INVALID_INIT");
|
|
1757
|
+
return parsed.data;
|
|
1758
|
+
}
|
|
1759
|
+
function assertInteractiveControlActive(identity) {
|
|
1760
|
+
if (identity === void 0) return;
|
|
1761
|
+
if (Date.parse(identity.control.expiresAt) <= Date.now()) throw new TerminalStreamError("Interactive terminal control claim is expired", "INTERACTIVE_CONTROL_EXPIRED");
|
|
1394
1762
|
}
|
|
1395
1763
|
/**
|
|
1396
1764
|
* Split input so each serialized `input` frame stays under the server's
|
|
@@ -1398,13 +1766,13 @@ function assertDimension(name, value) {
|
|
|
1398
1766
|
* pair would reach the PTY as two invalid UTF-8 writes).
|
|
1399
1767
|
*/
|
|
1400
1768
|
function* chunkInput(text) {
|
|
1401
|
-
if (text.length <=
|
|
1769
|
+
if (text.length <= 131072) {
|
|
1402
1770
|
yield text;
|
|
1403
1771
|
return;
|
|
1404
1772
|
}
|
|
1405
1773
|
let index = 0;
|
|
1406
1774
|
while (index < text.length) {
|
|
1407
|
-
let end = Math.min(index +
|
|
1775
|
+
let end = Math.min(index + TERMINAL_WS_INPUT_FRAME_CHUNK_CHARS, text.length);
|
|
1408
1776
|
if (end < text.length) {
|
|
1409
1777
|
const trailing = text.charCodeAt(end - 1);
|
|
1410
1778
|
if (trailing >= 55296 && trailing <= 56319) end -= 1;
|
|
@@ -1422,11 +1790,14 @@ function* chunkInput(text) {
|
|
|
1422
1790
|
var TerminalStream = class TerminalStream {
|
|
1423
1791
|
ws;
|
|
1424
1792
|
options;
|
|
1793
|
+
interactiveIdentity;
|
|
1794
|
+
initMessage;
|
|
1425
1795
|
handlers;
|
|
1426
1796
|
readyInfo = null;
|
|
1427
1797
|
closed = false;
|
|
1428
1798
|
closeWaiters = [];
|
|
1429
1799
|
textDecoder = new TextDecoder();
|
|
1800
|
+
reconnectInFlight;
|
|
1430
1801
|
/**
|
|
1431
1802
|
* Dial the endpoint, send `init` as the first frame, and resolve once the
|
|
1432
1803
|
* server responds `ready`. Rejects on a protocol `error` frame, a socket
|
|
@@ -1436,20 +1807,32 @@ var TerminalStream = class TerminalStream {
|
|
|
1436
1807
|
* fresh credential.
|
|
1437
1808
|
*/
|
|
1438
1809
|
static async connect(options) {
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1810
|
+
const interactiveIdentity = normalizeInteractiveSessionIdentity(options.incarnationId, options.control);
|
|
1811
|
+
options.signal?.throwIfAborted();
|
|
1812
|
+
const initMessage = buildTerminalInitMessage(options, interactiveIdentity);
|
|
1813
|
+
const token = await awaitTerminalOperation(resolveTerminalToken(options.token, options.signal), options.signal);
|
|
1814
|
+
options.signal?.throwIfAborted();
|
|
1815
|
+
return new TerminalStream(options, token, interactiveIdentity, initMessage).establish(options.signal);
|
|
1816
|
+
}
|
|
1817
|
+
constructor(options, resolvedToken, interactiveIdentity, initMessage) {
|
|
1818
|
+
const exactOptions = interactiveIdentity === void 0 ? options : {
|
|
1819
|
+
...options,
|
|
1820
|
+
incarnationId: interactiveIdentity.incarnationId,
|
|
1821
|
+
control: interactiveIdentity.control
|
|
1822
|
+
};
|
|
1445
1823
|
const WebSocketCtor = globalThis.WebSocket;
|
|
1446
1824
|
if (typeof WebSocketCtor !== "function") throw new TerminalStreamError("global WebSocket is unavailable in this environment (Node >= 22 or a browser is required)", "ENVIRONMENT");
|
|
1447
|
-
this.options =
|
|
1448
|
-
|
|
1449
|
-
|
|
1825
|
+
this.options = {
|
|
1826
|
+
...exactOptions,
|
|
1827
|
+
signal: void 0
|
|
1828
|
+
};
|
|
1829
|
+
this.interactiveIdentity = interactiveIdentity;
|
|
1830
|
+
this.initMessage = initMessage;
|
|
1831
|
+
this.handlers = exactOptions.handlers ?? {};
|
|
1832
|
+
this.ws = new WebSocketCtor(toWebSocketUrl(exactOptions.url), buildAuthSubprotocols(exactOptions.authFormat ?? "edge", resolvedToken));
|
|
1450
1833
|
this.ws.binaryType = "arraybuffer";
|
|
1451
1834
|
}
|
|
1452
|
-
establish() {
|
|
1835
|
+
establish(signal) {
|
|
1453
1836
|
return new Promise((resolve, reject) => {
|
|
1454
1837
|
let settled = false;
|
|
1455
1838
|
const timeoutMs = this.options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
@@ -1457,28 +1840,41 @@ var TerminalStream = class TerminalStream {
|
|
|
1457
1840
|
fail(new TerminalStreamError(`Terminal attach timed out after ${timeoutMs}ms waiting for ready`, "CONNECT_TIMEOUT"));
|
|
1458
1841
|
this.ws.close(1e3, "connect timeout");
|
|
1459
1842
|
}, timeoutMs);
|
|
1843
|
+
const cleanup = () => {
|
|
1844
|
+
clearTimeout(timer);
|
|
1845
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1846
|
+
};
|
|
1460
1847
|
const succeed = () => {
|
|
1461
1848
|
if (settled) return;
|
|
1462
1849
|
settled = true;
|
|
1463
|
-
|
|
1850
|
+
cleanup();
|
|
1464
1851
|
resolve(this);
|
|
1465
1852
|
};
|
|
1466
1853
|
const fail = (error) => {
|
|
1467
1854
|
if (settled) return;
|
|
1468
1855
|
settled = true;
|
|
1469
|
-
|
|
1856
|
+
cleanup();
|
|
1470
1857
|
reject(error);
|
|
1471
1858
|
};
|
|
1859
|
+
const onAbort = () => {
|
|
1860
|
+
if (!signal) return;
|
|
1861
|
+
fail(terminalAbortReason(signal));
|
|
1862
|
+
this.ws.close(1e3, "connect aborted");
|
|
1863
|
+
};
|
|
1864
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1865
|
+
if (signal?.aborted) {
|
|
1866
|
+
onAbort();
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1472
1869
|
this.ws.addEventListener("open", () => {
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
}));
|
|
1870
|
+
if (settled) return;
|
|
1871
|
+
try {
|
|
1872
|
+
assertInteractiveControlActive(this.interactiveIdentity);
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
fail(error instanceof TerminalStreamError ? error : new TerminalStreamError("Interactive terminal control claim is expired", "INTERACTIVE_CONTROL_EXPIRED"));
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
this.ws.send(JSON.stringify(this.initMessage));
|
|
1482
1878
|
});
|
|
1483
1879
|
this.ws.addEventListener("message", (event) => {
|
|
1484
1880
|
this.handleMessage(event.data, {
|
|
@@ -1499,25 +1895,44 @@ var TerminalStream = class TerminalStream {
|
|
|
1499
1895
|
});
|
|
1500
1896
|
});
|
|
1501
1897
|
}
|
|
1502
|
-
handleMessage(
|
|
1503
|
-
if (typeof
|
|
1504
|
-
if (
|
|
1898
|
+
handleMessage(raw, connectPhase) {
|
|
1899
|
+
if (typeof raw !== "string") {
|
|
1900
|
+
if (raw instanceof ArrayBuffer) this.handlers.onData?.(new Uint8Array(raw));
|
|
1505
1901
|
return;
|
|
1506
1902
|
}
|
|
1507
|
-
|
|
1903
|
+
const failProtocolFrame = () => {
|
|
1904
|
+
const error = new TerminalStreamError("Terminal control frame does not match the shared terminal protocol", "INVALID_FRAME");
|
|
1905
|
+
const beforeReady = !connectPhase.settled();
|
|
1906
|
+
if (this.ws.readyState === WebSocket.OPEN) this.ws.close(1e3, "invalid terminal frame");
|
|
1907
|
+
if (beforeReady) connectPhase.fail(error);
|
|
1908
|
+
else this.handlers.onError?.(error);
|
|
1909
|
+
};
|
|
1910
|
+
let decoded;
|
|
1508
1911
|
try {
|
|
1509
|
-
|
|
1912
|
+
decoded = JSON.parse(raw);
|
|
1510
1913
|
} catch {
|
|
1511
|
-
|
|
1914
|
+
failProtocolFrame();
|
|
1512
1915
|
return;
|
|
1513
1916
|
}
|
|
1514
|
-
|
|
1917
|
+
if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded) || typeof decoded.type !== "string") {
|
|
1918
|
+
failProtocolFrame();
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const message = decoded;
|
|
1922
|
+
if (message.type === "rec" || message.type === "rec.done") return;
|
|
1923
|
+
if (message.type !== "ready" && message.type !== "exit" && message.type !== "renewed" && message.type !== "error") return;
|
|
1924
|
+
const parsed = terminalServerMessageSchema.safeParse(message);
|
|
1925
|
+
if (!parsed.success) {
|
|
1926
|
+
failProtocolFrame();
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
switch (parsed.data.type) {
|
|
1515
1930
|
case "ready": {
|
|
1516
1931
|
const info = {
|
|
1517
|
-
connectionId:
|
|
1518
|
-
sessionId:
|
|
1519
|
-
restored:
|
|
1520
|
-
detachTimeoutMs:
|
|
1932
|
+
connectionId: parsed.data.connectionId,
|
|
1933
|
+
sessionId: parsed.data.sessionId,
|
|
1934
|
+
restored: parsed.data.restored,
|
|
1935
|
+
detachTimeoutMs: parsed.data.detachTimeoutMs
|
|
1521
1936
|
};
|
|
1522
1937
|
this.readyInfo = info;
|
|
1523
1938
|
connectPhase.succeed();
|
|
@@ -1526,12 +1941,13 @@ var TerminalStream = class TerminalStream {
|
|
|
1526
1941
|
}
|
|
1527
1942
|
case "exit":
|
|
1528
1943
|
this.handlers.onExit?.({
|
|
1529
|
-
exitCode:
|
|
1530
|
-
exitSignal:
|
|
1944
|
+
exitCode: parsed.data.exitCode,
|
|
1945
|
+
exitSignal: parsed.data.exitSignal
|
|
1531
1946
|
});
|
|
1532
1947
|
return;
|
|
1948
|
+
case "renewed": return;
|
|
1533
1949
|
case "error": {
|
|
1534
|
-
const error = new TerminalStreamError(
|
|
1950
|
+
const error = new TerminalStreamError(parsed.data.message, parsed.data.code);
|
|
1535
1951
|
if (!connectPhase.settled()) {
|
|
1536
1952
|
connectPhase.fail(error);
|
|
1537
1953
|
return;
|
|
@@ -1539,9 +1955,6 @@ var TerminalStream = class TerminalStream {
|
|
|
1539
1955
|
this.handlers.onError?.(error);
|
|
1540
1956
|
return;
|
|
1541
1957
|
}
|
|
1542
|
-
case "rec":
|
|
1543
|
-
case "rec.done": return;
|
|
1544
|
-
default: return;
|
|
1545
1958
|
}
|
|
1546
1959
|
}
|
|
1547
1960
|
/** PTY connection id this stream is attached to. */
|
|
@@ -1565,6 +1978,7 @@ var TerminalStream = class TerminalStream {
|
|
|
1565
1978
|
*/
|
|
1566
1979
|
write(data) {
|
|
1567
1980
|
this.assertOpen("write");
|
|
1981
|
+
assertInteractiveControlActive(this.interactiveIdentity);
|
|
1568
1982
|
const text = typeof data === "string" ? data : this.textDecoder.decode(data);
|
|
1569
1983
|
for (const chunk of chunkInput(text)) this.ws.send(JSON.stringify({
|
|
1570
1984
|
type: "input",
|
|
@@ -1576,6 +1990,7 @@ var TerminalStream = class TerminalStream {
|
|
|
1576
1990
|
assertDimension("cols", cols);
|
|
1577
1991
|
assertDimension("rows", rows);
|
|
1578
1992
|
this.assertOpen("resize");
|
|
1993
|
+
assertInteractiveControlActive(this.interactiveIdentity);
|
|
1579
1994
|
this.ws.send(JSON.stringify({
|
|
1580
1995
|
type: "resize",
|
|
1581
1996
|
cols,
|
|
@@ -1595,26 +2010,568 @@ var TerminalStream = class TerminalStream {
|
|
|
1595
2010
|
this.ws.close(1e3, "client detach");
|
|
1596
2011
|
});
|
|
1597
2012
|
}
|
|
1598
|
-
/**
|
|
1599
|
-
* Re-dial the same endpoint with the same connectionId on a fresh socket
|
|
1600
|
-
* — the server reattaches the PTY and replays its buffered history. When
|
|
1601
|
-
* the stream was constructed with a token PROVIDER, it is re-invoked for
|
|
1602
|
-
* the new dial (scoped terminal tokens expire within minutes, so the
|
|
1603
|
-
* original credential may be dead by reconnect time); with a plain string
|
|
1604
|
-
* token, pass a fresh `token` override yourself when it may have expired.
|
|
1605
|
-
* `handlers` rebinds callbacks for the new stream.
|
|
1606
|
-
*/
|
|
1607
|
-
reconnect(overrides) {
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
2013
|
+
/**
|
|
2014
|
+
* Re-dial the same endpoint with the same connectionId on a fresh socket
|
|
2015
|
+
* — the server reattaches the PTY and replays its buffered history. When
|
|
2016
|
+
* the stream was constructed with a token PROVIDER, it is re-invoked for
|
|
2017
|
+
* the new dial (scoped terminal tokens expire within minutes, so the
|
|
2018
|
+
* original credential may be dead by reconnect time); with a plain string
|
|
2019
|
+
* token, pass a fresh `token` override yourself when it may have expired.
|
|
2020
|
+
* `handlers` rebinds callbacks for the new stream.
|
|
2021
|
+
*/
|
|
2022
|
+
reconnect(overrides) {
|
|
2023
|
+
const existing = this.reconnectInFlight;
|
|
2024
|
+
if (existing !== void 0) return awaitTerminalOperation(existing, overrides?.signal);
|
|
2025
|
+
const operation = this.reconnectOnce(overrides);
|
|
2026
|
+
let tracked;
|
|
2027
|
+
tracked = operation.finally(() => {
|
|
2028
|
+
if (this.reconnectInFlight === tracked) this.reconnectInFlight = void 0;
|
|
2029
|
+
});
|
|
2030
|
+
this.reconnectInFlight = tracked;
|
|
2031
|
+
return awaitTerminalOperation(tracked, overrides?.signal);
|
|
2032
|
+
}
|
|
2033
|
+
async reconnectOnce(overrides) {
|
|
2034
|
+
const binding = await this.options.reconnectAuthority?.acquire(overrides?.signal);
|
|
2035
|
+
try {
|
|
2036
|
+
return await TerminalStream.connect({
|
|
2037
|
+
...this.options,
|
|
2038
|
+
...binding === void 0 ? {} : {
|
|
2039
|
+
incarnationId: binding.incarnationId,
|
|
2040
|
+
control: binding.control
|
|
2041
|
+
},
|
|
2042
|
+
token: overrides?.token ?? this.options.token,
|
|
2043
|
+
handlers: this.handlersForReconnect(overrides?.handlers),
|
|
2044
|
+
cols: overrides?.cols ?? this.options.cols,
|
|
2045
|
+
rows: overrides?.rows ?? this.options.rows,
|
|
2046
|
+
signal: overrides?.signal
|
|
2047
|
+
});
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
try {
|
|
2050
|
+
await binding?.onFailure?.();
|
|
2051
|
+
} catch {}
|
|
2052
|
+
throw error;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
handlersForReconnect(overrides) {
|
|
2056
|
+
const lifecycleHandlers = this.options.handlers;
|
|
2057
|
+
if (overrides === void 0 || lifecycleHandlers?.onClose === void 0) return overrides ?? lifecycleHandlers;
|
|
2058
|
+
return {
|
|
2059
|
+
...overrides,
|
|
2060
|
+
onClose: (code, reason) => {
|
|
2061
|
+
lifecycleHandlers.onClose?.(code, reason);
|
|
2062
|
+
overrides.onClose?.(code, reason);
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
assertOpen(action) {
|
|
2067
|
+
if (!this.isOpen) throw new TerminalStreamError(`Cannot ${action}: terminal stream is not open`, "NOT_OPEN");
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
//#endregion
|
|
2071
|
+
//#region src/interactive-controller.ts
|
|
2072
|
+
/**
|
|
2073
|
+
* Attach-capable controller for one exact interactive process.
|
|
2074
|
+
*
|
|
2075
|
+
* Control ownership lives in the dependency-light base controller. This layer
|
|
2076
|
+
* adds only the terminal attach operation after that ownership is resolved.
|
|
2077
|
+
*/
|
|
2078
|
+
var InteractiveSessionController = class extends InteractiveSessionControlController {
|
|
2079
|
+
attachInFlight;
|
|
2080
|
+
constructor(attachTransport, options) {
|
|
2081
|
+
super(attachTransport, options);
|
|
2082
|
+
this.attachTransport = attachTransport;
|
|
2083
|
+
}
|
|
2084
|
+
async attach(request, requestOptions) {
|
|
2085
|
+
requestOptions?.signal?.throwIfAborted();
|
|
2086
|
+
if (this.attachInFlight !== void 0) return await awaitWithAbort(this.attachInFlight, requestOptions?.signal);
|
|
2087
|
+
const operation = (async () => {
|
|
2088
|
+
const identity = await this.resolve(requestOptions);
|
|
2089
|
+
return this.attachWithIdentity(identity, request, requestOptions);
|
|
2090
|
+
})();
|
|
2091
|
+
let tracked;
|
|
2092
|
+
tracked = operation.finally(() => {
|
|
2093
|
+
if (this.attachInFlight === tracked) this.attachInFlight = void 0;
|
|
2094
|
+
});
|
|
2095
|
+
this.attachInFlight = tracked;
|
|
2096
|
+
return await tracked;
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Open a stream for a claim returned by this controller's `resolve()`.
|
|
2100
|
+
* The controller releases that claim when the stream cannot be opened.
|
|
2101
|
+
*/
|
|
2102
|
+
async attachWithIdentity(identity, request, requestOptions) {
|
|
2103
|
+
const exactRef = AgentInteractiveSessionRefSchema.parse(identity.ref);
|
|
2104
|
+
assertInteractiveSessionRef(exactRef, this.options);
|
|
2105
|
+
if (!agentInteractiveSessionControlClaimMatchesRef(exactRef, identity.control)) throw new Error("Interactive control claim is not bound to the session");
|
|
2106
|
+
let activeIdentity = {
|
|
2107
|
+
ref: exactRef,
|
|
2108
|
+
control: identity.control
|
|
2109
|
+
};
|
|
2110
|
+
const releasePromises = /* @__PURE__ */ new WeakMap();
|
|
2111
|
+
const releaseClaim = (target) => {
|
|
2112
|
+
const existing = releasePromises.get(target);
|
|
2113
|
+
if (existing !== void 0) return existing;
|
|
2114
|
+
const release = this.safelyReleaseClaim(target);
|
|
2115
|
+
releasePromises.set(target, release);
|
|
2116
|
+
return release;
|
|
2117
|
+
};
|
|
2118
|
+
let reconnectInFlight;
|
|
2119
|
+
const reconnectAuthority = { acquire: async (signal) => {
|
|
2120
|
+
if (!releasePromises.has(activeIdentity)) throw new Error("Interactive terminal must be closed before reconnecting");
|
|
2121
|
+
if (reconnectInFlight !== void 0) return await awaitWithAbort(reconnectInFlight, signal);
|
|
2122
|
+
const operation = (async () => {
|
|
2123
|
+
const next = await this.resolve({ signal });
|
|
2124
|
+
const nextRef = AgentInteractiveSessionRefSchema.parse(next.ref);
|
|
2125
|
+
assertInteractiveSessionRef(nextRef, this.options);
|
|
2126
|
+
const nextIdentity = {
|
|
2127
|
+
ref: nextRef,
|
|
2128
|
+
control: next.control
|
|
2129
|
+
};
|
|
2130
|
+
activeIdentity = nextIdentity;
|
|
2131
|
+
return {
|
|
2132
|
+
incarnationId: nextRef.incarnationId,
|
|
2133
|
+
control: next.control,
|
|
2134
|
+
onFailure: () => releaseClaim(nextIdentity)
|
|
2135
|
+
};
|
|
2136
|
+
})();
|
|
2137
|
+
let tracked;
|
|
2138
|
+
tracked = operation.finally(() => {
|
|
2139
|
+
if (reconnectInFlight === tracked) reconnectInFlight = void 0;
|
|
2140
|
+
});
|
|
2141
|
+
reconnectInFlight = tracked;
|
|
2142
|
+
return await tracked;
|
|
2143
|
+
} };
|
|
2144
|
+
try {
|
|
2145
|
+
requestOptions?.signal?.throwIfAborted();
|
|
2146
|
+
const callerHandlers = request.handlers;
|
|
2147
|
+
const exactRequest = {
|
|
2148
|
+
...request,
|
|
2149
|
+
control: identity.control,
|
|
2150
|
+
handlers: {
|
|
2151
|
+
...callerHandlers,
|
|
2152
|
+
onClose: (code, reason) => {
|
|
2153
|
+
releaseClaim(activeIdentity);
|
|
2154
|
+
callerHandlers?.onClose?.(code, reason);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
const stream = await this.attachTransport.attach(this.options.sessionId, exactRef, exactRequest, requestOptions, reconnectAuthority);
|
|
2159
|
+
if (requestOptions?.signal?.aborted) {
|
|
2160
|
+
await stream.close();
|
|
2161
|
+
requestOptions.signal.throwIfAborted();
|
|
2162
|
+
}
|
|
2163
|
+
return stream;
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
await releaseClaim(activeIdentity);
|
|
2166
|
+
throw error;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
function browserTransport(options) {
|
|
2171
|
+
return {
|
|
2172
|
+
...createBrowserInteractiveSessionControlTransport(options),
|
|
2173
|
+
async attach(sessionId, ref, request, requestOptions, reconnectAuthority) {
|
|
2174
|
+
requestOptions?.signal?.throwIfAborted();
|
|
2175
|
+
if (options.terminalTokenProvider === void 0) throw new Error("Browser interactive terminal attach requires terminalTokenProvider");
|
|
2176
|
+
const exactRef = AgentInteractiveSessionRefSchema.parse(ref);
|
|
2177
|
+
const { handlers, ...wireRequest } = request;
|
|
2178
|
+
const { control, ...terminalRequest } = AgentInteractiveSessionAttachSchema.parse(wireRequest);
|
|
2179
|
+
const terminalOptions = {
|
|
2180
|
+
url: browserInteractiveTerminalEndpoint(options.sessionApiUrl, sessionId),
|
|
2181
|
+
token: options.terminalTokenProvider,
|
|
2182
|
+
connectionId: sessionId,
|
|
2183
|
+
...terminalRequest,
|
|
2184
|
+
incarnationId: exactRef.incarnationId,
|
|
2185
|
+
control,
|
|
2186
|
+
authFormat: "edge",
|
|
2187
|
+
signal: requestOptions?.signal,
|
|
2188
|
+
...reconnectAuthority === void 0 ? {} : { reconnectAuthority },
|
|
2189
|
+
...handlers === void 0 ? {} : { handlers }
|
|
2190
|
+
};
|
|
2191
|
+
requestOptions?.signal?.throwIfAborted();
|
|
2192
|
+
const stream = await TerminalStream.connect(terminalOptions);
|
|
2193
|
+
if (requestOptions?.signal?.aborted) {
|
|
2194
|
+
await stream.close();
|
|
2195
|
+
requestOptions.signal.throwIfAborted();
|
|
2196
|
+
}
|
|
2197
|
+
return stream;
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
/** Concrete controller for browser deep links and terminal attachment. */
|
|
2202
|
+
var BrowserInteractiveSessionController = class extends InteractiveSessionController {
|
|
2203
|
+
constructor(options) {
|
|
2204
|
+
super(browserTransport(options), options);
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
//#endregion
|
|
2208
|
+
//#region src/interactive.ts
|
|
2209
|
+
/**
|
|
2210
|
+
* Exact native coding-agent sessions on the public SDK.
|
|
2211
|
+
*
|
|
2212
|
+
* Agent Interface owns the wire contracts. The SDK only adds local stream
|
|
2213
|
+
* handlers and binds the returned ref/control to a convenient session handle.
|
|
2214
|
+
*/
|
|
2215
|
+
function createInteractiveSessionController(host, sessionId, ref, holderId) {
|
|
2216
|
+
const releaseInteractiveControl = host._releaseInteractiveControl;
|
|
2217
|
+
return new InteractiveSessionController({
|
|
2218
|
+
status: (id, incarnationId, requestOptions) => requestOptions === void 0 ? host._interactiveStatus(id, incarnationId) : host._interactiveStatus(id, incarnationId, requestOptions),
|
|
2219
|
+
claimControl: (id, claimRequest, requestOptions) => host._claimInteractiveControl(id, claimRequest, requestOptions),
|
|
2220
|
+
attach: (id, attachRef, attachRequest, requestOptions, reconnectAuthority) => {
|
|
2221
|
+
if (reconnectAuthority === void 0) return requestOptions === void 0 ? host._attachInteractive(id, attachRef, attachRequest) : host._attachInteractive(id, attachRef, attachRequest, requestOptions);
|
|
2222
|
+
return host._attachInteractive(id, attachRef, attachRequest, requestOptions, reconnectAuthority);
|
|
2223
|
+
},
|
|
2224
|
+
releaseControl: (id, releaseRef, control, requestOptions) => requestOptions === void 0 ? releaseInteractiveControl(id, releaseRef, control) : releaseInteractiveControl(id, releaseRef, control, requestOptions)
|
|
2225
|
+
}, {
|
|
2226
|
+
sessionId,
|
|
2227
|
+
incarnationId: ref.incarnationId,
|
|
2228
|
+
environmentId: ref.run.environmentId,
|
|
2229
|
+
holderId
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Handle for one exact interactive harness session.
|
|
2234
|
+
*
|
|
2235
|
+
* `start()` binds the returned ref and control for prompt and stop commands.
|
|
2236
|
+
* Attach always observes and claims a fresh control before opening its stream.
|
|
2237
|
+
*/
|
|
2238
|
+
var InteractiveSessionHandle = class {
|
|
2239
|
+
ref;
|
|
2240
|
+
control;
|
|
2241
|
+
attachHolderId = createInteractiveControlHolderId("sdk");
|
|
2242
|
+
attachController;
|
|
2243
|
+
attachInFlight;
|
|
2244
|
+
constructor(host, sessionId, options = {}) {
|
|
2245
|
+
this.host = host;
|
|
2246
|
+
this.sessionId = sessionId;
|
|
2247
|
+
this.ref = options.ref;
|
|
2248
|
+
this.control = options.control;
|
|
2249
|
+
}
|
|
2250
|
+
start(options, requestOptions) {
|
|
2251
|
+
requestOptions?.signal?.throwIfAborted();
|
|
2252
|
+
const exactStart = AgentInteractiveSessionStartSchema.parse(options);
|
|
2253
|
+
if (exactStart.run.sessionId !== this.sessionId) throw new Error(`Interactive session ${this.sessionId} start is bound to session ${exactStart.run.sessionId}`);
|
|
2254
|
+
return (requestOptions === void 0 ? this.host._startInteractive(this.sessionId, exactStart) : this.host._startInteractive(this.sessionId, exactStart, requestOptions)).then((info) => {
|
|
2255
|
+
const ref = AgentInteractiveSessionRefSchema.parse(info.ref);
|
|
2256
|
+
const control = AgentInteractiveSessionControlClaimSchema.parse(info.control);
|
|
2257
|
+
if (!agentInteractiveSessionRefMatchesStart(exactStart, ref) || !agentInteractiveSessionControlClaimMatchesRef(ref, control)) throw new Error(`Interactive session ${this.sessionId} start returned an inexact ref or control claim`);
|
|
2258
|
+
const exactInfo = {
|
|
2259
|
+
...info,
|
|
2260
|
+
ref,
|
|
2261
|
+
control
|
|
2262
|
+
};
|
|
2263
|
+
this.ref = ref;
|
|
2264
|
+
this.control = control;
|
|
2265
|
+
this.attachController = void 0;
|
|
2266
|
+
return exactInfo;
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
requireBinding() {
|
|
2270
|
+
const ref = this.requireRef();
|
|
2271
|
+
if (!this.control) throw new Error(`Interactive session ${this.sessionId} handle is not bound to an exact ref and control claim`);
|
|
2272
|
+
const control = AgentInteractiveSessionControlClaimSchema.parse(this.control);
|
|
2273
|
+
if (!agentInteractiveSessionControlClaimMatchesRef(ref, control)) throw new Error(`Interactive session ${this.sessionId} handle is not bound to a control claim for its exact ref`);
|
|
2274
|
+
return {
|
|
2275
|
+
ref,
|
|
2276
|
+
control
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
requireRef() {
|
|
2280
|
+
if (!this.ref) throw new Error(`Interactive session ${this.sessionId} handle is not bound to an exact ref`);
|
|
2281
|
+
const ref = AgentInteractiveSessionRefSchema.parse(this.ref);
|
|
2282
|
+
if (ref.run.sessionId !== this.sessionId) throw new Error(`Interactive session ${this.sessionId} handle is bound to session ${ref.run.sessionId}`);
|
|
2283
|
+
return ref;
|
|
2284
|
+
}
|
|
2285
|
+
sendPrompt(command, options) {
|
|
2286
|
+
options?.signal?.throwIfAborted();
|
|
2287
|
+
const binding = this.requireBinding();
|
|
2288
|
+
const exactCommand = AgentInteractiveSessionPromptCommandSchema.parse(command);
|
|
2289
|
+
if (canonicalCandidateDigest(exactCommand.ref) !== canonicalCandidateDigest(binding.ref) || canonicalCandidateDigest(exactCommand.control) !== canonicalCandidateDigest(binding.control)) throw new Error(`Interactive session ${this.sessionId} prompt is bound to a different exact ref`);
|
|
2290
|
+
return (options === void 0 ? this.host._sendInteractivePrompt(this.sessionId, exactCommand) : this.host._sendInteractivePrompt(this.sessionId, exactCommand, options)).then((acknowledgement) => {
|
|
2291
|
+
const exactAcknowledgement = AgentInteractiveSessionPromptAcknowledgementSchema.parse(acknowledgement);
|
|
2292
|
+
if (!agentInteractiveSessionPromptAcknowledgementMatchesCommand(exactCommand, exactAcknowledgement)) throw new Error(`Interactive session ${this.sessionId} prompt acknowledgement does not match the command`);
|
|
2293
|
+
return exactAcknowledgement;
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
attach(request = {}, options) {
|
|
2297
|
+
options?.signal?.throwIfAborted();
|
|
2298
|
+
if (this.attachInFlight !== void 0) return awaitWithAbort(this.attachInFlight, options?.signal);
|
|
2299
|
+
const ref = this.requireRef();
|
|
2300
|
+
let controller = this.attachController;
|
|
2301
|
+
if (controller === void 0) {
|
|
2302
|
+
controller = createInteractiveSessionController(this.host, this.sessionId, ref, this.attachHolderId);
|
|
2303
|
+
this.attachController = controller;
|
|
2304
|
+
}
|
|
2305
|
+
const operation = controller.resolve(options).then(async (identity) => {
|
|
2306
|
+
const stream = await controller.attachWithIdentity(identity, request, options);
|
|
2307
|
+
this.ref = identity.ref;
|
|
2308
|
+
this.control = identity.control;
|
|
2309
|
+
return stream;
|
|
2310
|
+
});
|
|
2311
|
+
let tracked;
|
|
2312
|
+
tracked = operation.finally(() => {
|
|
2313
|
+
if (this.attachInFlight === tracked) this.attachInFlight = void 0;
|
|
2314
|
+
});
|
|
2315
|
+
this.attachInFlight = tracked;
|
|
2316
|
+
return tracked;
|
|
2317
|
+
}
|
|
2318
|
+
status(options) {
|
|
2319
|
+
options?.signal?.throwIfAborted();
|
|
2320
|
+
const ref = this.requireRef();
|
|
2321
|
+
return (options === void 0 ? this.host._interactiveStatus(this.sessionId, ref.incarnationId) : this.host._interactiveStatus(this.sessionId, ref.incarnationId, options)).then((status) => {
|
|
2322
|
+
if (status !== null && !agentInteractiveSessionStatusMatchesRef(ref, status)) throw new Error(`Interactive session ${this.sessionId} status returned an inexact ref`);
|
|
2323
|
+
return status;
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
claimControl(request, options) {
|
|
2327
|
+
options?.signal?.throwIfAborted();
|
|
2328
|
+
const ref = this.requireRef();
|
|
2329
|
+
const exactRequest = AgentInteractiveSessionControlClaimRequestSchema.parse(request);
|
|
2330
|
+
if (canonicalCandidateDigest(exactRequest.ref) !== canonicalCandidateDigest(ref)) throw new Error(`Interactive session ${this.sessionId} control claim is bound to a different exact ref`);
|
|
2331
|
+
return this.host._claimInteractiveControl(this.sessionId, exactRequest, options).then((acknowledgement) => {
|
|
2332
|
+
const exactAcknowledgement = AgentInteractiveSessionControlClaimAcknowledgementSchema.parse(acknowledgement);
|
|
2333
|
+
if (!agentInteractiveSessionControlClaimAcknowledgementMatchesRequest(exactRequest, exactAcknowledgement)) throw new Error(`Interactive session ${this.sessionId} control claim acknowledgement does not match the request`);
|
|
2334
|
+
if ((exactAcknowledgement.status === "accepted" || exactAcknowledgement.status === "replayed") && exactAcknowledgement.control) {
|
|
2335
|
+
const current = this.control ? AgentInteractiveSessionControlClaimSchema.parse(this.control) : void 0;
|
|
2336
|
+
if (current === void 0 || agentInteractiveSessionControlClaimIsNewer(exactAcknowledgement.control, current)) this.control = exactAcknowledgement.control;
|
|
2337
|
+
}
|
|
2338
|
+
return exactAcknowledgement;
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
stop(command, options) {
|
|
2342
|
+
options?.signal?.throwIfAborted();
|
|
2343
|
+
const binding = this.requireBinding();
|
|
2344
|
+
const exactCommand = AgentInteractiveSessionStopCommandSchema.parse(command);
|
|
2345
|
+
if (canonicalCandidateDigest(exactCommand.ref) !== canonicalCandidateDigest(binding.ref) || canonicalCandidateDigest(exactCommand.control) !== canonicalCandidateDigest(binding.control)) throw new Error(`Interactive session ${this.sessionId} stop is bound to a different exact ref`);
|
|
2346
|
+
return (options === void 0 ? this.host._stopInteractive(this.sessionId, exactCommand) : this.host._stopInteractive(this.sessionId, exactCommand, options)).then((acknowledgement) => {
|
|
2347
|
+
const exactAcknowledgement = AgentInteractiveSessionStopAcknowledgementSchema.parse(acknowledgement);
|
|
2348
|
+
if (!agentInteractiveSessionStopAcknowledgementMatchesCommand(exactCommand, exactAcknowledgement)) throw new Error(`Interactive session ${this.sessionId} stop acknowledgement does not match the command`);
|
|
2349
|
+
return exactAcknowledgement;
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
stopLifecycle(options) {
|
|
2353
|
+
options?.signal?.throwIfAborted();
|
|
2354
|
+
const ref = this.requireRef();
|
|
2355
|
+
return (options === void 0 ? this.host._stopInteractiveLifecycle(this.sessionId, ref) : this.host._stopInteractiveLifecycle(this.sessionId, ref, options)).then((acknowledgement) => {
|
|
2356
|
+
const exactAcknowledgement = AgentInteractiveSessionStopAcknowledgementSchema.parse(acknowledgement);
|
|
2357
|
+
if (canonicalCandidateDigest(exactAcknowledgement.ref) !== canonicalCandidateDigest(ref)) throw new Error(`Interactive session ${this.sessionId} lifecycle stop acknowledgement does not match the exact ref`);
|
|
2358
|
+
if (exactAcknowledgement.status !== "accepted" && exactAcknowledgement.status !== "replayed") throw new Error(`Interactive session ${this.sessionId} lifecycle stop was not confirmed (${exactAcknowledgement.status})`);
|
|
2359
|
+
if (exactAcknowledgement.effect === "unknown") throw new Error(`Interactive session ${this.sessionId} lifecycle stop returned an unknown outcome`);
|
|
2360
|
+
return exactAcknowledgement;
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
//#endregion
|
|
2365
|
+
//#region src/session.ts
|
|
2366
|
+
/**
|
|
2367
|
+
* A single agent session inside a sandbox. Created via
|
|
2368
|
+
* `box.session(id)` — does not hit the network until a method is called.
|
|
2369
|
+
*/
|
|
2370
|
+
var SandboxSession = class SandboxSession {
|
|
2371
|
+
/**
|
|
2372
|
+
* @internal SDK-internal constructor — apps should call `box.session(id)`.
|
|
2373
|
+
*/
|
|
2374
|
+
constructor(box, id) {
|
|
2375
|
+
this.box = box;
|
|
2376
|
+
this.id = id;
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Fetch the current session state from the sandbox. Includes status,
|
|
2380
|
+
* model, prompt count, token usage if known, and timing metadata.
|
|
2381
|
+
*
|
|
2382
|
+
* Throws on transport error; returns `null` if the session id is not
|
|
2383
|
+
* known to the sandbox (e.g. it ended and was reaped, or the id is
|
|
2384
|
+
* invalid).
|
|
2385
|
+
*/
|
|
2386
|
+
async status() {
|
|
2387
|
+
return this.box._sessionStatus(this.id);
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Stream events from this session as they arrive. With no `since`,
|
|
2391
|
+
* starts at the live tail; with `since`, replays strictly after that
|
|
2392
|
+
* event id — useful for reconnect-after-disconnect flows.
|
|
2393
|
+
*
|
|
2394
|
+
* The async iterator terminates when the session reaches a terminal
|
|
2395
|
+
* state (`completed`, `failed`, `cancelled`) and the corresponding
|
|
2396
|
+
* terminal event has been yielded, OR when the caller's signal aborts.
|
|
2397
|
+
*/
|
|
2398
|
+
events(opts) {
|
|
2399
|
+
return this.box._sessionEvents(this.id, opts);
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Await a terminal result. With `executionId`, drains only that execution;
|
|
2403
|
+
* without it, reads status and selects the latest execution. Event replay
|
|
2404
|
+
* makes either form safe after the execution has already finished.
|
|
2405
|
+
*
|
|
2406
|
+
* Use this to wait for a session that was started by another caller
|
|
2407
|
+
* (e.g. `dispatchPrompt`).
|
|
2408
|
+
*/
|
|
2409
|
+
async result(options) {
|
|
2410
|
+
return options ? this.box._sessionResult(this.id, options) : this.box._sessionResult(this.id);
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* List persisted messages for this session, including in-flight assistant
|
|
2414
|
+
* content when the runtime has flushed partial output.
|
|
2415
|
+
*/
|
|
2416
|
+
async messages(opts = {}) {
|
|
2417
|
+
return this.box.messages({
|
|
2418
|
+
...opts,
|
|
2419
|
+
sessionId: this.id
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Continue this session with an additional prompt. Equivalent to
|
|
2424
|
+
* `box.prompt(message, { ...opts, sessionId: this.id })` but reads
|
|
2425
|
+
* naturally on a Session reference.
|
|
2426
|
+
*/
|
|
2427
|
+
async prompt(message, opts) {
|
|
2428
|
+
return this.box.prompt(message, {
|
|
2429
|
+
...opts,
|
|
2430
|
+
sessionId: this.id
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Submit a structured message through the session message endpoint. The
|
|
2435
|
+
* returned receipt confirms admission; follow progress with `events()` or
|
|
2436
|
+
* await the terminal response with `result()`. Model and message identifiers
|
|
2437
|
+
* are translated to the runtime's wire names by the SDK.
|
|
2438
|
+
*/
|
|
2439
|
+
async sendMessage(request, options) {
|
|
2440
|
+
return this.box._sessionSendMessage(this.id, request, options);
|
|
2441
|
+
}
|
|
2442
|
+
/** Delete this session and its persisted runtime state. */
|
|
2443
|
+
async delete() {
|
|
2444
|
+
return this.box._sessionDelete(this.id);
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Fork this session into a new queued session. The fork shares the
|
|
2448
|
+
* parent workspace and copies conversation history up to `messageId`
|
|
2449
|
+
* when supplied.
|
|
2450
|
+
*/
|
|
2451
|
+
async fork(opts) {
|
|
2452
|
+
const info = await this.box._sessionFork(this.id, opts);
|
|
2453
|
+
return {
|
|
2454
|
+
session: new SandboxSession(this.box, info.id),
|
|
2455
|
+
info
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Drive this session's harness in INTERACTIVE mode: spawn its native TUI,
|
|
2460
|
+
* stream the live framebuffer, and inject prompts as keystrokes. Distinct
|
|
2461
|
+
* from the headless `prompt()`/`events()` surface above. Lazy — does not hit
|
|
2462
|
+
* the network until a handle method is called.
|
|
2463
|
+
*/
|
|
2464
|
+
interactive(options = {}) {
|
|
2465
|
+
return new InteractiveSessionHandle(this.box, this.id, options);
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Resolve a pending permission interaction the agent raised mid-turn,
|
|
2469
|
+
* forwarding `allow`/`deny` to the blocked agent through the unified
|
|
2470
|
+
* interaction channel. Throws if no matching permission is outstanding.
|
|
2471
|
+
*/
|
|
2472
|
+
async respondToPermission(permissionID, options) {
|
|
2473
|
+
return this.box._respondToPermission(this.id, permissionID, options);
|
|
2474
|
+
}
|
|
2475
|
+
/** Return every interaction that still awaits a response on this session. */
|
|
2476
|
+
async interactions(options) {
|
|
2477
|
+
return this.box._listInteractions(this.id, options);
|
|
2478
|
+
}
|
|
2479
|
+
/** Respond to one exact interaction with a retry-safe command. */
|
|
2480
|
+
async respondToInteraction(command, options) {
|
|
2481
|
+
return this.box._respondToInteraction(this.id, command, options);
|
|
2482
|
+
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Interrupt an execution without deleting the session. Pass `executionId`
|
|
2485
|
+
* to make the cancellation exact: a newer run on the same session is never
|
|
2486
|
+
* touched. Without it, the current execution is selected. Returns
|
|
2487
|
+
* `{ cancelled: false }` when the selected execution is not running.
|
|
2488
|
+
*/
|
|
2489
|
+
async interrupt(options) {
|
|
2490
|
+
return options ? this.box._interrupt(this.id, options) : this.box._interrupt(this.id);
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Cancel one exact run through the durable operation contract. Reusing the
|
|
2494
|
+
* same request after a lost response returns the sidecar's replayed
|
|
2495
|
+
* acknowledgement instead of issuing a second cancellation.
|
|
2496
|
+
*/
|
|
2497
|
+
async cancelRun(request, options) {
|
|
2498
|
+
return this.box._cancelRun(this.id, request, options);
|
|
2499
|
+
}
|
|
2500
|
+
/** Answer one exact question without selecting another outstanding ask. */
|
|
2501
|
+
async answer(interactionId, answers, options) {
|
|
2502
|
+
return this.box._answerQuestion(this.id, interactionId, answers, options);
|
|
2503
|
+
}
|
|
2504
|
+
/** Return the newest plan awaiting a decision for this session. */
|
|
2505
|
+
async plan() {
|
|
2506
|
+
return this.box._currentPlan(this.id);
|
|
2507
|
+
}
|
|
2508
|
+
/** Approve a durable plan and enqueue its separate execution turn. */
|
|
2509
|
+
async approvePlan(planId) {
|
|
2510
|
+
const id = planId ?? (await this.requireCurrentPlan()).id;
|
|
2511
|
+
return this.box._decidePlan(this.id, id, { outcome: "approved" });
|
|
2512
|
+
}
|
|
2513
|
+
/** Reject a durable plan with required feedback and enqueue a revision turn. */
|
|
2514
|
+
async rejectPlan(feedback, planId) {
|
|
2515
|
+
if (!feedback.trim()) throw new Error("Plan rejection feedback is required");
|
|
2516
|
+
const id = planId ?? (await this.requireCurrentPlan()).id;
|
|
2517
|
+
return this.box._decidePlan(this.id, id, {
|
|
2518
|
+
outcome: "rejected",
|
|
2519
|
+
feedback: feedback.trim()
|
|
1614
2520
|
});
|
|
1615
2521
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
2522
|
+
async requireCurrentPlan() {
|
|
2523
|
+
const plan = await this.plan();
|
|
2524
|
+
if (!plan) throw new Error("No durable plan is awaiting a decision");
|
|
2525
|
+
return plan;
|
|
2526
|
+
}
|
|
2527
|
+
};
|
|
2528
|
+
//#endregion
|
|
2529
|
+
//#region src/tangle/tee-intercept.ts
|
|
2530
|
+
/** hono's default unmatched-route body. */
|
|
2531
|
+
const HONO_NOT_FOUND_BODY = "404 Not Found";
|
|
2532
|
+
/**
|
|
2533
|
+
* A 404 carrying hono's own unmatched-route body came from the router.
|
|
2534
|
+
*
|
|
2535
|
+
* hono answers an unmatched route with exactly "404 Not Found", so that body
|
|
2536
|
+
* is the deployment saying it has no such route — which for `/tee` means the
|
|
2537
|
+
* operator intercept is absent, not that the sandbox is missing.
|
|
2538
|
+
* `ensureRunning()` has already confirmed the sandbox at this point, which is
|
|
2539
|
+
* what makes the distinction safe to draw.
|
|
2540
|
+
*/
|
|
2541
|
+
function isRouterNotFound(body) {
|
|
2542
|
+
return body === HONO_NOT_FOUND_BODY;
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Request a TEE surface, refusing by name when the deployment cannot serve it.
|
|
2546
|
+
*
|
|
2547
|
+
* Returns the successful response for the caller to parse. Throws
|
|
2548
|
+
* `CapabilityError` when the intercept is absent, and otherwise the same
|
|
2549
|
+
* parsed error every other SDK call throws.
|
|
2550
|
+
*/
|
|
2551
|
+
async function fetchTeeSurface(client, sandboxId, surface, init) {
|
|
2552
|
+
const path = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/${surface}`;
|
|
2553
|
+
const response = await client.fetch(path, init);
|
|
2554
|
+
if (response.ok) return response;
|
|
2555
|
+
const body = await response.text();
|
|
2556
|
+
if (response.status === 404 && isRouterNotFound(body)) throw new CapabilityError(`TEE ${surface} is not served by this deployment. The /tee paths are answered by a Tangle confidential operator that intercepts them before the sandbox API; a sandbox reached through the public API has no such route. Point the client at the operator, or create the sandbox with driver.type "tangle" on a deployment that fronts it.`, "TEE_UNSUPPORTED", 404, void 0, "tee");
|
|
2557
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2558
|
+
}
|
|
2559
|
+
//#endregion
|
|
2560
|
+
//#region src/task-session.ts
|
|
2561
|
+
/** A background task session with isolated changeset operations. */
|
|
2562
|
+
var SandboxTaskSession = class extends SandboxSession {
|
|
2563
|
+
/** @internal Apps should call `box.taskSession(id)`. */
|
|
2564
|
+
constructor(taskBox, id) {
|
|
2565
|
+
super(taskBox, id);
|
|
2566
|
+
this.taskBox = taskBox;
|
|
2567
|
+
}
|
|
2568
|
+
/** Read the task's isolated file changes. */
|
|
2569
|
+
async changes() {
|
|
2570
|
+
return this.taskBox._taskSessionChanges(this.id);
|
|
2571
|
+
}
|
|
2572
|
+
/** Apply all or a selected subset of the task's isolated file changes. */
|
|
2573
|
+
async commit(options) {
|
|
2574
|
+
return this.taskBox._taskSessionCommit(this.id, options);
|
|
1618
2575
|
}
|
|
1619
2576
|
};
|
|
1620
2577
|
//#endregion
|
|
@@ -2011,153 +2968,6 @@ function runtimeFilesystemPath(endpoint, path) {
|
|
|
2011
2968
|
const encodedPath = segments.map(encodeURIComponent).join("/");
|
|
2012
2969
|
return `${endpoint}/${isAbsolute ? "/" : ""}${encodedPath}`;
|
|
2013
2970
|
}
|
|
2014
|
-
/**
|
|
2015
|
-
* Single source of truth for a run's outcome, shared by
|
|
2016
|
-
* `prompt`/`task`/`_sessionResult`/turn settling. A run succeeds only when it
|
|
2017
|
-
* reached a terminal event, raised no run-level error, hit no approval gate,
|
|
2018
|
-
* left no unanswered question, and recorded no failed (`isError`) tool. A
|
|
2019
|
-
* tool-only turn with no text succeeds unless the server enforces visible-output
|
|
2020
|
-
* requirements; a turn whose tool failed (even if narrated around) fails.
|
|
2021
|
-
*/
|
|
2022
|
-
function deriveOutcome(inputs) {
|
|
2023
|
-
if (inputs.plan) return {
|
|
2024
|
-
status: "awaiting_plan_decision",
|
|
2025
|
-
success: false,
|
|
2026
|
-
error: "Agent is awaiting a durable plan decision"
|
|
2027
|
-
};
|
|
2028
|
-
if (inputs.question) return {
|
|
2029
|
-
status: "awaiting_question",
|
|
2030
|
-
success: false,
|
|
2031
|
-
error: inputs.runError ?? "Agent is awaiting an answer to a question"
|
|
2032
|
-
};
|
|
2033
|
-
if (inputs.approval) return {
|
|
2034
|
-
status: "blocked_on_approval",
|
|
2035
|
-
success: false,
|
|
2036
|
-
error: inputs.runError ?? inputs.approval.message ?? "Agent is blocked on a hub approval"
|
|
2037
|
-
};
|
|
2038
|
-
if (inputs.runError) return {
|
|
2039
|
-
status: "failed",
|
|
2040
|
-
success: false,
|
|
2041
|
-
error: inputs.runError,
|
|
2042
|
-
errorCode: inputs.runErrorCode
|
|
2043
|
-
};
|
|
2044
|
-
const failedTool = inputs.toolInvocations?.find((t) => t.isError === true);
|
|
2045
|
-
if (failedTool) return {
|
|
2046
|
-
status: "failed",
|
|
2047
|
-
success: false,
|
|
2048
|
-
error: `Tool "${failedTool.toolName}" failed`
|
|
2049
|
-
};
|
|
2050
|
-
if (!inputs.terminalReached) return {
|
|
2051
|
-
status: "failed",
|
|
2052
|
-
success: false,
|
|
2053
|
-
error: "Agent stream ended without a terminal event"
|
|
2054
|
-
};
|
|
2055
|
-
return {
|
|
2056
|
-
status: "success",
|
|
2057
|
-
success: true
|
|
2058
|
-
};
|
|
2059
|
-
}
|
|
2060
|
-
function readAwaitingPlan(data) {
|
|
2061
|
-
const direct = data.plan;
|
|
2062
|
-
const outcome = data.outcome;
|
|
2063
|
-
const candidate = direct && typeof direct === "object" ? direct : outcome && typeof outcome === "object" && outcome.type === "awaiting_plan_decision" ? outcome.plan : void 0;
|
|
2064
|
-
if (!candidate || typeof candidate !== "object") return void 0;
|
|
2065
|
-
const plan = candidate;
|
|
2066
|
-
if (typeof plan.id !== "string" || typeof plan.revision !== "number" || typeof plan.body !== "string" || typeof plan.submittedAt !== "string") return;
|
|
2067
|
-
return plan;
|
|
2068
|
-
}
|
|
2069
|
-
function readPlanFromEvent(event) {
|
|
2070
|
-
return event.type === "result" || event.type === "done" || event.type === "plan.submitted" ? readAwaitingPlan(event.data) : void 0;
|
|
2071
|
-
}
|
|
2072
|
-
/** Read tool invocations off a `result` event payload. */
|
|
2073
|
-
function readToolInvocations(data) {
|
|
2074
|
-
const raw = data.toolInvocations;
|
|
2075
|
-
return Array.isArray(raw) ? raw : void 0;
|
|
2076
|
-
}
|
|
2077
|
-
/** Hub error code surfaced when a write tool hits an approval gate. */
|
|
2078
|
-
const HUB_APPROVAL_CODE = "HUB_APPROVAL_REQUIRED";
|
|
2079
|
-
/** Best-effort string view of an arbitrary tool result for signal matching. */
|
|
2080
|
-
function stringifyToolResult(result) {
|
|
2081
|
-
if (typeof result === "string") return result;
|
|
2082
|
-
try {
|
|
2083
|
-
return JSON.stringify(result) ?? "";
|
|
2084
|
-
} catch {
|
|
2085
|
-
return String(result);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
/**
|
|
2089
|
-
* Walk a tool result for an `approval` object and return its `id`/`connectionId`.
|
|
2090
|
-
* The in-sandbox hub bridge carries these in `structuredContent.details.approval`,
|
|
2091
|
-
* but the exact nesting depends on how the backend serialized the MCP result, so
|
|
2092
|
-
* search defensively. Returns `undefined` when not present — the id is then
|
|
2093
|
-
* surfaced honestly as `undefined`, never invented.
|
|
2094
|
-
*/
|
|
2095
|
-
function findApprovalDetails(value, depth = 0) {
|
|
2096
|
-
if (depth > 6 || value === null || typeof value !== "object") return void 0;
|
|
2097
|
-
const obj = value;
|
|
2098
|
-
const approval = obj.approval;
|
|
2099
|
-
if (approval && typeof approval === "object") {
|
|
2100
|
-
const a = approval;
|
|
2101
|
-
return {
|
|
2102
|
-
...typeof a.id === "string" ? { id: a.id } : {},
|
|
2103
|
-
...typeof a.connectionId === "string" ? { connectionId: a.connectionId } : {}
|
|
2104
|
-
};
|
|
2105
|
-
}
|
|
2106
|
-
for (const v of Object.values(obj)) {
|
|
2107
|
-
const found = findApprovalDetails(v, depth + 1);
|
|
2108
|
-
if (found) return found;
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
/** Find a human-readable `message` string anywhere in a tool result. */
|
|
2112
|
-
function findResultMessage(value, depth = 0) {
|
|
2113
|
-
if (depth > 6 || value === null || typeof value !== "object") return void 0;
|
|
2114
|
-
const obj = value;
|
|
2115
|
-
if (typeof obj.message === "string" && obj.message.length > 0) return obj.message;
|
|
2116
|
-
for (const v of Object.values(obj)) {
|
|
2117
|
-
const found = findResultMessage(v, depth + 1);
|
|
2118
|
-
if (found) return found;
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
/**
|
|
2122
|
-
* Detect a hub approval gate among a run's tool invocations: a failed tool whose
|
|
2123
|
-
* result carries `HUB_APPROVAL_REQUIRED`. Returns the approval id/connectionId
|
|
2124
|
-
* when the structured signal survived serialization, else just the message — so
|
|
2125
|
-
* the run can settle as `blocked_on_approval` even when the id is unrecoverable.
|
|
2126
|
-
*/
|
|
2127
|
-
function detectHubApproval(toolInvocations) {
|
|
2128
|
-
if (!toolInvocations) return void 0;
|
|
2129
|
-
for (const tool of toolInvocations) {
|
|
2130
|
-
if (tool.isError !== true) continue;
|
|
2131
|
-
if (!stringifyToolResult(tool.result).includes(HUB_APPROVAL_CODE)) continue;
|
|
2132
|
-
const approval = findApprovalDetails(tool.result);
|
|
2133
|
-
const message = findResultMessage(tool.result) ?? "Hub action requires approval";
|
|
2134
|
-
return {
|
|
2135
|
-
...approval?.id ? { approvalId: approval.id } : {},
|
|
2136
|
-
...approval?.connectionId ? { connectionId: approval.connectionId } : {},
|
|
2137
|
-
message
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
/** Read an `interaction` event (kind `question`) into an awaiting-question requirement. */
|
|
2142
|
-
function readQuestionEvent(data) {
|
|
2143
|
-
const request = data.request;
|
|
2144
|
-
if (!request || request.kind !== "question" || typeof request.id !== "string") return;
|
|
2145
|
-
return {
|
|
2146
|
-
questionId: request.id,
|
|
2147
|
-
questions: questionsFromAnswerSpec(request)
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
/** Reconstruct the prompt/options shape from a question interaction's answerSpec. */
|
|
2151
|
-
function questionsFromAnswerSpec(request) {
|
|
2152
|
-
return request.answerSpec.fields.map((field) => field.type === "select" ? {
|
|
2153
|
-
question: field.label,
|
|
2154
|
-
options: field.options.map((o) => ({
|
|
2155
|
-
label: o.label,
|
|
2156
|
-
description: o.description
|
|
2157
|
-
})),
|
|
2158
|
-
multiSelect: field.multi === true
|
|
2159
|
-
} : { question: field.label });
|
|
2160
|
-
}
|
|
2161
2971
|
function isProfileNotCachedError(err) {
|
|
2162
2972
|
return err instanceof SandboxError && err.code === "PROFILE_NOT_CACHED";
|
|
2163
2973
|
}
|
|
@@ -2314,6 +3124,10 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2314
3124
|
defaultRuntimeBackend;
|
|
2315
3125
|
runtime;
|
|
2316
3126
|
workspaceImages;
|
|
3127
|
+
interactiveOperations;
|
|
3128
|
+
interactiveTerminalOperations;
|
|
3129
|
+
/** @internal Test seam for terminal attach unit tests. */
|
|
3130
|
+
_terminalStreamConnect = (options) => TerminalStream.connect(options);
|
|
2317
3131
|
/**
|
|
2318
3132
|
* A profile the runtime has cached (create-time priming), letting turns
|
|
2319
3133
|
* send `backend.profileSha256` instead of re-shipping the profile's static
|
|
@@ -2341,6 +3155,17 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2341
3155
|
fetch: (path, init) => this.runtimeFetch(path, init)
|
|
2342
3156
|
});
|
|
2343
3157
|
this.workspaceImages = new WorkspaceImages(this.client, this.info.id, () => this.ensureRunning(), () => this.info.filesystemIncarnationId);
|
|
3158
|
+
this.interactiveOperations = new SandboxInteractiveOperations({
|
|
3159
|
+
id: this.info.id,
|
|
3160
|
+
ensureRunning: (signal) => this.ensureRunning(signal),
|
|
3161
|
+
runtimeFetch: (endpoint, request) => this.runtimeFetch(endpoint, request)
|
|
3162
|
+
});
|
|
3163
|
+
this.interactiveTerminalOperations = new SandboxInteractiveTerminalOperations({
|
|
3164
|
+
id: this.info.id,
|
|
3165
|
+
mintScopedToken: (options) => this.mintScopedToken(options),
|
|
3166
|
+
terminalStreamConnect: (options) => this._terminalStreamConnect(options),
|
|
3167
|
+
interactiveStatus: (id, incarnationId, options) => this._interactiveStatus(id, incarnationId, options)
|
|
3168
|
+
});
|
|
2344
3169
|
}
|
|
2345
3170
|
/** Unique sandbox identifier */
|
|
2346
3171
|
get id() {
|
|
@@ -2541,15 +3366,16 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2541
3366
|
/**
|
|
2542
3367
|
* Refresh sandbox information from the server.
|
|
2543
3368
|
*/
|
|
2544
|
-
async refresh() {
|
|
3369
|
+
async refresh(signal) {
|
|
3370
|
+
signal?.throwIfAborted();
|
|
2545
3371
|
if (this.refreshInFlight) {
|
|
2546
|
-
await this.refreshInFlight;
|
|
3372
|
+
await awaitWithAbort(this.refreshInFlight, signal);
|
|
2547
3373
|
return;
|
|
2548
3374
|
}
|
|
2549
3375
|
const refresh = this.refreshInfo();
|
|
2550
3376
|
this.refreshInFlight = refresh;
|
|
2551
3377
|
try {
|
|
2552
|
-
await refresh;
|
|
3378
|
+
await awaitWithAbort(refresh, signal);
|
|
2553
3379
|
} finally {
|
|
2554
3380
|
if (this.refreshInFlight === refresh) this.refreshInFlight = void 0;
|
|
2555
3381
|
}
|
|
@@ -2588,7 +3414,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2588
3414
|
this.info = {
|
|
2589
3415
|
...this.info,
|
|
2590
3416
|
metadata: {
|
|
2591
|
-
...this.info.metadata
|
|
3417
|
+
...this.info.metadata,
|
|
2592
3418
|
teeAttestationJson: JSON.stringify(data.attestation),
|
|
2593
3419
|
...data.attestationNonce ? { attestationNonce: data.attestationNonce } : {}
|
|
2594
3420
|
}
|
|
@@ -2613,7 +3439,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2613
3439
|
this.info = {
|
|
2614
3440
|
...this.info,
|
|
2615
3441
|
metadata: {
|
|
2616
|
-
...this.info.metadata
|
|
3442
|
+
...this.info.metadata,
|
|
2617
3443
|
teePublicKeyJson: JSON.stringify(data.public_key)
|
|
2618
3444
|
}
|
|
2619
3445
|
};
|
|
@@ -2945,16 +3771,11 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2945
3771
|
await this.ensureRunning();
|
|
2946
3772
|
const startTime = Date.now();
|
|
2947
3773
|
let responseText;
|
|
2948
|
-
|
|
2949
|
-
let runErrorCode;
|
|
3774
|
+
const outcomeTracker = createAgentRunOutcomeTracker();
|
|
2950
3775
|
let executionId = options?.lastEventId ? options.executionId : void 0;
|
|
2951
3776
|
let traceId;
|
|
2952
3777
|
let usage;
|
|
2953
3778
|
let costUsd;
|
|
2954
|
-
let toolInvocations;
|
|
2955
|
-
let question;
|
|
2956
|
-
let plan;
|
|
2957
|
-
let terminalReached = false;
|
|
2958
3779
|
const controller = new AbortController();
|
|
2959
3780
|
try {
|
|
2960
3781
|
const signal = options?.signal ? combineAbortSignals([options.signal, controller.signal]) : controller.signal;
|
|
@@ -2962,6 +3783,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2962
3783
|
...options,
|
|
2963
3784
|
signal
|
|
2964
3785
|
})) {
|
|
3786
|
+
outcomeTracker.observe(event);
|
|
2965
3787
|
responseText = applySandboxEventText(responseText, event);
|
|
2966
3788
|
const eventExecutionId = event.data.executionId;
|
|
2967
3789
|
if (typeof eventExecutionId === "string" && eventExecutionId.length > 0) executionId = eventExecutionId;
|
|
@@ -2969,26 +3791,9 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2969
3791
|
usage = readTokenUsage(event.data) ?? usage;
|
|
2970
3792
|
costUsd = readTokenCostUsd(event.data) ?? costUsd;
|
|
2971
3793
|
}
|
|
2972
|
-
plan = readPlanFromEvent(event) ?? plan;
|
|
2973
|
-
if (event.type === "result") toolInvocations = readToolInvocations(event.data) ?? toolInvocations;
|
|
2974
|
-
if (event.type === "result" || event.type === "done") terminalReached = true;
|
|
2975
|
-
if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
|
|
2976
3794
|
if (event.type === "trace.id") traceId = event.data.traceId;
|
|
2977
|
-
if (event.type === "error") {
|
|
2978
|
-
runError = event.data.message;
|
|
2979
|
-
runErrorCode = event.data.code;
|
|
2980
|
-
}
|
|
2981
3795
|
}
|
|
2982
|
-
const
|
|
2983
|
-
const outcome = deriveOutcome({
|
|
2984
|
-
terminalReached,
|
|
2985
|
-
runError,
|
|
2986
|
-
runErrorCode,
|
|
2987
|
-
toolInvocations,
|
|
2988
|
-
approval,
|
|
2989
|
-
question,
|
|
2990
|
-
plan
|
|
2991
|
-
});
|
|
3796
|
+
const outcome = outcomeTracker.finish();
|
|
2992
3797
|
return {
|
|
2993
3798
|
success: outcome.success,
|
|
2994
3799
|
status: outcome.status,
|
|
@@ -2996,10 +3801,11 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2996
3801
|
response: responseText,
|
|
2997
3802
|
error: outcome.error,
|
|
2998
3803
|
errorCode: outcome.errorCode,
|
|
2999
|
-
toolInvocations,
|
|
3000
|
-
approval,
|
|
3001
|
-
|
|
3002
|
-
|
|
3804
|
+
toolInvocations: outcome.toolInvocations,
|
|
3805
|
+
approval: outcome.approval,
|
|
3806
|
+
interaction: outcome.interaction,
|
|
3807
|
+
question: outcome.question,
|
|
3808
|
+
plan: outcome.plan,
|
|
3003
3809
|
traceId,
|
|
3004
3810
|
durationMs: Date.now() - startTime,
|
|
3005
3811
|
usage,
|
|
@@ -3028,15 +3834,13 @@ var SandboxInstance = class SandboxInstance {
|
|
|
3028
3834
|
* Stream events from an agent prompt.
|
|
3029
3835
|
* Use this for real-time updates during agent execution.
|
|
3030
3836
|
*
|
|
3031
|
-
* Guarantees
|
|
3032
|
-
* ends with
|
|
3033
|
-
* error
|
|
3034
|
-
*
|
|
3035
|
-
* never a thrown generator. Caller-initiated cancellation (aborting
|
|
3036
|
-
* `options.signal`) ends the stream silently with no synthetic terminal.
|
|
3837
|
+
* Guarantees terminal completion on every non-cancelled path. A clean run
|
|
3838
|
+
* emits a `result` summary and ends with `done`. A failure is an in-band
|
|
3839
|
+
* `error` followed by `done`, never a thrown generator. Caller cancellation
|
|
3840
|
+
* ends the stream silently with no synthetic terminal.
|
|
3037
3841
|
*
|
|
3038
3842
|
* Automatically reconnects via the runtime event replay endpoint if the
|
|
3039
|
-
* SSE stream drops before a terminal event (`
|
|
3843
|
+
* SSE stream drops before a terminal event (`done` or `error`) is
|
|
3040
3844
|
* received. Reconnection is transparent — replayed events that were
|
|
3041
3845
|
* already yielded (based on event ID tracking) are deduplicated.
|
|
3042
3846
|
*/
|
|
@@ -3119,7 +3923,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
3119
3923
|
try {
|
|
3120
3924
|
for await (const event of this.streamPromptInner(message, options, { forceFullProfile: profileResendAttempted })) {
|
|
3121
3925
|
yieldedThisAttempt = true;
|
|
3122
|
-
if (event.type === "
|
|
3926
|
+
if (event.type === "done") receivedTerminal = true;
|
|
3123
3927
|
if (event.type === "execution.started" && event.data.executionId) executionId = event.data.executionId;
|
|
3124
3928
|
const sid = event.data.sessionId;
|
|
3125
3929
|
if (typeof sid === "string" && sid.length > 0) sessionId = sid;
|
|
@@ -3278,7 +4082,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
3278
4082
|
if (event.type === "execution.started" && event.data?.executionId) executionId = event.data.executionId;
|
|
3279
4083
|
const eventSessionId = event.data?.sessionId;
|
|
3280
4084
|
if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
|
|
3281
|
-
if (event.type === "
|
|
4085
|
+
if (event.type === "done" || event.type === "error") {
|
|
3282
4086
|
receivedTerminal = true;
|
|
3283
4087
|
logStreamCheckpoint("terminal_event", {
|
|
3284
4088
|
eventType: event.type,
|
|
@@ -3348,6 +4152,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
3348
4152
|
sessionId,
|
|
3349
4153
|
executionId,
|
|
3350
4154
|
status: info.status,
|
|
4155
|
+
...info.status === "completed" ? { outcome: { type: "completed" } } : {},
|
|
3351
4156
|
replayUnavailable: true
|
|
3352
4157
|
}
|
|
3353
4158
|
};
|
|
@@ -3372,7 +4177,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
3372
4177
|
if (event.id) lastEventId = event.id;
|
|
3373
4178
|
const eventSessionId = event.data?.sessionId;
|
|
3374
4179
|
if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
|
|
3375
|
-
if (event.type === "
|
|
4180
|
+
if (event.type === "done" || event.type === "error") {
|
|
3376
4181
|
receivedTerminal = true;
|
|
3377
4182
|
logStreamCheckpoint("terminal_event", {
|
|
3378
4183
|
eventType: event.type,
|
|
@@ -4622,62 +5427,64 @@ var SandboxInstance = class SandboxInstance {
|
|
|
4622
5427
|
return this.createProcessHandle(pid, data.data.command);
|
|
4623
5428
|
}
|
|
4624
5429
|
createProcessHandle(pid, command) {
|
|
4625
|
-
const
|
|
5430
|
+
const status = async () => {
|
|
5431
|
+
const response = await this.runtimeFetch(`/process/${pid}`, { method: "GET" });
|
|
5432
|
+
if (!response.ok) {
|
|
5433
|
+
const body = await response.text();
|
|
5434
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
5435
|
+
}
|
|
5436
|
+
const process = (await response.json()).data;
|
|
5437
|
+
return {
|
|
5438
|
+
pid: process.pid,
|
|
5439
|
+
...process.idempotencyKey === void 0 ? {} : { idempotencyKey: process.idempotencyKey },
|
|
5440
|
+
command: process.command,
|
|
5441
|
+
cwd: process.cwd,
|
|
5442
|
+
running: process.running,
|
|
5443
|
+
exitCode: process.exitCode,
|
|
5444
|
+
exitSignal: process.exitSignal,
|
|
5445
|
+
startedAt: new Date(process.startedAt),
|
|
5446
|
+
exitedAt: process.exitedAt ? new Date(process.exitedAt) : void 0
|
|
5447
|
+
};
|
|
5448
|
+
};
|
|
5449
|
+
const kill = async (signal = "SIGTERM", options) => {
|
|
5450
|
+
const response = await this.runtimeFetch(`/process/${pid}`, {
|
|
5451
|
+
method: "DELETE",
|
|
5452
|
+
body: JSON.stringify({
|
|
5453
|
+
signal,
|
|
5454
|
+
...options?.tree === true ? { tree: true } : {}
|
|
5455
|
+
})
|
|
5456
|
+
});
|
|
5457
|
+
if (!response.ok && response.status !== 404) {
|
|
5458
|
+
const body = await response.text();
|
|
5459
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
5460
|
+
}
|
|
5461
|
+
};
|
|
5462
|
+
const logs = async function* (runtimeFetch, parseProcessLogStream) {
|
|
5463
|
+
const response = await runtimeFetch(`/process/${pid}/logs`, { method: "GET" });
|
|
5464
|
+
if (!response.ok) {
|
|
5465
|
+
const body = await response.text();
|
|
5466
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
5467
|
+
}
|
|
5468
|
+
yield* parseProcessLogStream(response);
|
|
5469
|
+
}.bind(void 0, this.runtimeFetch.bind(this), this.parseProcessLogStream.bind(this));
|
|
4626
5470
|
return {
|
|
4627
5471
|
pid,
|
|
4628
5472
|
command,
|
|
4629
|
-
|
|
4630
|
-
const response = await self.runtimeFetch(`/process/${pid}`, { method: "GET" });
|
|
4631
|
-
if (!response.ok) {
|
|
4632
|
-
const body = await response.text();
|
|
4633
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
4634
|
-
}
|
|
4635
|
-
const p = (await response.json()).data;
|
|
4636
|
-
return {
|
|
4637
|
-
pid: p.pid,
|
|
4638
|
-
...p.idempotencyKey === void 0 ? {} : { idempotencyKey: p.idempotencyKey },
|
|
4639
|
-
command: p.command,
|
|
4640
|
-
cwd: p.cwd,
|
|
4641
|
-
running: p.running,
|
|
4642
|
-
exitCode: p.exitCode,
|
|
4643
|
-
exitSignal: p.exitSignal,
|
|
4644
|
-
startedAt: new Date(p.startedAt),
|
|
4645
|
-
exitedAt: p.exitedAt ? new Date(p.exitedAt) : void 0
|
|
4646
|
-
};
|
|
4647
|
-
},
|
|
5473
|
+
status,
|
|
4648
5474
|
async wait() {
|
|
4649
5475
|
while (true) {
|
|
4650
|
-
const s = await
|
|
5476
|
+
const s = await status();
|
|
4651
5477
|
if (!s.running) return s.exitCode;
|
|
4652
5478
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4653
5479
|
}
|
|
4654
5480
|
},
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
method: "DELETE",
|
|
4658
|
-
body: JSON.stringify({
|
|
4659
|
-
signal,
|
|
4660
|
-
...options?.tree === true ? { tree: true } : {}
|
|
4661
|
-
})
|
|
4662
|
-
});
|
|
4663
|
-
if (!response.ok && response.status !== 404) {
|
|
4664
|
-
const body = await response.text();
|
|
4665
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
4666
|
-
}
|
|
4667
|
-
},
|
|
4668
|
-
async *logs() {
|
|
4669
|
-
const response = await self.runtimeFetch(`/process/${pid}/logs`, { method: "GET" });
|
|
4670
|
-
if (!response.ok) {
|
|
4671
|
-
const body = await response.text();
|
|
4672
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
4673
|
-
}
|
|
4674
|
-
yield* self.parseProcessLogStream(response);
|
|
4675
|
-
},
|
|
5481
|
+
kill,
|
|
5482
|
+
logs,
|
|
4676
5483
|
async *stdout() {
|
|
4677
|
-
for await (const entry of
|
|
5484
|
+
for await (const entry of logs()) if (entry.type === "stdout") yield entry.data;
|
|
4678
5485
|
},
|
|
4679
5486
|
async *stderr() {
|
|
4680
|
-
for await (const entry of
|
|
5487
|
+
for await (const entry of logs()) if (entry.type === "stderr") yield entry.data;
|
|
4681
5488
|
}
|
|
4682
5489
|
};
|
|
4683
5490
|
}
|
|
@@ -5911,10 +6718,12 @@ var SandboxInstance = class SandboxInstance {
|
|
|
5911
6718
|
if (!statuses.includes(this.status)) return false;
|
|
5912
6719
|
return this.status !== "running" || this.filesystemIncarnationReadiness === "ready";
|
|
5913
6720
|
}
|
|
5914
|
-
async ensureRunning() {
|
|
6721
|
+
async ensureRunning(signal) {
|
|
6722
|
+
signal?.throwIfAborted();
|
|
5915
6723
|
const statusObservedAt = sandboxStatusObservedAt.get(this) ?? 0;
|
|
5916
6724
|
const statusAgeMs = Date.now() - statusObservedAt;
|
|
5917
|
-
if (directRuntimeInstances.has(this) || statusObservedAt <= 0 || statusAgeMs < 0 || statusAgeMs >= SANDBOX_STATUS_FRESHNESS_MS) await this.refresh();
|
|
6725
|
+
if (directRuntimeInstances.has(this) || statusObservedAt <= 0 || statusAgeMs < 0 || statusAgeMs >= SANDBOX_STATUS_FRESHNESS_MS) await this.refresh(signal);
|
|
6726
|
+
signal?.throwIfAborted();
|
|
5918
6727
|
if (this.status !== "running") throw new StateError(`Sandbox is not running (status: ${this.status})`, this.status, "running");
|
|
5919
6728
|
this.assertFilesystemIncarnationReady();
|
|
5920
6729
|
}
|
|
@@ -6399,31 +7208,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
6399
7208
|
* while session-runtime/read-only scopes work with SandboxRuntimeClient.
|
|
6400
7209
|
*/
|
|
6401
7210
|
async mintScopedToken(opts) {
|
|
6402
|
-
|
|
6403
|
-
if (opts.scope === "session" && !opts.runtimeSessionId) throw new ValidationError("session scope requires the actual runtimeSessionId");
|
|
6404
|
-
const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/scoped-token`, {
|
|
6405
|
-
method: "POST",
|
|
6406
|
-
headers: { "Content-Type": "application/json" },
|
|
6407
|
-
body: JSON.stringify({
|
|
6408
|
-
scope: opts.scope,
|
|
6409
|
-
...opts.sessionId ? { sessionId: opts.sessionId } : {},
|
|
6410
|
-
...opts.scope === "session" ? { runtimeSessionId: opts.runtimeSessionId } : {},
|
|
6411
|
-
...opts.ttlMinutes ? { ttlMinutes: opts.ttlMinutes } : {}
|
|
6412
|
-
})
|
|
6413
|
-
});
|
|
6414
|
-
if (!response.ok) {
|
|
6415
|
-
const body = await response.text();
|
|
6416
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
6417
|
-
}
|
|
6418
|
-
const data = await response.json();
|
|
6419
|
-
if (typeof data.runtimeUrl !== "string" || typeof data.sidecarProxyUrl !== "string") throw new Error("Scoped token response missing runtime URLs.");
|
|
6420
|
-
return {
|
|
6421
|
-
token: data.token,
|
|
6422
|
-
expiresAt: /* @__PURE__ */ new Date(data.expiresAt * 1e3),
|
|
6423
|
-
runtimeUrl: data.runtimeUrl,
|
|
6424
|
-
sidecarProxyUrl: data.sidecarProxyUrl,
|
|
6425
|
-
scope: data.scope
|
|
6426
|
-
};
|
|
7211
|
+
return requestScopedToken(this.client, this.id, opts);
|
|
6427
7212
|
}
|
|
6428
7213
|
/** @internal — invoked by SandboxSession.status(). */
|
|
6429
7214
|
async _sessionStatus(id) {
|
|
@@ -6450,7 +7235,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
6450
7235
|
}
|
|
6451
7236
|
for await (const event of this.parseSSEStream(response, opts?.signal)) {
|
|
6452
7237
|
yield event;
|
|
6453
|
-
const turnTerminal = event.type === "
|
|
7238
|
+
const turnTerminal = event.type === "done" || event.type === "error";
|
|
6454
7239
|
if ((stopOnTurnTerminal || opts?.executionId !== void 0) && turnTerminal) return;
|
|
6455
7240
|
}
|
|
6456
7241
|
}
|
|
@@ -6469,7 +7254,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
6469
7254
|
}
|
|
6470
7255
|
for await (const event of this.parseSSEStream(response)) {
|
|
6471
7256
|
yield event;
|
|
6472
|
-
if (event.type === "
|
|
7257
|
+
if (event.type === "done" || event.type === "error") return;
|
|
6473
7258
|
}
|
|
6474
7259
|
}
|
|
6475
7260
|
/** @internal — invoked by SandboxSession.result(). */
|
|
@@ -6491,20 +7276,13 @@ var SandboxInstance = class SandboxInstance {
|
|
|
6491
7276
|
}
|
|
6492
7277
|
let resultExecutionId = executionId;
|
|
6493
7278
|
let response;
|
|
6494
|
-
|
|
6495
|
-
let runErrorCode;
|
|
7279
|
+
const outcomeTracker = createAgentRunOutcomeTracker();
|
|
6496
7280
|
let traceId;
|
|
6497
7281
|
let usage;
|
|
6498
7282
|
let costUsd;
|
|
6499
|
-
|
|
6500
|
-
let question;
|
|
6501
|
-
let plan;
|
|
6502
|
-
let terminalReached = false;
|
|
6503
|
-
const events = options?.executionId ? this._executionEvents(id, executionId) : this._sessionEvents(id, {
|
|
6504
|
-
since: "0",
|
|
6505
|
-
executionId
|
|
6506
|
-
}, true);
|
|
7283
|
+
const events = this._executionEvents(id, executionId);
|
|
6507
7284
|
for await (const event of events) {
|
|
7285
|
+
outcomeTracker.observe(event);
|
|
6508
7286
|
response = applySandboxEventText(response, event);
|
|
6509
7287
|
if (event.type === "result" || event.type === "done") {
|
|
6510
7288
|
const data = event.data;
|
|
@@ -6512,33 +7290,10 @@ var SandboxInstance = class SandboxInstance {
|
|
|
6512
7290
|
usage = readTokenUsage(data) ?? usage;
|
|
6513
7291
|
costUsd = readTokenCostUsd(data) ?? costUsd;
|
|
6514
7292
|
}
|
|
6515
|
-
plan = readPlanFromEvent(event) ?? plan;
|
|
6516
|
-
if (event.type === "result") {
|
|
6517
|
-
const data = event.data;
|
|
6518
|
-
toolInvocations = readToolInvocations(data) ?? toolInvocations;
|
|
6519
|
-
}
|
|
6520
|
-
if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
|
|
6521
7293
|
if (event.type === "trace.id") traceId = event.data.traceId;
|
|
6522
|
-
if (event.type === "error")
|
|
6523
|
-
const data = event.data;
|
|
6524
|
-
runError = data.message;
|
|
6525
|
-
runErrorCode = data.code;
|
|
6526
|
-
}
|
|
6527
|
-
if (event.type === "done" || event.type === "result") {
|
|
6528
|
-
terminalReached = true;
|
|
6529
|
-
break;
|
|
6530
|
-
}
|
|
7294
|
+
if (event.type === "done" || event.type === "error") break;
|
|
6531
7295
|
}
|
|
6532
|
-
const
|
|
6533
|
-
const outcome = deriveOutcome({
|
|
6534
|
-
terminalReached,
|
|
6535
|
-
runError,
|
|
6536
|
-
runErrorCode,
|
|
6537
|
-
toolInvocations,
|
|
6538
|
-
approval,
|
|
6539
|
-
question,
|
|
6540
|
-
plan
|
|
6541
|
-
});
|
|
7296
|
+
const outcome = outcomeTracker.finish();
|
|
6542
7297
|
return {
|
|
6543
7298
|
success: outcome.success,
|
|
6544
7299
|
status: outcome.status,
|
|
@@ -6546,151 +7301,52 @@ var SandboxInstance = class SandboxInstance {
|
|
|
6546
7301
|
response,
|
|
6547
7302
|
error: outcome.error,
|
|
6548
7303
|
errorCode: outcome.errorCode,
|
|
6549
|
-
toolInvocations,
|
|
6550
|
-
approval,
|
|
6551
|
-
|
|
6552
|
-
|
|
7304
|
+
toolInvocations: outcome.toolInvocations,
|
|
7305
|
+
approval: outcome.approval,
|
|
7306
|
+
interaction: outcome.interaction,
|
|
7307
|
+
question: outcome.question,
|
|
7308
|
+
plan: outcome.plan,
|
|
6553
7309
|
traceId,
|
|
6554
7310
|
durationMs: Date.now() - startTime,
|
|
6555
7311
|
usage,
|
|
6556
7312
|
costUsd
|
|
6557
7313
|
};
|
|
6558
7314
|
}
|
|
6559
|
-
/** @internal —
|
|
6560
|
-
async _startInteractive(id, options) {
|
|
6561
|
-
|
|
6562
|
-
await this.ensureRunning();
|
|
6563
|
-
const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, {
|
|
6564
|
-
method: "POST",
|
|
6565
|
-
body: JSON.stringify(options)
|
|
6566
|
-
});
|
|
6567
|
-
if (!response.ok) {
|
|
6568
|
-
const body = await response.text();
|
|
6569
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
6570
|
-
}
|
|
6571
|
-
const data = (await response.json())?.data;
|
|
6572
|
-
if (!data || typeof data.sessionId !== "string" || typeof data.streamUrl !== "string") throw new ServerError("Interactive session response missing data", 502, {
|
|
6573
|
-
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`,
|
|
6574
|
-
origin: "runtime"
|
|
6575
|
-
});
|
|
6576
|
-
return data;
|
|
7315
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7316
|
+
async _startInteractive(id, options, requestOptions) {
|
|
7317
|
+
return this.interactiveOperations._startInteractive(id, options, requestOptions);
|
|
6577
7318
|
}
|
|
6578
|
-
/** @internal —
|
|
6579
|
-
async _sendInteractivePrompt(id,
|
|
6580
|
-
|
|
6581
|
-
const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive/prompt`, {
|
|
6582
|
-
method: "POST",
|
|
6583
|
-
body: JSON.stringify({ prompt })
|
|
6584
|
-
});
|
|
6585
|
-
if (!response.ok) {
|
|
6586
|
-
const body = await response.text();
|
|
6587
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
6588
|
-
}
|
|
7319
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7320
|
+
async _sendInteractivePrompt(id, command, options) {
|
|
7321
|
+
return this.interactiveOperations._sendInteractivePrompt(id, command, options);
|
|
6589
7322
|
}
|
|
6590
|
-
/** @internal —
|
|
6591
|
-
async
|
|
6592
|
-
|
|
6593
|
-
const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, { method: "GET" });
|
|
6594
|
-
if (response.status === 404) return null;
|
|
6595
|
-
if (!response.ok) {
|
|
6596
|
-
const body = await response.text();
|
|
6597
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
6598
|
-
}
|
|
6599
|
-
const data = (await response.json())?.data;
|
|
6600
|
-
if (!data || data.state !== "running" && data.state !== "exited") throw new ServerError("Interactive status response missing data", 502, {
|
|
6601
|
-
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`,
|
|
6602
|
-
origin: "runtime"
|
|
6603
|
-
});
|
|
6604
|
-
return data;
|
|
7323
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7324
|
+
async _claimInteractiveControl(id, request, options) {
|
|
7325
|
+
return this.interactiveOperations._claimInteractiveControl(id, request, options);
|
|
6605
7326
|
}
|
|
6606
|
-
/**
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
* go through `TerminalStream.connect`.
|
|
6610
|
-
*/
|
|
6611
|
-
_terminalStreamConnect = (options) => TerminalStream.connect(options);
|
|
6612
|
-
/**
|
|
6613
|
-
* Resolve a sandbox-relative terminal stream path against the browser-safe
|
|
6614
|
-
* sidecar proxy base returned by the scoped-token mint. Concatenation, not
|
|
6615
|
-
* URL resolution: the proxy base carries a path prefix
|
|
6616
|
-
* (`/v1/sidecar-proxy/{id}`) that `new URL(absolutePath, base)` would drop.
|
|
6617
|
-
*/
|
|
6618
|
-
resolveTerminalStreamUrl(sidecarProxyUrl, streamPath) {
|
|
6619
|
-
if (!streamPath.startsWith("/")) throw new ServerError(`Terminal stream path must be sandbox-relative (got "${streamPath}")`, 502, { origin: "runtime" });
|
|
6620
|
-
return `${sidecarProxyUrl.replace(/\/+$/, "")}${streamPath}`;
|
|
7327
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7328
|
+
async _releaseInteractiveControl(id, ref, control, options) {
|
|
7329
|
+
return this.interactiveOperations._releaseInteractiveControl(id, ref, control, options);
|
|
6621
7330
|
}
|
|
6622
|
-
/** @internal —
|
|
6623
|
-
async
|
|
6624
|
-
|
|
6625
|
-
if (!status) throw new NotFoundError("Interactive session", id, {
|
|
6626
|
-
origin: "runtime",
|
|
6627
|
-
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
|
|
6628
|
-
});
|
|
6629
|
-
if (status.state !== "running") throw new StateError(`Interactive session ${id} has exited; there is no live PTY to attach`, status.state, "running", {
|
|
6630
|
-
origin: "runtime",
|
|
6631
|
-
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
|
|
6632
|
-
}, "INTERACTIVE_SESSION_EXITED");
|
|
6633
|
-
const scoped = await this.mintScopedToken({
|
|
6634
|
-
scope: "session-runtime",
|
|
6635
|
-
sessionId: id
|
|
6636
|
-
});
|
|
6637
|
-
return this._terminalStreamConnect({
|
|
6638
|
-
url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, status.streamUrl),
|
|
6639
|
-
token: this.terminalTokenProvider(scoped.token, id),
|
|
6640
|
-
connectionId: id,
|
|
6641
|
-
cols: options.cols,
|
|
6642
|
-
rows: options.rows,
|
|
6643
|
-
handlers: options.handlers
|
|
6644
|
-
});
|
|
7331
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7332
|
+
async _interactiveStatus(id, incarnationId, options) {
|
|
7333
|
+
return this.interactiveOperations._interactiveStatus(id, incarnationId, options);
|
|
6645
7334
|
}
|
|
6646
|
-
/**
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
* (a `reconnect()` after the socket dropped or the detach window) re-mints,
|
|
6650
|
-
* because scoped terminal tokens are clamped to a <= 15-minute TTL and the
|
|
6651
|
-
* original is likely dead by then.
|
|
6652
|
-
*/
|
|
6653
|
-
terminalTokenProvider(initialToken, sessionId) {
|
|
6654
|
-
let unused = initialToken;
|
|
6655
|
-
return async () => {
|
|
6656
|
-
if (unused !== null) {
|
|
6657
|
-
const token = unused;
|
|
6658
|
-
unused = null;
|
|
6659
|
-
return token;
|
|
6660
|
-
}
|
|
6661
|
-
return (await this.mintScopedToken({
|
|
6662
|
-
scope: "session-runtime",
|
|
6663
|
-
sessionId
|
|
6664
|
-
})).token;
|
|
6665
|
-
};
|
|
7335
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7336
|
+
async _attachInteractive(id, ref, request, options, reconnectAuthority) {
|
|
7337
|
+
return this.interactiveTerminalOperations._attachInteractive(id, ref, request, options, reconnectAuthority);
|
|
6666
7338
|
}
|
|
6667
|
-
/** @internal —
|
|
7339
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
6668
7340
|
async _attachTerminal(connectionId, options = {}) {
|
|
6669
|
-
|
|
6670
|
-
const scoped = await this.mintScopedToken({
|
|
6671
|
-
scope: "session-runtime",
|
|
6672
|
-
sessionId: connectionId
|
|
6673
|
-
});
|
|
6674
|
-
return this._terminalStreamConnect({
|
|
6675
|
-
url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, `/terminals/${encodeURIComponent(connectionId)}/ws`),
|
|
6676
|
-
token: this.terminalTokenProvider(scoped.token, connectionId),
|
|
6677
|
-
connectionId,
|
|
6678
|
-
cols: options.cols,
|
|
6679
|
-
rows: options.rows,
|
|
6680
|
-
command: options.command,
|
|
6681
|
-
cwd: options.cwd,
|
|
6682
|
-
env: options.env,
|
|
6683
|
-
handlers: options.handlers
|
|
6684
|
-
});
|
|
7341
|
+
return this.interactiveTerminalOperations._attachTerminal(connectionId, options);
|
|
6685
7342
|
}
|
|
6686
|
-
/** @internal —
|
|
6687
|
-
async _stopInteractive(id) {
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
}
|
|
7343
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7344
|
+
async _stopInteractive(id, command, options) {
|
|
7345
|
+
return this.interactiveOperations._stopInteractive(id, command, options);
|
|
7346
|
+
}
|
|
7347
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7348
|
+
async _stopInteractiveLifecycle(id, ref, options) {
|
|
7349
|
+
return this.interactiveOperations._stopInteractiveLifecycle(id, ref, options);
|
|
6694
7350
|
}
|
|
6695
7351
|
interactionTransport() {
|
|
6696
7352
|
return {
|
|
@@ -6843,21 +7499,16 @@ function isSessionInFlight(info) {
|
|
|
6843
7499
|
return info.status === "queued" && !isSessionAwaitingFirstTurn(info);
|
|
6844
7500
|
}
|
|
6845
7501
|
function settleTurnDrive(result) {
|
|
6846
|
-
const
|
|
6847
|
-
if (plan) return {
|
|
7502
|
+
const outcome = deriveAgentResultOutcome(result);
|
|
7503
|
+
if (outcome.status === "awaiting_plan_decision" && outcome.plan) return {
|
|
6848
7504
|
state: "awaiting_plan_decision",
|
|
6849
|
-
plan,
|
|
7505
|
+
plan: outcome.plan,
|
|
6850
7506
|
result
|
|
6851
7507
|
};
|
|
6852
7508
|
const text = typeof result.text === "string" ? result.text : typeof result.response === "string" ? result.response : void 0;
|
|
6853
|
-
|
|
6854
|
-
terminalReached: true,
|
|
6855
|
-
runError: typeof result.error === "string" ? result.error : void 0,
|
|
6856
|
-
toolInvocations: Array.isArray(result.toolInvocations) ? result.toolInvocations : void 0
|
|
6857
|
-
}).status;
|
|
6858
|
-
if (status !== "success") return {
|
|
7509
|
+
if (outcome.status !== "success") return {
|
|
6859
7510
|
state: "failed",
|
|
6860
|
-
error:
|
|
7511
|
+
error: outcome.error ?? `turn ${outcome.status}`
|
|
6861
7512
|
};
|
|
6862
7513
|
const usage = readTokenUsage(result);
|
|
6863
7514
|
return {
|
|
@@ -6933,4 +7584,4 @@ function quoteForShell(value) {
|
|
|
6933
7584
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
6934
7585
|
}
|
|
6935
7586
|
//#endregion
|
|
6936
|
-
export {
|
|
7587
|
+
export { normalizeConnection as C, toOtelJson as D, otelTraceIdForTangleTrace as E, parseSSEStream as O, isToolBearingEvent as S, exportTraceBundle as T, requestInteractiveSessionToken as _, WorkspaceImages as a, collectAgentResponseText as b, SandboxTaskSession as c, agentInteractiveSessionPromptRequestDigest as d, BrowserInteractiveSessionController as f, TERMINAL_WS_ECHO_SUBPROTOCOL as g, TerminalStreamError as h, normalizeStartupDiagnostics as i, SandboxSession as l, TerminalStream as m, createSandboxInstanceFromResponse as n, GPU_LEASE_PROVIDER_NAMES as o, InteractiveSessionController as p, normalizeSandboxCreateReceipt as r, MAX_EGRESS_DENIALS_LIMIT as s, SandboxInstance as t, InteractiveSessionHandle as u, applySandboxEventText as v, buildTraceExportPayload as w, getSandboxEventText as x, collectAgentFinalMessageText as y };
|