@tangle-network/sandbox 0.30.2-develop.20260820100122.dc85046 → 0.30.2-develop.20260820200640.75917
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-TwIC-Ccd.d.ts → agent-run-outcome-CM7xD7Mg.d.ts} +1 -1
- 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-CCKEVUaD.js → client-BfnJ44gF.js} +19 -6
- package/dist/{client-1OK1sjCh.d.ts → client-Do_HS671.d.ts} +20 -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-DAxioMfr.d.ts → index-ebsVOlUc.d.ts} +2 -2
- package/dist/index.d.ts +11 -9
- package/dist/index.js +7 -6
- package/dist/interactive-control.d.ts +2 -0
- package/dist/interactive-control.js +2 -0
- package/dist/{runtime-api-TVLbV10b.js → runtime-api-B0tx7NJM.js} +2 -6
- package/dist/runtime.d.ts +3 -3
- package/dist/runtime.js +2 -1
- package/dist/{sandbox-DM-VD8ja.js → sandbox-AUHcCJe1.js} +1418 -565
- package/dist/{sandbox-Caev7isY.d.ts → sandbox-Cr-4qzaA.d.ts} +134 -187
- package/dist/tangle/index.d.ts +1 -1
- package/dist/tangle/index.js +1 -1
- package/dist/{tangle-lzUyvhKp.js → tangle-iDk_s8vt.js} +1 -1
- package/dist/terminal-stream-4KNJ6SmL.d.ts +256 -0
- package/dist/{types-D_ykdqn6.d.ts → types-BwYLVqxX.d.ts} +14 -250
- 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;
|
|
@@ -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
1503
|
}
|
|
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
|
-
}
|
|
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();
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded) || typeof decoded.type !== "string") {
|
|
1918
|
+
failProtocolFrame();
|
|
1512
1919
|
return;
|
|
1513
1920
|
}
|
|
1514
|
-
|
|
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,56 +1978,600 @@ 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",
|
|
1571
1985
|
data: chunk
|
|
1572
1986
|
}));
|
|
1573
1987
|
}
|
|
1574
|
-
/** Resize the PTY. */
|
|
1575
|
-
resize(cols, rows) {
|
|
1576
|
-
assertDimension("cols", cols);
|
|
1577
|
-
assertDimension("rows", rows);
|
|
1578
|
-
this.assertOpen("resize");
|
|
1579
|
-
this.
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1988
|
+
/** Resize the PTY. */
|
|
1989
|
+
resize(cols, rows) {
|
|
1990
|
+
assertDimension("cols", cols);
|
|
1991
|
+
assertDimension("rows", rows);
|
|
1992
|
+
this.assertOpen("resize");
|
|
1993
|
+
assertInteractiveControlActive(this.interactiveIdentity);
|
|
1994
|
+
this.ws.send(JSON.stringify({
|
|
1995
|
+
type: "resize",
|
|
1996
|
+
cols,
|
|
1997
|
+
rows
|
|
1998
|
+
}));
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Detach cleanly: close the socket and resolve once the close completes.
|
|
2002
|
+
* The PTY survives server-side for `ready.detachTimeoutMs` (5 minutes
|
|
2003
|
+
* today) — reattach within that window via {@link reconnect} (or a fresh
|
|
2004
|
+
* `connect()` with the same connectionId) to replay history and continue.
|
|
2005
|
+
*/
|
|
2006
|
+
close() {
|
|
2007
|
+
if (this.closed) return Promise.resolve();
|
|
2008
|
+
return new Promise((resolve) => {
|
|
2009
|
+
this.closeWaiters.push(resolve);
|
|
2010
|
+
this.ws.close(1e3, "client detach");
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
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);
|
|
1584
2482
|
}
|
|
1585
2483
|
/**
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1589
|
-
* `
|
|
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.
|
|
1590
2488
|
*/
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
return new Promise((resolve) => {
|
|
1594
|
-
this.closeWaiters.push(resolve);
|
|
1595
|
-
this.ws.close(1e3, "client detach");
|
|
1596
|
-
});
|
|
2489
|
+
async interrupt(options) {
|
|
2490
|
+
return options ? this.box._interrupt(this.id, options) : this.box._interrupt(this.id);
|
|
1597
2491
|
}
|
|
1598
2492
|
/**
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
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.
|
|
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.
|
|
1606
2496
|
*/
|
|
1607
|
-
|
|
1608
|
-
return
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
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
|
|
@@ -2167,6 +3124,10 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2167
3124
|
defaultRuntimeBackend;
|
|
2168
3125
|
runtime;
|
|
2169
3126
|
workspaceImages;
|
|
3127
|
+
interactiveOperations;
|
|
3128
|
+
interactiveTerminalOperations;
|
|
3129
|
+
/** @internal Test seam for terminal attach unit tests. */
|
|
3130
|
+
_terminalStreamConnect = (options) => TerminalStream.connect(options);
|
|
2170
3131
|
/**
|
|
2171
3132
|
* A profile the runtime has cached (create-time priming), letting turns
|
|
2172
3133
|
* send `backend.profileSha256` instead of re-shipping the profile's static
|
|
@@ -2194,6 +3155,17 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2194
3155
|
fetch: (path, init) => this.runtimeFetch(path, init)
|
|
2195
3156
|
});
|
|
2196
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
|
+
});
|
|
2197
3169
|
}
|
|
2198
3170
|
/** Unique sandbox identifier */
|
|
2199
3171
|
get id() {
|
|
@@ -2394,15 +3366,16 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2394
3366
|
/**
|
|
2395
3367
|
* Refresh sandbox information from the server.
|
|
2396
3368
|
*/
|
|
2397
|
-
async refresh() {
|
|
3369
|
+
async refresh(signal) {
|
|
3370
|
+
signal?.throwIfAborted();
|
|
2398
3371
|
if (this.refreshInFlight) {
|
|
2399
|
-
await this.refreshInFlight;
|
|
3372
|
+
await awaitWithAbort(this.refreshInFlight, signal);
|
|
2400
3373
|
return;
|
|
2401
3374
|
}
|
|
2402
3375
|
const refresh = this.refreshInfo();
|
|
2403
3376
|
this.refreshInFlight = refresh;
|
|
2404
3377
|
try {
|
|
2405
|
-
await refresh;
|
|
3378
|
+
await awaitWithAbort(refresh, signal);
|
|
2406
3379
|
} finally {
|
|
2407
3380
|
if (this.refreshInFlight === refresh) this.refreshInFlight = void 0;
|
|
2408
3381
|
}
|
|
@@ -2441,7 +3414,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2441
3414
|
this.info = {
|
|
2442
3415
|
...this.info,
|
|
2443
3416
|
metadata: {
|
|
2444
|
-
...this.info.metadata
|
|
3417
|
+
...this.info.metadata,
|
|
2445
3418
|
teeAttestationJson: JSON.stringify(data.attestation),
|
|
2446
3419
|
...data.attestationNonce ? { attestationNonce: data.attestationNonce } : {}
|
|
2447
3420
|
}
|
|
@@ -2466,7 +3439,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
2466
3439
|
this.info = {
|
|
2467
3440
|
...this.info,
|
|
2468
3441
|
metadata: {
|
|
2469
|
-
...this.info.metadata
|
|
3442
|
+
...this.info.metadata,
|
|
2470
3443
|
teePublicKeyJson: JSON.stringify(data.public_key)
|
|
2471
3444
|
}
|
|
2472
3445
|
};
|
|
@@ -4454,62 +5427,64 @@ var SandboxInstance = class SandboxInstance {
|
|
|
4454
5427
|
return this.createProcessHandle(pid, data.data.command);
|
|
4455
5428
|
}
|
|
4456
5429
|
createProcessHandle(pid, command) {
|
|
4457
|
-
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));
|
|
4458
5470
|
return {
|
|
4459
5471
|
pid,
|
|
4460
5472
|
command,
|
|
4461
|
-
|
|
4462
|
-
const response = await self.runtimeFetch(`/process/${pid}`, { method: "GET" });
|
|
4463
|
-
if (!response.ok) {
|
|
4464
|
-
const body = await response.text();
|
|
4465
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
4466
|
-
}
|
|
4467
|
-
const p = (await response.json()).data;
|
|
4468
|
-
return {
|
|
4469
|
-
pid: p.pid,
|
|
4470
|
-
...p.idempotencyKey === void 0 ? {} : { idempotencyKey: p.idempotencyKey },
|
|
4471
|
-
command: p.command,
|
|
4472
|
-
cwd: p.cwd,
|
|
4473
|
-
running: p.running,
|
|
4474
|
-
exitCode: p.exitCode,
|
|
4475
|
-
exitSignal: p.exitSignal,
|
|
4476
|
-
startedAt: new Date(p.startedAt),
|
|
4477
|
-
exitedAt: p.exitedAt ? new Date(p.exitedAt) : void 0
|
|
4478
|
-
};
|
|
4479
|
-
},
|
|
5473
|
+
status,
|
|
4480
5474
|
async wait() {
|
|
4481
5475
|
while (true) {
|
|
4482
|
-
const s = await
|
|
5476
|
+
const s = await status();
|
|
4483
5477
|
if (!s.running) return s.exitCode;
|
|
4484
5478
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4485
5479
|
}
|
|
4486
5480
|
},
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
method: "DELETE",
|
|
4490
|
-
body: JSON.stringify({
|
|
4491
|
-
signal,
|
|
4492
|
-
...options?.tree === true ? { tree: true } : {}
|
|
4493
|
-
})
|
|
4494
|
-
});
|
|
4495
|
-
if (!response.ok && response.status !== 404) {
|
|
4496
|
-
const body = await response.text();
|
|
4497
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
4498
|
-
}
|
|
4499
|
-
},
|
|
4500
|
-
async *logs() {
|
|
4501
|
-
const response = await self.runtimeFetch(`/process/${pid}/logs`, { method: "GET" });
|
|
4502
|
-
if (!response.ok) {
|
|
4503
|
-
const body = await response.text();
|
|
4504
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
4505
|
-
}
|
|
4506
|
-
yield* self.parseProcessLogStream(response);
|
|
4507
|
-
},
|
|
5481
|
+
kill,
|
|
5482
|
+
logs,
|
|
4508
5483
|
async *stdout() {
|
|
4509
|
-
for await (const entry of
|
|
5484
|
+
for await (const entry of logs()) if (entry.type === "stdout") yield entry.data;
|
|
4510
5485
|
},
|
|
4511
5486
|
async *stderr() {
|
|
4512
|
-
for await (const entry of
|
|
5487
|
+
for await (const entry of logs()) if (entry.type === "stderr") yield entry.data;
|
|
4513
5488
|
}
|
|
4514
5489
|
};
|
|
4515
5490
|
}
|
|
@@ -5743,10 +6718,12 @@ var SandboxInstance = class SandboxInstance {
|
|
|
5743
6718
|
if (!statuses.includes(this.status)) return false;
|
|
5744
6719
|
return this.status !== "running" || this.filesystemIncarnationReadiness === "ready";
|
|
5745
6720
|
}
|
|
5746
|
-
async ensureRunning() {
|
|
6721
|
+
async ensureRunning(signal) {
|
|
6722
|
+
signal?.throwIfAborted();
|
|
5747
6723
|
const statusObservedAt = sandboxStatusObservedAt.get(this) ?? 0;
|
|
5748
6724
|
const statusAgeMs = Date.now() - statusObservedAt;
|
|
5749
|
-
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();
|
|
5750
6727
|
if (this.status !== "running") throw new StateError(`Sandbox is not running (status: ${this.status})`, this.status, "running");
|
|
5751
6728
|
this.assertFilesystemIncarnationReady();
|
|
5752
6729
|
}
|
|
@@ -6231,31 +7208,7 @@ var SandboxInstance = class SandboxInstance {
|
|
|
6231
7208
|
* while session-runtime/read-only scopes work with SandboxRuntimeClient.
|
|
6232
7209
|
*/
|
|
6233
7210
|
async mintScopedToken(opts) {
|
|
6234
|
-
|
|
6235
|
-
if (opts.scope === "session" && !opts.runtimeSessionId) throw new ValidationError("session scope requires the actual runtimeSessionId");
|
|
6236
|
-
const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/scoped-token`, {
|
|
6237
|
-
method: "POST",
|
|
6238
|
-
headers: { "Content-Type": "application/json" },
|
|
6239
|
-
body: JSON.stringify({
|
|
6240
|
-
scope: opts.scope,
|
|
6241
|
-
...opts.sessionId ? { sessionId: opts.sessionId } : {},
|
|
6242
|
-
...opts.scope === "session" ? { runtimeSessionId: opts.runtimeSessionId } : {},
|
|
6243
|
-
...opts.ttlMinutes ? { ttlMinutes: opts.ttlMinutes } : {}
|
|
6244
|
-
})
|
|
6245
|
-
});
|
|
6246
|
-
if (!response.ok) {
|
|
6247
|
-
const body = await response.text();
|
|
6248
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
6249
|
-
}
|
|
6250
|
-
const data = await response.json();
|
|
6251
|
-
if (typeof data.runtimeUrl !== "string" || typeof data.sidecarProxyUrl !== "string") throw new Error("Scoped token response missing runtime URLs.");
|
|
6252
|
-
return {
|
|
6253
|
-
token: data.token,
|
|
6254
|
-
expiresAt: /* @__PURE__ */ new Date(data.expiresAt * 1e3),
|
|
6255
|
-
runtimeUrl: data.runtimeUrl,
|
|
6256
|
-
sidecarProxyUrl: data.sidecarProxyUrl,
|
|
6257
|
-
scope: data.scope
|
|
6258
|
-
};
|
|
7211
|
+
return requestScopedToken(this.client, this.id, opts);
|
|
6259
7212
|
}
|
|
6260
7213
|
/** @internal — invoked by SandboxSession.status(). */
|
|
6261
7214
|
async _sessionStatus(id) {
|
|
@@ -6359,141 +7312,41 @@ var SandboxInstance = class SandboxInstance {
|
|
|
6359
7312
|
costUsd
|
|
6360
7313
|
};
|
|
6361
7314
|
}
|
|
6362
|
-
/** @internal —
|
|
6363
|
-
async _startInteractive(id, options) {
|
|
6364
|
-
|
|
6365
|
-
await this.ensureRunning();
|
|
6366
|
-
const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, {
|
|
6367
|
-
method: "POST",
|
|
6368
|
-
body: JSON.stringify(options)
|
|
6369
|
-
});
|
|
6370
|
-
if (!response.ok) {
|
|
6371
|
-
const body = await response.text();
|
|
6372
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
6373
|
-
}
|
|
6374
|
-
const data = (await response.json())?.data;
|
|
6375
|
-
if (!data || typeof data.sessionId !== "string" || typeof data.streamUrl !== "string") throw new ServerError("Interactive session response missing data", 502, {
|
|
6376
|
-
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`,
|
|
6377
|
-
origin: "runtime"
|
|
6378
|
-
});
|
|
6379
|
-
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);
|
|
6380
7318
|
}
|
|
6381
|
-
/** @internal —
|
|
6382
|
-
async _sendInteractivePrompt(id,
|
|
6383
|
-
|
|
6384
|
-
const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive/prompt`, {
|
|
6385
|
-
method: "POST",
|
|
6386
|
-
body: JSON.stringify({ prompt })
|
|
6387
|
-
});
|
|
6388
|
-
if (!response.ok) {
|
|
6389
|
-
const body = await response.text();
|
|
6390
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
6391
|
-
}
|
|
7319
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7320
|
+
async _sendInteractivePrompt(id, command, options) {
|
|
7321
|
+
return this.interactiveOperations._sendInteractivePrompt(id, command, options);
|
|
6392
7322
|
}
|
|
6393
|
-
/** @internal —
|
|
6394
|
-
async
|
|
6395
|
-
|
|
6396
|
-
const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, { method: "GET" });
|
|
6397
|
-
if (response.status === 404) return null;
|
|
6398
|
-
if (!response.ok) {
|
|
6399
|
-
const body = await response.text();
|
|
6400
|
-
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
6401
|
-
}
|
|
6402
|
-
const data = (await response.json())?.data;
|
|
6403
|
-
if (!data || data.state !== "running" && data.state !== "exited") throw new ServerError("Interactive status response missing data", 502, {
|
|
6404
|
-
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`,
|
|
6405
|
-
origin: "runtime"
|
|
6406
|
-
});
|
|
6407
|
-
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);
|
|
6408
7326
|
}
|
|
6409
|
-
/**
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
* go through `TerminalStream.connect`.
|
|
6413
|
-
*/
|
|
6414
|
-
_terminalStreamConnect = (options) => TerminalStream.connect(options);
|
|
6415
|
-
/**
|
|
6416
|
-
* Resolve a sandbox-relative terminal stream path against the browser-safe
|
|
6417
|
-
* sidecar proxy base returned by the scoped-token mint. Concatenation, not
|
|
6418
|
-
* URL resolution: the proxy base carries a path prefix
|
|
6419
|
-
* (`/v1/sidecar-proxy/{id}`) that `new URL(absolutePath, base)` would drop.
|
|
6420
|
-
*/
|
|
6421
|
-
resolveTerminalStreamUrl(sidecarProxyUrl, streamPath) {
|
|
6422
|
-
if (!streamPath.startsWith("/")) throw new ServerError(`Terminal stream path must be sandbox-relative (got "${streamPath}")`, 502, { origin: "runtime" });
|
|
6423
|
-
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);
|
|
6424
7330
|
}
|
|
6425
|
-
/** @internal —
|
|
6426
|
-
async
|
|
6427
|
-
|
|
6428
|
-
if (!status) throw new NotFoundError("Interactive session", id, {
|
|
6429
|
-
origin: "runtime",
|
|
6430
|
-
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
|
|
6431
|
-
});
|
|
6432
|
-
if (status.state !== "running") throw new StateError(`Interactive session ${id} has exited; there is no live PTY to attach`, status.state, "running", {
|
|
6433
|
-
origin: "runtime",
|
|
6434
|
-
endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
|
|
6435
|
-
}, "INTERACTIVE_SESSION_EXITED");
|
|
6436
|
-
const scoped = await this.mintScopedToken({
|
|
6437
|
-
scope: "session-runtime",
|
|
6438
|
-
sessionId: id
|
|
6439
|
-
});
|
|
6440
|
-
return this._terminalStreamConnect({
|
|
6441
|
-
url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, status.streamUrl),
|
|
6442
|
-
token: this.terminalTokenProvider(scoped.token, id),
|
|
6443
|
-
connectionId: id,
|
|
6444
|
-
cols: options.cols,
|
|
6445
|
-
rows: options.rows,
|
|
6446
|
-
handlers: options.handlers
|
|
6447
|
-
});
|
|
7331
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
7332
|
+
async _interactiveStatus(id, incarnationId, options) {
|
|
7333
|
+
return this.interactiveOperations._interactiveStatus(id, incarnationId, options);
|
|
6448
7334
|
}
|
|
6449
|
-
/**
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
* (a `reconnect()` after the socket dropped or the detach window) re-mints,
|
|
6453
|
-
* because scoped terminal tokens are clamped to a <= 15-minute TTL and the
|
|
6454
|
-
* original is likely dead by then.
|
|
6455
|
-
*/
|
|
6456
|
-
terminalTokenProvider(initialToken, sessionId) {
|
|
6457
|
-
let unused = initialToken;
|
|
6458
|
-
return async () => {
|
|
6459
|
-
if (unused !== null) {
|
|
6460
|
-
const token = unused;
|
|
6461
|
-
unused = null;
|
|
6462
|
-
return token;
|
|
6463
|
-
}
|
|
6464
|
-
return (await this.mintScopedToken({
|
|
6465
|
-
scope: "session-runtime",
|
|
6466
|
-
sessionId
|
|
6467
|
-
})).token;
|
|
6468
|
-
};
|
|
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);
|
|
6469
7338
|
}
|
|
6470
|
-
/** @internal —
|
|
7339
|
+
/** @internal — delegated to the native interactive operations module. */
|
|
6471
7340
|
async _attachTerminal(connectionId, options = {}) {
|
|
6472
|
-
|
|
6473
|
-
const scoped = await this.mintScopedToken({
|
|
6474
|
-
scope: "session-runtime",
|
|
6475
|
-
sessionId: connectionId
|
|
6476
|
-
});
|
|
6477
|
-
return this._terminalStreamConnect({
|
|
6478
|
-
url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, `/terminals/${encodeURIComponent(connectionId)}/ws`),
|
|
6479
|
-
token: this.terminalTokenProvider(scoped.token, connectionId),
|
|
6480
|
-
connectionId,
|
|
6481
|
-
cols: options.cols,
|
|
6482
|
-
rows: options.rows,
|
|
6483
|
-
command: options.command,
|
|
6484
|
-
cwd: options.cwd,
|
|
6485
|
-
env: options.env,
|
|
6486
|
-
handlers: options.handlers
|
|
6487
|
-
});
|
|
7341
|
+
return this.interactiveTerminalOperations._attachTerminal(connectionId, options);
|
|
6488
7342
|
}
|
|
6489
|
-
/** @internal —
|
|
6490
|
-
async _stopInteractive(id) {
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
}
|
|
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);
|
|
6497
7350
|
}
|
|
6498
7351
|
interactionTransport() {
|
|
6499
7352
|
return {
|
|
@@ -6731,4 +7584,4 @@ function quoteForShell(value) {
|
|
|
6731
7584
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
6732
7585
|
}
|
|
6733
7586
|
//#endregion
|
|
6734
|
-
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 };
|