@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.
Files changed (35) hide show
  1. package/README.md +1 -0
  2. package/dist/abort-signal-si1WMfJb.js +31 -0
  3. package/dist/agent/index.d.ts +3 -3
  4. package/dist/agent/index.js +3 -3
  5. package/dist/{agent-run-outcome-TwIC-Ccd.d.ts → agent-run-outcome-CM7xD7Mg.d.ts} +1 -1
  6. package/dist/auth/index.d.ts +1 -1
  7. package/dist/browser-interactive-control-controller-B476gcui.d.ts +96 -0
  8. package/dist/browser-interactive-control-controller-wXQ7W5pT.js +276 -0
  9. package/dist/{client-CCKEVUaD.js → client-BfnJ44gF.js} +19 -6
  10. package/dist/{client-1OK1sjCh.d.ts → client-Do_HS671.d.ts} +20 -3
  11. package/dist/collaboration/index.d.ts +1 -1
  12. package/dist/collaboration/index.js +1 -1
  13. package/dist/core.d.ts +4 -4
  14. package/dist/core.js +2 -2
  15. package/dist/{index-DAxioMfr.d.ts → index-ebsVOlUc.d.ts} +2 -2
  16. package/dist/index.d.ts +11 -9
  17. package/dist/index.js +7 -6
  18. package/dist/interactive-control.d.ts +2 -0
  19. package/dist/interactive-control.js +2 -0
  20. package/dist/{runtime-api-TVLbV10b.js → runtime-api-B0tx7NJM.js} +2 -6
  21. package/dist/runtime.d.ts +3 -3
  22. package/dist/runtime.js +2 -1
  23. package/dist/{sandbox-DM-VD8ja.js → sandbox-AUHcCJe1.js} +1418 -565
  24. package/dist/{sandbox-Caev7isY.d.ts → sandbox-Cr-4qzaA.d.ts} +134 -187
  25. package/dist/tangle/index.d.ts +1 -1
  26. package/dist/tangle/index.js +1 -1
  27. package/dist/{tangle-lzUyvhKp.js → tangle-iDk_s8vt.js} +1 -1
  28. package/dist/terminal-stream-4KNJ6SmL.d.ts +256 -0
  29. package/dist/{types-D_ykdqn6.d.ts → types-BwYLVqxX.d.ts} +14 -250
  30. package/package.json +9 -4
  31. /package/dist/{collaboration-COCXdRrk.js → collaboration-DQD9kCdN.js} +0 -0
  32. /package/dist/{errors-Bdy5jXzc.d.ts → errors-CQOvGddH.d.ts} +0 -0
  33. /package/dist/{index-DtRFVx5U.d.ts → index-Bq_z_u6A.d.ts} +0 -0
  34. /package/dist/{index-Cp1KYanB.d.ts → index-CMLW3u5K.d.ts} +0 -0
  35. /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 combineAbortSignals, d as applyRequestedModel, f as assertCurrentBackendType, h as deriveAgentResultOutcome, i as sessionHeaders, l as encodePromptForWire, m as createAgentRunOutcomeTracker, n as normalizeSessionInfo, o as SANDBOX_PROXY_REQUEST_MAX_BYTES, p as normalizeRuntimeBackendConfig, r as appendSessionIdQuery, t as SandboxRuntimeApi } from "./runtime-api-TVLbV10b.js";
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 { AgentExactRunControlRefSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, InteractionAcknowledgementSchema, InteractionBindingSchema, InteractionRequestSchema, InteractionResponseCommandSchema, InteractionResponseSchema, agentRunCancellationAcknowledgementMatchesRequest, canonicalCandidateDigest, sha256DigestSchema } from "@tangle-network/agent-interface";
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(value) {
479
- if (value === null || typeof value !== "object" || Array.isArray(value)) invalidCapabilities("a non-object document");
480
- const record = value;
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 src/interaction-response.ts
607
- const APPLIED_INTERACTION_STATUS = z.enum(["accepted", "already_resolved_same"]);
608
- const REFUSED_INTERACTION_STATUS = z.enum(["already_resolved_different", "binding_mismatch"]);
609
- const InteractionResolutionSchema = z.strictObject({
610
- status: APPLIED_INTERACTION_STATUS,
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
- const InteractionResolutionEnvelopeSchema = z.object({
650
+ /** Canonical success envelope for the public session interaction endpoint. */
651
+ const interactionResolutionResponseSchema = z.strictObject({
625
652
  success: z.literal(true),
626
- resolution: InteractionResolutionSchema
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 = InteractionResolutionEnvelopeSchema.safeParse(parseJson(body, response, endpoint));
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 src/interactive.ts
1041
- /**
1042
- * Handle for one session's interactive harness. Obtained via
1043
- * `box.session(id).interactive()`; does not hit the network until a method is
1044
- * called.
1045
- */
1046
- var InteractiveSessionHandle = class {
1047
- constructor(host, sessionId) {
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
- * Spawn the harness's interactive TUI for this session. Returns the
1053
- * `streamUrl` of the live framebuffer — open it programmatically with
1054
- * `attach()`, or hand it to a terminal frontend. Throws if the harness has
1055
- * no interactive entrypoint (e.g. cursor, which is SDK-only), the binary is
1056
- * not installed, or a session is already running for this id.
1057
- */
1058
- start(options) {
1059
- return this.host._startInteractive(this.sessionId, options);
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
- /** Inject a prompt as keystrokes into the live harness (submitted on send). */
1062
- sendPrompt(prompt) {
1063
- return this.host._sendInteractivePrompt(this.sessionId, prompt);
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
- * Attach to the live harness PTY as a {@link TerminalStream}: the same PTY
1067
- * the interactive TUI runs in (the sidecar keys it by this session id), so
1068
- * the stream sees the harness's screen and its keystrokes drive the
1069
- * harness — never a fresh shell. The host mints a short-lived scoped
1070
- * terminal token and dials the session's `streamUrl` through the secured
1071
- * sidecar proxy; resolves once the server acknowledges the attach (the
1072
- * buffered screen then replays through `handlers.onData`).
1073
- *
1074
- * Throws when no interactive session is running for this id — attach after
1075
- * `start()`, or to a session another caller started.
1076
- */
1077
- attach(options) {
1078
- if (!this.host._attachInteractive) return Promise.reject(/* @__PURE__ */ new Error("Interactive session attach is not supported by this host"));
1079
- return this.host._attachInteractive(this.sessionId, options);
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
- * Observe the session: running, exited (with its exit code), or `null`
1083
- * when the sidecar no longer knows the id. Poll this to detect completion —
1084
- * an interactive session has no structured result stream, so the exit
1085
- * tombstone is the completion signal.
1086
- */
1087
- status() {
1088
- if (!this.host._interactiveStatus) return Promise.reject(/* @__PURE__ */ new Error("Interactive session status is not supported by this host"));
1089
- return this.host._interactiveStatus(this.sessionId);
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
- /** Stop the interactive harness and reap its PTY. */
1092
- stop() {
1093
- return this.host._stopInteractive(this.sessionId);
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/session.ts
1098
- /**
1099
- * A single agent session inside a sandbox. Created via
1100
- * `box.session(id)` — does not hit the network until a method is called.
1101
- */
1102
- var SandboxSession = class SandboxSession {
1103
- /**
1104
- * @internal SDK-internal constructor — apps should call `box.session(id)`.
1105
- */
1106
- constructor(box, id) {
1107
- this.box = box;
1108
- this.id = id;
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
- * Fetch the current session state from the sandbox. Includes status,
1112
- * model, prompt count, token usage if known, and timing metadata.
1113
- *
1114
- * Throws on transport error; returns `null` if the session id is not
1115
- * known to the sandbox (e.g. it ended and was reaped, or the id is
1116
- * invalid).
1117
- */
1118
- async status() {
1119
- return this.box._sessionStatus(this.id);
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
- * Stream events from this session as they arrive. With no `since`,
1123
- * starts at the live tail; with `since`, replays strictly after that
1124
- * event id — useful for reconnect-after-disconnect flows.
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
- * Await a terminal result. With `executionId`, drains only that execution;
1135
- * without it, reads status and selects the latest execution. Event replay
1136
- * makes either form safe after the execution has already finished.
1137
- *
1138
- * Use this to wait for a session that was started by another caller
1139
- * (e.g. `dispatchPrompt`).
1140
- */
1141
- async result(options) {
1142
- return options ? this.box._sessionResult(this.id, options) : this.box._sessionResult(this.id);
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
- * List persisted messages for this session, including in-flight assistant
1146
- * content when the runtime has flushed partial output.
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
- async messages(opts = {}) {
1149
- return this.box.messages({
1150
- ...opts,
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
- * Continue this session with an additional prompt. Equivalent to
1156
- * `box.prompt(message, { ...opts, sessionId: this.id })` but reads
1157
- * naturally on a Session reference.
1158
- */
1159
- async prompt(message, opts) {
1160
- return this.box.prompt(message, {
1161
- ...opts,
1162
- sessionId: this.id
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
- * Submit a structured message through the session message endpoint. The
1167
- * returned receipt confirms admission; follow progress with `events()` or
1168
- * await the terminal response with `result()`. Model and message identifiers
1169
- * are translated to the runtime's wire names by the SDK.
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
- async sendMessage(request, options) {
1172
- return this.box._sessionSendMessage(this.id, request, options);
1173
- }
1174
- /** Delete this session and its persisted runtime state. */
1175
- async delete() {
1176
- return this.box._sessionDelete(this.id);
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
- * Fork this session into a new queued session. The fork shares the
1180
- * parent workspace and copies conversation history up to `messageId`
1181
- * when supplied.
1182
- */
1183
- async fork(opts) {
1184
- const info = await this.box._sessionFork(this.id, opts);
1185
- return {
1186
- session: new SandboxSession(this.box, info.id),
1187
- info
1188
- };
1189
- }
1190
- /**
1191
- * Drive this session's harness in INTERACTIVE mode: spawn its native TUI,
1192
- * stream the live framebuffer, and inject prompts as keystrokes. Distinct
1193
- * from the headless `prompt()`/`events()` surface above. Lazy — does not hit
1194
- * the network until a handle method is called.
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/tangle/tee-intercept.ts
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
- * A 404 carrying hono's own unmatched-route body came from the router.
1588
+ * TerminalStream — typed client for the sandbox terminal WebSocket protocol.
1266
1589
  *
1267
- * hono answers an unmatched route with exactly "404 Not Found", so that body
1268
- * is the deployment saying it has no such route — which for `/tee` means the
1269
- * operator intercept is absent, not that the sandbox is missing.
1270
- * `ensureRunning()` has already confirmed the sandbox at this point, which is
1271
- * what makes the distinction safe to draw.
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
- * Returns the successful response for the caller to parse. Throws
1280
- * `CapabilityError` when the intercept is absent, and otherwise the same
1281
- * parsed error every other SDK call throws.
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 (`WS_MAX_PAYLOAD_BYTES` in
1323
- * `apps/sidecar/src/routes/terminal-ws.ts`) and drops the connection on
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 [`${SIDECAR_BEARER_SUBPROTOCOL_PREFIX}${token}`, TERMINAL_WS_ECHO_SUBPROTOCOL];
1368
- return [`${EDGE_BEARER_SUBPROTOCOL_PREFIX}${base64UrlEncodeUtf8(token)}`, TERMINAL_WS_ECHO_SUBPROTOCOL];
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 < MIN_DIMENSION || value > MAX_DIMENSION) throw new TerminalStreamError(`${name} must be an integer in [${MIN_DIMENSION}, ${MAX_DIMENSION}], got ${value}`, "INVALID_DIMENSION");
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 <= INPUT_FRAME_CHUNK_CHARS) {
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 + INPUT_FRAME_CHUNK_CHARS, text.length);
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
- return new TerminalStream(options, await resolveTerminalToken(options.token)).establish();
1440
- }
1441
- constructor(options, resolvedToken) {
1442
- if (!options.connectionId.trim()) throw new TerminalStreamError("Terminal stream connectionId must not be empty", "INVALID_CONNECTION_ID");
1443
- if (options.cols !== void 0) assertDimension("cols", options.cols);
1444
- if (options.rows !== void 0) assertDimension("rows", options.rows);
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 = options;
1448
- this.handlers = options.handlers ?? {};
1449
- this.ws = new WebSocketCtor(toWebSocketUrl(options.url), buildAuthSubprotocols(options.authFormat ?? "edge", resolvedToken));
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
- clearTimeout(timer);
1850
+ cleanup();
1464
1851
  resolve(this);
1465
1852
  };
1466
1853
  const fail = (error) => {
1467
1854
  if (settled) return;
1468
1855
  settled = true;
1469
- clearTimeout(timer);
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
- this.ws.send(JSON.stringify({
1474
- type: "init",
1475
- connectionId: this.options.connectionId,
1476
- cols: this.options.cols ?? DEFAULT_COLS,
1477
- rows: this.options.rows ?? DEFAULT_ROWS,
1478
- ...this.options.command !== void 0 ? { command: this.options.command } : {},
1479
- ...this.options.cwd !== void 0 ? { cwd: this.options.cwd } : {},
1480
- ...this.options.env !== void 0 ? { env: this.options.env } : {}
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(data, connectPhase) {
1503
- if (typeof data !== "string") {
1504
- if (data instanceof ArrayBuffer) this.handlers.onData?.(new Uint8Array(data));
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
- let message;
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
- message = JSON.parse(data);
1912
+ decoded = JSON.parse(raw);
1510
1913
  } catch {
1511
- this.handlers.onError?.(new TerminalStreamError("Terminal control frame is not valid JSON", "INVALID_FRAME"));
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
- switch (message.type) {
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: String(message.connectionId ?? ""),
1518
- sessionId: String(message.sessionId ?? ""),
1519
- restored: message.restored === true,
1520
- detachTimeoutMs: Number(message.detachTimeoutMs ?? 0)
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: typeof message.exitCode === "number" ? message.exitCode : void 0,
1530
- exitSignal: typeof message.exitSignal === "string" ? message.exitSignal : void 0
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(typeof message.message === "string" ? message.message : "terminal error", typeof message.code === "string" ? message.code : "UNKNOWN");
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.ws.send(JSON.stringify({
1580
- type: "resize",
1581
- cols,
1582
- rows
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
- * Detach cleanly: close the socket and resolve once the close completes.
1587
- * The PTY survives server-side for `ready.detachTimeoutMs` (5 minutes
1588
- * today) — reattach within that window via {@link reconnect} (or a fresh
1589
- * `connect()` with the same connectionId) to replay history and continue.
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
- close() {
1592
- if (this.closed) return Promise.resolve();
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
- * Re-dial the same endpoint with the same connectionId on a fresh socket
1600
- * — the server reattaches the PTY and replays its buffered history. When
1601
- * the stream was constructed with a token PROVIDER, it is re-invoked for
1602
- * the new dial (scoped terminal tokens expire within minutes, so the
1603
- * original credential may be dead by reconnect time); with a plain string
1604
- * token, pass a fresh `token` override yourself when it may have expired.
1605
- * `handlers` rebinds callbacks for the new stream.
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
- reconnect(overrides) {
1608
- return TerminalStream.connect({
1609
- ...this.options,
1610
- token: overrides?.token ?? this.options.token,
1611
- handlers: overrides?.handlers ?? this.options.handlers,
1612
- cols: overrides?.cols ?? this.options.cols,
1613
- rows: overrides?.rows ?? this.options.rows
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
- assertOpen(action) {
1617
- if (!this.isOpen) throw new TerminalStreamError(`Cannot ${action}: terminal stream is not open`, "NOT_OPEN");
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 self = this;
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
- async status() {
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 this.status();
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
- async kill(signal = "SIGTERM", options) {
4488
- const response = await self.runtimeFetch(`/process/${pid}`, {
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 this.logs()) if (entry.type === "stdout") yield entry.data;
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 this.logs()) if (entry.type === "stderr") yield entry.data;
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
- if ((opts.scope === "session" || opts.scope === "session-runtime") && !opts.sessionId) throw new ValidationError(`${opts.scope} scope requires a sessionId`);
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 — invoked by InteractiveSessionHandle.start(). */
6363
- async _startInteractive(id, options) {
6364
- assertCurrentBackendType(options.harness, "harness");
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 — invoked by InteractiveSessionHandle.sendPrompt(). */
6382
- async _sendInteractivePrompt(id, prompt) {
6383
- await this.ensureRunning();
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 — invoked by InteractiveSessionHandle.status(). */
6394
- async _interactiveStatus(id) {
6395
- await this.ensureRunning();
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
- * @internal Test seam: unit tests replace this to intercept terminal
6411
- * WebSocket dials without opening a real socket. Production paths always
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 — invoked by InteractiveSessionHandle.attach(). */
6426
- async _attachInteractive(id, options = {}) {
6427
- const status = await this._interactiveStatus(id);
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
- * Per-dial credential for a terminal attach. The first dial consumes the
6451
- * token minted alongside the proxy URL (no second mint); every later dial
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 — invoked by `box.terminals.attach()`. */
7339
+ /** @internal — delegated to the native interactive operations module. */
6471
7340
  async _attachTerminal(connectionId, options = {}) {
6472
- if (!connectionId.trim()) throw new ValidationError("Terminal connectionId is required");
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 — invoked by InteractiveSessionHandle.stop(). */
6490
- async _stopInteractive(id) {
6491
- await this.ensureRunning();
6492
- const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, { method: "DELETE" });
6493
- if (!response.ok && response.status !== 404) {
6494
- const body = await response.text();
6495
- throw parseErrorResponse(response.status, body, void 0, response.headers);
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 { toOtelJson as C, otelTraceIdForTangleTrace as S, getSandboxEventText as _, WorkspaceImages as a, buildTraceExportPayload as b, TERMINAL_WS_ECHO_SUBPROTOCOL as c, SandboxTaskSession as d, SandboxSession as f, collectAgentResponseText as g, collectAgentFinalMessageText as h, normalizeStartupDiagnostics as i, TerminalStream as l, applySandboxEventText as m, createSandboxInstanceFromResponse as n, GPU_LEASE_PROVIDER_NAMES as o, InteractiveSessionHandle as p, normalizeSandboxCreateReceipt as r, MAX_EGRESS_DENIALS_LIMIT as s, SandboxInstance as t, TerminalStreamError as u, isToolBearingEvent as v, parseSSEStream as w, exportTraceBundle as x, normalizeConnection as y };
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 };