@coder/ai-sdk-sandbox 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -496,9 +496,2095 @@ function waitForLocalPort(port, child, timeoutMs, abortSignal) {
496
496
  });
497
497
  }
498
498
 
499
+ // src/native-api.ts
500
+ import { readFile } from "fs/promises";
501
+ import { setTimeout as delay } from "timers/promises";
502
+ import { parse as parseYaml } from "yaml";
503
+ var DEFAULT_BUILD_POLL_INTERVAL_MS = 1e3;
504
+ var DEFAULT_BUILD_TIMEOUT_MS = 30 * 6e4;
505
+ var MAX_API_REDIRECTS = 10;
506
+ var CoderNativeApiError = class extends Error {
507
+ status;
508
+ method;
509
+ path;
510
+ detail;
511
+ constructor(options) {
512
+ super(
513
+ `Coder API ${options.method} ${options.path} failed (${options.status}): ${options.message}` + (options.detail ? `: ${options.detail}` : "")
514
+ );
515
+ this.name = "CoderNativeApiError";
516
+ this.status = options.status;
517
+ this.method = options.method;
518
+ this.path = options.path;
519
+ this.detail = options.detail;
520
+ }
521
+ };
522
+ function parseNativeWorkspaceRef(ref) {
523
+ const parts = ref.split("/");
524
+ if (parts.length > 2 || parts.some((part) => part === "")) {
525
+ throw new Error(`invalid workspace reference "${ref}"; expected [owner/]name[.agent]`);
526
+ }
527
+ const [first = "", second] = parts;
528
+ const owner = parts.length === 2 ? first : "me";
529
+ const nameAndAgent = second ?? first;
530
+ const dot = nameAndAgent.indexOf(".");
531
+ const name = dot === -1 ? nameAndAgent : nameAndAgent.slice(0, dot);
532
+ const agent = dot === -1 ? void 0 : nameAndAgent.slice(dot + 1);
533
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name) || agent === "") {
534
+ throw new Error(`invalid workspace reference "${ref}"; expected [owner/]name[.agent]`);
535
+ }
536
+ if (agent !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(agent)) {
537
+ throw new Error(`invalid workspace reference "${ref}"; expected [owner/]name[.agent]`);
538
+ }
539
+ return { owner, name, ...agent === void 0 ? {} : { agent } };
540
+ }
541
+ var CoderApiClient = class {
542
+ baseUrl;
543
+ token;
544
+ headers;
545
+ #fetch;
546
+ #buildPollIntervalMs;
547
+ #buildTimeoutMs;
548
+ #lifecycleTails = /* @__PURE__ */ new Map();
549
+ constructor(options) {
550
+ const parsed = new URL(options.url);
551
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
552
+ throw new Error(`Coder URL must use http or https, got ${parsed.protocol}`);
553
+ }
554
+ if (options.token === "") throw new Error("Coder session token must not be empty");
555
+ this.baseUrl = options.url.replace(/\/$/, "");
556
+ this.token = options.token;
557
+ this.headers = { ...options.headers };
558
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
559
+ this.#buildPollIntervalMs = options.buildPollIntervalMs ?? DEFAULT_BUILD_POLL_INTERVAL_MS;
560
+ this.#buildTimeoutMs = options.buildTimeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS;
561
+ }
562
+ websocketUrl(path2) {
563
+ const url = this.#url(path2);
564
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
565
+ return url.toString();
566
+ }
567
+ websocketHeaders() {
568
+ return { ...this.headers, "Coder-Session-Token": this.token };
569
+ }
570
+ async workspace(ref, signal) {
571
+ const { owner, name } = parseNativeWorkspaceRef(ref);
572
+ try {
573
+ return await this.request(
574
+ "GET",
575
+ `/api/v2/users/${encodeURIComponent(owner)}/workspace/${encodeURIComponent(name)}`,
576
+ void 0,
577
+ signal
578
+ );
579
+ } catch (error) {
580
+ if (error instanceof CoderNativeApiError && error.status === 404) return null;
581
+ throw error;
582
+ }
583
+ }
584
+ async currentUserId(signal) {
585
+ const user = await this.request("GET", "/api/v2/users/me", void 0, signal);
586
+ return user.id;
587
+ }
588
+ async requireWorkspace(ref, signal) {
589
+ const workspace = await this.workspace(ref, signal);
590
+ if (workspace === null) throw new Error(`Coder workspace "${ref}" does not exist`);
591
+ return workspace;
592
+ }
593
+ async resolveAgent(ref, signal) {
594
+ const parsed = parseNativeWorkspaceRef(ref);
595
+ const workspace = await this.requireWorkspace(ref, signal);
596
+ const agents = workspace.latest_build.resources?.flatMap((resource) => resource.agents ?? []) ?? [];
597
+ const agent = parsed.agent === void 0 ? agents.length === 1 ? agents[0] : void 0 : agents.find((candidate) => candidate.name === parsed.agent);
598
+ if (agent !== void 0) return { workspace, agent };
599
+ if (agents.length === 0) {
600
+ throw new Error(`Coder workspace "${ref}" has no agents in its latest build`);
601
+ }
602
+ if (parsed.agent !== void 0) {
603
+ throw new Error(
604
+ `Coder workspace "${parsed.owner}/${parsed.name}" has no agent "${parsed.agent}"; available agents: ${agents.map((candidate) => candidate.name).join(", ")}`
605
+ );
606
+ }
607
+ throw new Error(
608
+ `Coder workspace "${ref}" has multiple agents (${agents.map((candidate) => candidate.name).join(", ")}); select one with workspace.agent`
609
+ );
610
+ }
611
+ async status(ref, options) {
612
+ const workspace = await this.workspace(ref, options?.abortSignal);
613
+ return workspace === null ? null : toWorkspaceStatus(workspace);
614
+ }
615
+ async start(ref, options) {
616
+ const signal = options?.abortSignal;
617
+ const initial = await this.requireWorkspace(ref, signal);
618
+ await this.#withLifecycleLock(initial.id, signal, async () => {
619
+ let workspace = await this.requireWorkspace(ref, signal);
620
+ for (; ; ) {
621
+ if (workspace.latest_build.status === "running") return;
622
+ if (isWorkspaceBuildInFlight(workspace.latest_build)) {
623
+ const transition = workspace.latest_build.transition;
624
+ const completed = await this.#waitForBuild(workspace.latest_build.id, signal, true);
625
+ if (transition === "start" && completed.job?.status === "succeeded") return;
626
+ workspace = await this.requireWorkspace(ref, signal);
627
+ continue;
628
+ }
629
+ if (workspace.latest_build.status === "failed" && workspace.latest_build.transition === "start") {
630
+ const cleanup = await this.#createBuild(workspace.id, { transition: "stop" }, signal);
631
+ await this.#waitForBuild(cleanup.id, signal);
632
+ workspace = await this.requireWorkspace(ref, signal);
633
+ continue;
634
+ }
635
+ if (workspace.dormant_at) {
636
+ await this.request(
637
+ "PUT",
638
+ `/api/v2/workspaces/${workspace.id}/dormant`,
639
+ { dormant: false },
640
+ signal
641
+ );
642
+ }
643
+ const templateVersionId = workspace.automatic_updates === "always" || workspace.template_require_active_version ? workspace.template_active_version_id : workspace.latest_build.template_version_id;
644
+ const build = await this.#createBuild(
645
+ workspace.id,
646
+ {
647
+ transition: "start",
648
+ ...templateVersionId ? { template_version_id: templateVersionId } : {}
649
+ },
650
+ signal
651
+ );
652
+ await this.#waitForBuild(build.id, signal);
653
+ return;
654
+ }
655
+ });
656
+ }
657
+ async stop(ref, options) {
658
+ const signal = options?.abortSignal;
659
+ const initial = await this.requireWorkspace(ref, signal);
660
+ await this.#withLifecycleLock(initial.id, signal, async () => {
661
+ let workspace = await this.requireWorkspace(ref, signal);
662
+ for (; ; ) {
663
+ if (workspace.latest_build.status === "stopped") return;
664
+ if (isWorkspaceBuildInFlight(workspace.latest_build)) {
665
+ const transition = workspace.latest_build.transition;
666
+ const completed = await this.#waitForBuild(workspace.latest_build.id, signal, true);
667
+ if (transition === "stop" && completed.job?.status === "succeeded") return;
668
+ workspace = await this.requireWorkspace(ref, signal);
669
+ continue;
670
+ }
671
+ const build = await this.#createBuild(workspace.id, { transition: "stop" }, signal);
672
+ await this.#waitForBuild(build.id, signal);
673
+ return;
674
+ }
675
+ });
676
+ }
677
+ async destroy(ref, options) {
678
+ const signal = options?.abortSignal;
679
+ const initial = await this.workspace(ref, signal);
680
+ if (initial === null) return;
681
+ await this.#withLifecycleLock(initial.id, signal, async () => {
682
+ let workspace = await this.workspace(ref, signal);
683
+ for (; ; ) {
684
+ if (workspace === null || workspace.latest_build.status === "deleted") return;
685
+ if (isWorkspaceBuildInFlight(workspace.latest_build)) {
686
+ const transition = workspace.latest_build.transition;
687
+ const completed = await this.#waitForBuild(workspace.latest_build.id, signal, true);
688
+ if (transition === "delete" && completed.job?.status === "succeeded") return;
689
+ workspace = await this.workspace(ref, signal);
690
+ continue;
691
+ }
692
+ const build = await this.#createBuild(workspace.id, { transition: "delete" }, signal);
693
+ await this.#waitForBuild(build.id, signal);
694
+ return;
695
+ }
696
+ });
697
+ }
698
+ async create(options) {
699
+ const ref = parseNativeWorkspaceRef(options.workspace);
700
+ if (ref.agent !== void 0) {
701
+ throw new Error("a workspace agent cannot be selected while creating a workspace");
702
+ }
703
+ const template = await this.#resolveTemplate(
704
+ options.template,
705
+ options.org,
706
+ options.abortSignal
707
+ );
708
+ const versionId = options.templateVersion ? (await this.request(
709
+ "GET",
710
+ `/api/v2/templates/${template.id}/versions/${encodeURIComponent(options.templateVersion)}`,
711
+ void 0,
712
+ options.abortSignal
713
+ )).id : template.active_version_id;
714
+ const noPreset = options.preset?.toLowerCase() === "none";
715
+ const presets = noPreset ? [] : await this.#presets(versionId, options.abortSignal);
716
+ const preset = options.preset === void 0 ? presets.find(presetDefault) : presets.find((candidate) => presetName(candidate) === options.preset);
717
+ if (options.preset !== void 0 && !noPreset && preset === void 0) {
718
+ throw new Error(
719
+ `preset "${options.preset}" not found for template "${options.template}"; available presets: ${presets.map(presetName).join(", ") || "none"}`
720
+ );
721
+ }
722
+ const fileParameters = options.parameterFile ? await readParameterFile(options.parameterFile) : {};
723
+ const parameterValues = {
724
+ ...fileParameters,
725
+ ...options.parameters,
726
+ ...options.ephemeralParameters,
727
+ ...preset ? presetParameterValues(preset) : {}
728
+ };
729
+ const templateParameters = await this.#templateParameters(
730
+ template,
731
+ versionId,
732
+ ref.owner,
733
+ parameterValues,
734
+ options.abortSignal
735
+ );
736
+ const resolvedParameterValues = resolveCreateParameterValues(
737
+ templateParameters,
738
+ parameterValues,
739
+ options.useParameterDefaults === true
740
+ );
741
+ const body = {
742
+ template_version_id: versionId,
743
+ name: ref.name,
744
+ ...options.stopAfter ? { ttl_ms: parseDurationMillis(options.stopAfter) } : {},
745
+ ...Object.keys(resolvedParameterValues).length > 0 ? {
746
+ rich_parameter_values: Object.entries(resolvedParameterValues).map(([name, value]) => ({
747
+ name,
748
+ value
749
+ }))
750
+ } : {},
751
+ ...options.automaticUpdates ? { automatic_updates: options.automaticUpdates } : {},
752
+ ...preset ? { template_version_preset_id: presetId(preset) } : {}
753
+ };
754
+ const workspace = await this.request(
755
+ "POST",
756
+ `/api/v2/users/${encodeURIComponent(ref.owner)}/workspaces`,
757
+ body,
758
+ options.abortSignal
759
+ );
760
+ await this.#waitForBuild(workspace.latest_build.id, options.abortSignal);
761
+ }
762
+ async listPresets(options) {
763
+ const template = await this.#resolveTemplate(
764
+ options.template,
765
+ options.org,
766
+ options.abortSignal
767
+ );
768
+ const versionId = options.templateVersion ? (await this.request(
769
+ "GET",
770
+ `/api/v2/templates/${template.id}/versions/${encodeURIComponent(options.templateVersion)}`,
771
+ void 0,
772
+ options.abortSignal
773
+ )).id : template.active_version_id;
774
+ const presets = await this.#presets(versionId, options.abortSignal);
775
+ return presets.map((preset) => {
776
+ const description = preset.Description ?? preset.description;
777
+ return {
778
+ name: presetName(preset),
779
+ default: preset.Default ?? preset.default ?? false,
780
+ ...description ? { description } : {}
781
+ };
782
+ });
783
+ }
784
+ async request(method, path2, body, signal) {
785
+ const requestHeaders = {
786
+ ...this.headers,
787
+ "Coder-Session-Token": this.token,
788
+ Accept: "application/json"
789
+ };
790
+ if (body !== void 0) requestHeaders["Content-Type"] = "application/json";
791
+ const initialUrl = this.#url(path2);
792
+ let requestUrl = initialUrl;
793
+ let requestMethod = method.toUpperCase();
794
+ let requestBody = body === void 0 ? void 0 : JSON.stringify(body);
795
+ let redirectCount = 0;
796
+ let response;
797
+ for (; ; ) {
798
+ response = await waitWithAbort(
799
+ this.#fetch(requestUrl, {
800
+ method: requestMethod,
801
+ headers: requestHeaders,
802
+ body: requestBody,
803
+ signal,
804
+ redirect: "manual"
805
+ }),
806
+ signal
807
+ );
808
+ const location = response.headers.get("location");
809
+ if (!isRedirectStatus(response.status) || location === null) break;
810
+ redirectCount += 1;
811
+ if (redirectCount > MAX_API_REDIRECTS) {
812
+ cancelResponseBody(response);
813
+ throw new Error(
814
+ `Coder API ${method} ${path2} exceeded ${MAX_API_REDIRECTS} same-origin redirects`
815
+ );
816
+ }
817
+ const redirectedUrl = new URL(location, requestUrl);
818
+ if (redirectedUrl.origin !== initialUrl.origin) {
819
+ cancelResponseBody(response);
820
+ throw new Error(
821
+ `Coder API ${method} ${path2} refused cross-origin redirect from ${initialUrl.origin} to ${redirectedUrl.origin}`
822
+ );
823
+ }
824
+ cancelResponseBody(response);
825
+ if (response.status === 303 && requestMethod !== "GET" && requestMethod !== "HEAD" || (response.status === 301 || response.status === 302) && requestMethod === "POST") {
826
+ requestMethod = "GET";
827
+ requestBody = void 0;
828
+ deleteRequestBodyHeaders(requestHeaders);
829
+ }
830
+ requestUrl = redirectedUrl;
831
+ }
832
+ const text = await readResponseText(response, signal);
833
+ let parsed;
834
+ try {
835
+ parsed = text === "" ? void 0 : JSON.parse(text);
836
+ } catch {
837
+ parsed = void 0;
838
+ }
839
+ if (!response.ok) {
840
+ const error = asRecord2(parsed);
841
+ const validations = error.validations?.map((validation) => [validation.field, validation.detail].filter(Boolean).join(": ")).filter(Boolean).join("; ");
842
+ throw new CoderNativeApiError({
843
+ status: response.status,
844
+ method,
845
+ path: path2,
846
+ message: error.message ?? (response.statusText || "request failed"),
847
+ detail: [error.detail, validations, parsed === void 0 ? text.slice(0, 500) : void 0].filter(Boolean).join("; ") || void 0
848
+ });
849
+ }
850
+ return parsed;
851
+ }
852
+ #url(path2) {
853
+ const base = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
854
+ return new URL(path2.replace(/^\//, ""), base);
855
+ }
856
+ async #createBuild(workspaceId, body, signal) {
857
+ return await this.request(
858
+ "POST",
859
+ `/api/v2/workspaces/${workspaceId}/builds`,
860
+ body,
861
+ signal
862
+ );
863
+ }
864
+ async #withLifecycleLock(workspaceId, signal, operation) {
865
+ const previous = this.#lifecycleTails.get(workspaceId) ?? Promise.resolve();
866
+ let release;
867
+ const gate = new Promise((resolve) => {
868
+ release = resolve;
869
+ });
870
+ const tail = previous.then(() => gate);
871
+ this.#lifecycleTails.set(workspaceId, tail);
872
+ void tail.then(() => {
873
+ if (this.#lifecycleTails.get(workspaceId) === tail) {
874
+ this.#lifecycleTails.delete(workspaceId);
875
+ }
876
+ });
877
+ try {
878
+ await waitWithAbort(previous, signal);
879
+ return await operation();
880
+ } finally {
881
+ release();
882
+ }
883
+ }
884
+ async #waitForBuild(buildId, signal, allowCanceled = false) {
885
+ if (signal?.aborted) throw abortReason(signal);
886
+ const deadline = Date.now() + this.#buildTimeoutMs;
887
+ const timeoutError = () => new Error(`timed out after ${this.#buildTimeoutMs}ms waiting for Coder build ${buildId}`);
888
+ for (; ; ) {
889
+ const remaining = deadline - Date.now();
890
+ if (remaining <= 0) throw timeoutError();
891
+ const controller = new AbortController();
892
+ let rejectInterrupted;
893
+ const interrupted = new Promise((_resolve, reject) => {
894
+ rejectInterrupted = reject;
895
+ });
896
+ const onAbort = () => {
897
+ const error = abortReason(signal);
898
+ rejectInterrupted(error);
899
+ controller.abort(error);
900
+ };
901
+ signal?.addEventListener("abort", onAbort, { once: true });
902
+ if (signal?.aborted) onAbort();
903
+ const timer = setTimeout(() => {
904
+ const error = timeoutError();
905
+ rejectInterrupted(error);
906
+ controller.abort(error);
907
+ }, remaining);
908
+ let build;
909
+ try {
910
+ build = await Promise.race([
911
+ this.request(
912
+ "GET",
913
+ `/api/v2/workspacebuilds/${buildId}`,
914
+ void 0,
915
+ controller.signal
916
+ ),
917
+ interrupted
918
+ ]);
919
+ } finally {
920
+ clearTimeout(timer);
921
+ signal?.removeEventListener("abort", onAbort);
922
+ }
923
+ const jobStatus = build.job?.status;
924
+ if (jobStatus === "succeeded") return build;
925
+ if (jobStatus === "canceled" && allowCanceled) return build;
926
+ if (jobStatus === "failed" || jobStatus === "canceled") {
927
+ throw new Error(
928
+ `Coder workspace ${build.transition} build ${buildId} ${jobStatus}` + (build.job?.error ? `: ${build.job.error}` : "") + (build.job?.error_code ? ` (${build.job.error_code})` : "")
929
+ );
930
+ }
931
+ const wait = Math.min(this.#buildPollIntervalMs, Math.max(0, deadline - Date.now()));
932
+ if (wait <= 0) throw timeoutError();
933
+ try {
934
+ await delay(wait, void 0, { signal });
935
+ } catch (error) {
936
+ if (signal?.aborted) throw abortReason(signal);
937
+ throw error;
938
+ }
939
+ }
940
+ }
941
+ async #resolveTemplate(name, org, signal) {
942
+ const query = new URLSearchParams({ q: `exact_name:"${name}"` });
943
+ const templates = await this.request(
944
+ "GET",
945
+ `/api/v2/templates?${query.toString()}`,
946
+ void 0,
947
+ signal
948
+ );
949
+ const matches = templates.filter(
950
+ (template) => template.name === name && (org === void 0 || template.organization_id === org || template.organization_name === org)
951
+ );
952
+ const [onlyMatch] = matches;
953
+ if (matches.length === 1 && onlyMatch !== void 0) return onlyMatch;
954
+ if (matches.length === 0) {
955
+ throw new Error(
956
+ `Coder template "${name}"${org ? ` in organization "${org}"` : ""} was not found`
957
+ );
958
+ }
959
+ throw new Error(
960
+ `Coder template "${name}" is ambiguous across organizations (${matches.map((template) => template.organization_name).join(", ")}); set org`
961
+ );
962
+ }
963
+ async #presets(versionId, signal) {
964
+ const presets = await this.request(
965
+ "GET",
966
+ `/api/v2/templateversions/${versionId}/presets`,
967
+ void 0,
968
+ signal
969
+ );
970
+ return Array.isArray(presets) ? presets : [];
971
+ }
972
+ async #templateParameters(template, versionId, owner, initialValues, signal) {
973
+ if (template.use_classic_parameter_flow !== false) {
974
+ const parameters = await this.request(
975
+ "GET",
976
+ `/api/v2/templateversions/${versionId}/rich-parameters`,
977
+ void 0,
978
+ signal
979
+ );
980
+ return Array.isArray(parameters) ? parameters : [];
981
+ }
982
+ const ownerId = owner === "me" ? void 0 : (await this.request(
983
+ "GET",
984
+ `/api/v2/users/${encodeURIComponent(owner)}`,
985
+ void 0,
986
+ signal
987
+ )).id;
988
+ const evaluation = await this.request(
989
+ "POST",
990
+ `/api/v2/templateversions/${versionId}/dynamic-parameters/evaluate`,
991
+ {
992
+ id: 0,
993
+ inputs: initialValues,
994
+ ...ownerId ? { owner_id: ownerId } : {}
995
+ },
996
+ signal
997
+ );
998
+ return (evaluation.parameters ?? []).map((parameter) => ({
999
+ name: parameter.name,
1000
+ ...parameter.display_name ? { display_name: parameter.display_name } : {},
1001
+ default_value: parameter.default_value?.value ?? "",
1002
+ ...parameter.default_value?.valid === void 0 ? {} : { default_valid: parameter.default_value.valid },
1003
+ required: parameter.required ?? false,
1004
+ ephemeral: parameter.ephemeral ?? false
1005
+ }));
1006
+ }
1007
+ };
1008
+ function toWorkspaceStatus(workspace) {
1009
+ const agents = workspace.latest_build.resources?.flatMap(
1010
+ (resource) => (resource.agents ?? []).map((agent) => ({
1011
+ name: agent.name,
1012
+ status: agent.status,
1013
+ lifecycleState: agent.lifecycle_state
1014
+ }))
1015
+ ) ?? [];
1016
+ return {
1017
+ id: workspace.id,
1018
+ name: workspace.name,
1019
+ buildStatus: workspace.latest_build.status,
1020
+ transition: workspace.latest_build.transition,
1021
+ agents
1022
+ };
1023
+ }
1024
+ function presetName(preset) {
1025
+ return preset.Name ?? preset.name ?? "";
1026
+ }
1027
+ function presetId(preset) {
1028
+ const id = preset.ID ?? preset.id;
1029
+ if (!id) throw new Error(`Coder preset "${presetName(preset)}" has no id`);
1030
+ return id;
1031
+ }
1032
+ function presetDefault(preset) {
1033
+ return preset.Default ?? preset.default ?? false;
1034
+ }
1035
+ function presetParameterValues(preset) {
1036
+ const result = {};
1037
+ for (const parameter of preset.Parameters ?? preset.parameters ?? []) {
1038
+ const name = parameter.Name ?? parameter.name;
1039
+ const value = parameter.Value ?? parameter.value;
1040
+ if (name !== void 0 && value !== void 0) result[name] = value;
1041
+ }
1042
+ return result;
1043
+ }
1044
+ function resolveCreateParameterValues(parameters, supplied, useDefaults) {
1045
+ const resolved = { ...supplied };
1046
+ const required = [];
1047
+ const awaitingDefaults = [];
1048
+ for (const parameter of parameters) {
1049
+ if (Object.hasOwn(resolved, parameter.name)) continue;
1050
+ if (parameter.ephemeral && !parameter.required) continue;
1051
+ const name = parameter.display_name || parameter.name;
1052
+ if (parameter.required) {
1053
+ required.push(name);
1054
+ } else if (useDefaults && parameter.default_valid !== false) {
1055
+ resolved[parameter.name] = parameter.default_value;
1056
+ } else {
1057
+ awaitingDefaults.push(name);
1058
+ }
1059
+ }
1060
+ if (required.length > 0) {
1061
+ const names = required.map((name) => `"${name}"`).join(", ");
1062
+ throw new Error(
1063
+ `required Coder workspace parameters have no defaults: ${names}; supply values with parameters, parameterFile, or a preset`
1064
+ );
1065
+ }
1066
+ if (awaitingDefaults.length > 0) {
1067
+ const names = awaitingDefaults.map((name) => `"${name}"`).join(", ");
1068
+ throw new Error(
1069
+ `Coder workspace parameters require explicit values: ${names}; supply values with parameters, parameterFile, or a preset` + (useDefaults ? "" : ", or set useParameterDefaults: true")
1070
+ );
1071
+ }
1072
+ return resolved;
1073
+ }
1074
+ function asRecord2(value) {
1075
+ return typeof value === "object" && value !== null ? value : {};
1076
+ }
1077
+ function isRedirectStatus(status) {
1078
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
1079
+ }
1080
+ function isWorkspaceBuildInFlight(build) {
1081
+ return build.status === "pending" || build.status === "starting" || build.status === "stopping" || build.status === "deleting" || build.status === "canceling";
1082
+ }
1083
+ function deleteRequestBodyHeaders(headers) {
1084
+ const bodyHeaders = /* @__PURE__ */ new Set([
1085
+ "content-encoding",
1086
+ "content-language",
1087
+ "content-location",
1088
+ "content-type"
1089
+ ]);
1090
+ for (const name of Object.keys(headers)) {
1091
+ if (bodyHeaders.has(name.toLowerCase())) delete headers[name];
1092
+ }
1093
+ }
1094
+ function cancelResponseBody(response) {
1095
+ try {
1096
+ void response.body?.cancel().catch(() => {
1097
+ });
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ function abortReason(signal) {
1102
+ if (signal?.reason instanceof Error) return signal.reason;
1103
+ return new DOMException("The operation was aborted", "AbortError");
1104
+ }
1105
+ async function waitWithAbort(promise, signal) {
1106
+ if (signal === void 0) return await promise;
1107
+ if (signal.aborted) throw abortReason(signal);
1108
+ let onAbort;
1109
+ const aborted = new Promise((_resolve, reject) => {
1110
+ onAbort = () => reject(abortReason(signal));
1111
+ signal.addEventListener("abort", onAbort, { once: true });
1112
+ });
1113
+ try {
1114
+ return await Promise.race([promise, aborted]);
1115
+ } finally {
1116
+ signal.removeEventListener("abort", onAbort);
1117
+ }
1118
+ }
1119
+ async function readResponseText(response, signal) {
1120
+ if (response.body === null) return "";
1121
+ const reader = response.body.getReader();
1122
+ const decoder = new TextDecoder();
1123
+ let text = "";
1124
+ let releaseAfterCancel = false;
1125
+ try {
1126
+ for (; ; ) {
1127
+ const { done, value } = await waitWithAbort(reader.read(), signal);
1128
+ if (done) return text + decoder.decode();
1129
+ text += decoder.decode(value, { stream: true });
1130
+ }
1131
+ } catch (error) {
1132
+ if (!signal?.aborted) throw error;
1133
+ const reason = abortReason(signal);
1134
+ releaseAfterCancel = true;
1135
+ try {
1136
+ void reader.cancel(reason).catch(() => {
1137
+ }).then(() => {
1138
+ try {
1139
+ reader.releaseLock();
1140
+ } catch {
1141
+ }
1142
+ });
1143
+ } catch {
1144
+ }
1145
+ throw reason;
1146
+ } finally {
1147
+ if (!releaseAfterCancel) reader.releaseLock();
1148
+ }
1149
+ }
1150
+ async function readParameterFile(path2) {
1151
+ let parsed;
1152
+ try {
1153
+ parsed = parseYaml(await readFile(path2, "utf8"));
1154
+ } catch (error) {
1155
+ throw new Error(`failed to parse Coder parameter file "${path2}"`, { cause: error });
1156
+ }
1157
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1158
+ throw new Error(`Coder parameter file "${path2}" must contain a YAML mapping`);
1159
+ }
1160
+ const result = {};
1161
+ for (const [name, value] of Object.entries(parsed)) {
1162
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
1163
+ result[name] = String(value);
1164
+ } else if (Array.isArray(value)) {
1165
+ result[name] = JSON.stringify(value);
1166
+ } else {
1167
+ throw new Error(
1168
+ `invalid value for Coder parameter "${name}" in "${path2}": expected string, number, boolean, or list`
1169
+ );
1170
+ }
1171
+ }
1172
+ return result;
1173
+ }
1174
+ function parseDurationMillis(input) {
1175
+ const unitMillis = {
1176
+ ms: 1,
1177
+ s: 1e3,
1178
+ m: 6e4,
1179
+ h: 36e5
1180
+ };
1181
+ const pattern = /(\d+(?:\.\d+)?)(ms|s|m|h)/gy;
1182
+ let total = 0;
1183
+ let offset = 0;
1184
+ for (; ; ) {
1185
+ pattern.lastIndex = offset;
1186
+ const match = pattern.exec(input);
1187
+ if (match === null) break;
1188
+ const amount = match[1];
1189
+ const multiplier = match[2] === void 0 ? void 0 : unitMillis[match[2]];
1190
+ if (amount === void 0 || multiplier === void 0) break;
1191
+ total += Number(amount) * multiplier;
1192
+ offset = pattern.lastIndex;
1193
+ }
1194
+ if (offset !== input.length || offset === 0 || !Number.isFinite(total) || total < 0) {
1195
+ throw new Error(
1196
+ `invalid stopAfter duration "${input}"; expected a Go-style duration such as "8h" or "1h30m"`
1197
+ );
1198
+ }
1199
+ return Math.round(total);
1200
+ }
1201
+
1202
+ // src/native-relay.ts
1203
+ import { randomUUID } from "crypto";
1204
+ import net2 from "net";
1205
+ import WebSocket from "ws";
1206
+ var RELAY_PROTOCOL_VERSION = 1;
1207
+ var CARRIER_HIGH_WATER_MARK = 1024 * 1024;
1208
+ var CARRIER_LOW_WATER_MARK = 256 * 1024;
1209
+ var CARRIER_DRAIN_POLL_INTERVAL_MS = 10;
1210
+ var PROCESS_STDIN_CHUNK_BYTES = 64 * 1024;
1211
+ var PROCESS_STDIN_BATCH_BYTES = 256 * 1024;
1212
+ var RELAY_CLOSE_GRACE_MS = 1e3;
1213
+ var NATIVE_RELAY_BOOTSTRAP_MARKER = "__CODER_AI_SDK_RELAY_BOOTSTRAP_READY_V1__";
1214
+ var BOOTSTRAP_DIAGNOSTIC_LIMIT = 500;
1215
+ var BOOTSTRAP_FRAME_LIMIT = 64 * 1024;
1216
+ var PROTOCOL_FRAME_LIMIT = 1024 * 1024;
1217
+ var NATIVE_RELAY_SOURCE = String.raw`'use strict';
1218
+ const childProcess = require('node:child_process');
1219
+ const fs = require('node:fs');
1220
+ const net = require('node:net');
1221
+ const os = require('node:os');
1222
+ const path = require('node:path');
1223
+ const readline = require('node:readline');
1224
+ const processes = new Map();
1225
+ const discardedProcessOutputs = new Map();
1226
+ const processOutputPauses = new Map();
1227
+ const processInputPauses = new Set();
1228
+ const sockets = new Map();
1229
+ const socketOutputPauses = new Map();
1230
+ const signalNumbers = os.constants.signals;
1231
+ const childShutdownGraceMs = 500;
1232
+ let outputCarrierPaused = false;
1233
+ function resolveExecutable(name) {
1234
+ for (const directory of String(process.env.PATH || '/usr/local/bin:/usr/bin:/bin').split(path.delimiter)) {
1235
+ const candidate = path.resolve(directory || '.', name);
1236
+ try { fs.accessSync(candidate, fs.constants.X_OK); return candidate; } catch (_) {}
1237
+ }
1238
+ return name;
1239
+ }
1240
+ const bashPath = resolveExecutable('bash');
1241
+ function processStream(child, streamName) {
1242
+ return streamName === 'stdout' ? child && child.stdout : streamName === 'stderr' ? child && child.stderr : undefined;
1243
+ }
1244
+ function setProcessOutputPaused(id, streamName, reason, paused) {
1245
+ const child = processes.get(id);
1246
+ const pauses = processOutputPauses.get(id);
1247
+ const stream = processStream(child, streamName);
1248
+ const reasons = pauses && pauses[streamName];
1249
+ if (!stream || !reasons) return;
1250
+ if (discardedProcessOutputs.get(id)?.has(streamName)) {
1251
+ reasons.clear();
1252
+ stream.resume();
1253
+ return;
1254
+ }
1255
+ if (paused) {
1256
+ reasons.add(reason);
1257
+ stream.pause();
1258
+ } else if (reasons.delete(reason) && reasons.size === 0) {
1259
+ stream.resume();
1260
+ }
1261
+ }
1262
+ function setSocketOutputPaused(id, reason, paused) {
1263
+ const socket = sockets.get(id);
1264
+ const reasons = socketOutputPauses.get(id);
1265
+ if (!socket || !reasons) return;
1266
+ if (paused) {
1267
+ reasons.add(reason);
1268
+ socket.pause();
1269
+ } else if (reasons.delete(reason) && reasons.size === 0) {
1270
+ socket.resume();
1271
+ }
1272
+ }
1273
+ function pauseOutputCarrier() {
1274
+ if (outputCarrierPaused) return;
1275
+ outputCarrierPaused = true;
1276
+ for (const id of processes.keys()) {
1277
+ setProcessOutputPaused(id, 'stdout', 'carrier', true);
1278
+ setProcessOutputPaused(id, 'stderr', 'carrier', true);
1279
+ }
1280
+ for (const id of sockets.keys()) setSocketOutputPaused(id, 'carrier', true);
1281
+ }
1282
+ function resumeOutputCarrier() {
1283
+ if (!outputCarrierPaused) return;
1284
+ outputCarrierPaused = false;
1285
+ for (const id of processes.keys()) {
1286
+ setProcessOutputPaused(id, 'stdout', 'carrier', false);
1287
+ if (outputCarrierPaused) return;
1288
+ setProcessOutputPaused(id, 'stderr', 'carrier', false);
1289
+ if (outputCarrierPaused) return;
1290
+ }
1291
+ for (const id of sockets.keys()) {
1292
+ setSocketOutputPaused(id, 'carrier', false);
1293
+ if (outputCarrierPaused) return;
1294
+ }
1295
+ }
1296
+ function emit(message) {
1297
+ if (!process.stdout.write(JSON.stringify(Object.assign({ v: 1 }, message)) + '\n')) pauseOutputCarrier();
1298
+ }
1299
+ function bytes(value) {
1300
+ return Buffer.from(value || '', 'base64');
1301
+ }
1302
+ function shellQuote(value) {
1303
+ return "'" + String(value).replace(/'/g, "'\\''") + "'";
1304
+ }
1305
+ function commandScript(message) {
1306
+ const directory = message.cwd ? 'cd ' + shellQuote(message.cwd) + ' && ' : '';
1307
+ const entries = Object.entries(message.env || {});
1308
+ if (entries.length === 0) return directory + message.command;
1309
+ const assignments = entries.map(([key, value]) => shellQuote(key + '=' + String(value))).join(' ');
1310
+ return directory + 'exec env ' + assignments + ' ' + shellQuote(bashPath) + ' -c ' + shellQuote(message.command);
1311
+ }
1312
+ function processExitCode(code, signal) {
1313
+ if (typeof code === 'number') return code;
1314
+ return 128 + (signalNumbers[signal] || 0);
1315
+ }
1316
+ function start(message) {
1317
+ if (processes.has(message.id)) {
1318
+ emit({ type: 'proc-error', id: message.id, message: 'duplicate process id' });
1319
+ return;
1320
+ }
1321
+ const args = [message.loginShell === false ? '-c' : '-lc', commandScript(message)];
1322
+ let child;
1323
+ try {
1324
+ child = childProcess.spawn(bashPath, args, {
1325
+ env: process.env,
1326
+ detached: process.platform !== 'win32',
1327
+ stdio: ['pipe', 'pipe', 'pipe'],
1328
+ });
1329
+ } catch (error) {
1330
+ emit({ type: 'proc-error', id: message.id, message: String(error && error.message || error) });
1331
+ return;
1332
+ }
1333
+ const discardedOutputs = new Set();
1334
+ processes.set(message.id, child);
1335
+ discardedProcessOutputs.set(message.id, discardedOutputs);
1336
+ processOutputPauses.set(message.id, { stdout: new Set(), stderr: new Set() });
1337
+ child.once('spawn', () => emit({ type: 'started', id: message.id, pid: child.pid }));
1338
+ child.stdout.on('data', (data) => {
1339
+ if (!discardedOutputs.has('stdout')) emit({ type: 'stdout', id: message.id, data: data.toString('base64') });
1340
+ });
1341
+ child.stderr.on('data', (data) => {
1342
+ if (!discardedOutputs.has('stderr')) emit({ type: 'stderr', id: message.id, data: data.toString('base64') });
1343
+ });
1344
+ if (outputCarrierPaused) {
1345
+ setProcessOutputPaused(message.id, 'stdout', 'carrier', true);
1346
+ setProcessOutputPaused(message.id, 'stderr', 'carrier', true);
1347
+ }
1348
+ child.once('error', (error) => {
1349
+ processes.delete(message.id);
1350
+ discardedProcessOutputs.delete(message.id);
1351
+ processOutputPauses.delete(message.id);
1352
+ processInputPauses.delete(message.id);
1353
+ emit({ type: 'proc-error', id: message.id, message: String(error && error.message || error) });
1354
+ });
1355
+ child.once('close', (code, signal) => {
1356
+ if (!processes.delete(message.id)) return;
1357
+ discardedProcessOutputs.delete(message.id);
1358
+ processOutputPauses.delete(message.id);
1359
+ processInputPauses.delete(message.id);
1360
+ emit({ type: 'exit', id: message.id, code: processExitCode(code, signal), signal: signal || undefined });
1361
+ });
1362
+ // Child exit is authoritative; a stdin EPIPE only means it stopped reading.
1363
+ child.stdin.on('error', () => { processInputPauses.delete(message.id); });
1364
+ child.stdin.once('close', () => {
1365
+ processInputPauses.delete(message.id);
1366
+ if (processes.has(message.id)) emit({ type: 'proc-stdin-close', id: message.id });
1367
+ });
1368
+ child.stdin.on('drain', () => {
1369
+ if (processInputPauses.delete(message.id) && processes.has(message.id)) {
1370
+ emit({ type: 'proc-stdin-resume', id: message.id });
1371
+ }
1372
+ });
1373
+ if (message.stdinMode !== 'stream') {
1374
+ if (message.stdin) child.stdin.write(bytes(message.stdin));
1375
+ child.stdin.end();
1376
+ }
1377
+ }
1378
+ function processStdinData(message) {
1379
+ const child = processes.get(message.id);
1380
+ if (!child || child.stdin.destroyed || child.stdin.writableEnded) return;
1381
+ try {
1382
+ if (!child.stdin.write(bytes(message.data)) && !processInputPauses.has(message.id)) {
1383
+ processInputPauses.add(message.id);
1384
+ emit({ type: 'proc-stdin-pause', id: message.id });
1385
+ }
1386
+ } catch (_) {}
1387
+ }
1388
+ function processStdinEnd(message) {
1389
+ const child = processes.get(message.id);
1390
+ if (!child || child.stdin.destroyed || child.stdin.writableEnded) return;
1391
+ try { child.stdin.end(); } catch (_) {}
1392
+ }
1393
+ function signalProcessGroup(pid, signal) {
1394
+ try {
1395
+ process.kill(process.platform !== 'win32' ? -pid : pid, signal);
1396
+ return true;
1397
+ } catch (_) {
1398
+ return false;
1399
+ }
1400
+ }
1401
+ function processGroupAlive(pid) {
1402
+ return signalProcessGroup(pid, 0);
1403
+ }
1404
+ function terminate(child, signal) {
1405
+ if (!child || !child.pid) return;
1406
+ if (signalProcessGroup(child.pid, signal)) return;
1407
+ try { child.kill(signal); } catch (_) {}
1408
+ }
1409
+ function discardProcessOutput(id, streamName) {
1410
+ const child = processes.get(id);
1411
+ const discardedOutputs = discardedProcessOutputs.get(id);
1412
+ const stream = processStream(child, streamName);
1413
+ if (!stream || !discardedOutputs) return;
1414
+ discardedOutputs.add(streamName);
1415
+ processOutputPauses.get(id)?.[streamName]?.clear();
1416
+ stream.resume();
1417
+ }
1418
+ function kill(message) {
1419
+ if (message.discardOutput) {
1420
+ discardProcessOutput(message.id, 'stdout');
1421
+ discardProcessOutput(message.id, 'stderr');
1422
+ }
1423
+ terminate(processes.get(message.id), message.signal || 'SIGTERM');
1424
+ }
1425
+ function tcpOpen(message) {
1426
+ if (sockets.has(message.id)) {
1427
+ emit({ type: 'tcp-error', id: message.id, message: 'duplicate socket id' });
1428
+ return;
1429
+ }
1430
+ let socket;
1431
+ try { socket = net.createConnection({ host: '127.0.0.1', port: message.port, allowHalfOpen: true }); }
1432
+ catch (error) {
1433
+ emit({ type: 'tcp-error', id: message.id, message: String(error && error.message || error) });
1434
+ emit({ type: 'tcp-close', id: message.id });
1435
+ return;
1436
+ }
1437
+ sockets.set(message.id, socket);
1438
+ socketOutputPauses.set(message.id, new Set());
1439
+ socket.once('connect', () => emit({ type: 'tcp-opened', id: message.id }));
1440
+ socket.on('data', (data) => emit({ type: 'tcp-data', id: message.id, data: data.toString('base64') }));
1441
+ socket.on('drain', () => emit({ type: 'tcp-resume', id: message.id }));
1442
+ socket.once('end', () => emit({ type: 'tcp-end', id: message.id }));
1443
+ socket.once('error', (error) => emit({ type: 'tcp-error', id: message.id, message: String(error && error.message || error) }));
1444
+ socket.once('close', () => {
1445
+ sockets.delete(message.id);
1446
+ socketOutputPauses.delete(message.id);
1447
+ emit({ type: 'tcp-close', id: message.id });
1448
+ });
1449
+ if (outputCarrierPaused) setSocketOutputPaused(message.id, 'carrier', true);
1450
+ }
1451
+ function receive(line) {
1452
+ if (!line) return;
1453
+ let message;
1454
+ try { message = JSON.parse(line); }
1455
+ catch (error) { emit({ type: 'error', message: 'invalid JSON: ' + String(error && error.message || error) }); return; }
1456
+ if (message.v !== 1) { emit({ type: 'error', message: 'unsupported protocol version' }); return; }
1457
+ switch (message.type) {
1458
+ case 'start': start(message); break;
1459
+ case 'proc-stdin': processStdinData(message); break;
1460
+ case 'proc-stdin-end': processStdinEnd(message); break;
1461
+ case 'kill': kill(message); break;
1462
+ case 'proc-pause': {
1463
+ setProcessOutputPaused(message.id, message.stream, 'host', true);
1464
+ break;
1465
+ }
1466
+ case 'proc-resume': {
1467
+ setProcessOutputPaused(message.id, message.stream, 'host', false);
1468
+ break;
1469
+ }
1470
+ case 'proc-discard': {
1471
+ discardProcessOutput(message.id, message.stream);
1472
+ break;
1473
+ }
1474
+ case 'tcp-open': tcpOpen(message); break;
1475
+ case 'tcp-data': {
1476
+ const socket = sockets.get(message.id);
1477
+ if (socket && !socket.write(bytes(message.data))) emit({ type: 'tcp-pause', id: message.id });
1478
+ break;
1479
+ }
1480
+ case 'tcp-end': { const socket = sockets.get(message.id); if (socket) socket.end(); break; }
1481
+ case 'tcp-close': { const socket = sockets.get(message.id); if (socket) socket.destroy(); break; }
1482
+ case 'tcp-pause': { setSocketOutputPaused(message.id, 'host', true); break; }
1483
+ case 'tcp-resume': { setSocketOutputPaused(message.id, 'host', false); break; }
1484
+ case 'ping': emit({ type: 'pong' }); break;
1485
+ default: emit({ type: 'error', id: message.id, message: 'unknown message type: ' + message.type });
1486
+ }
1487
+ }
1488
+ let shuttingDown = false;
1489
+ function shutdown(exitCode) {
1490
+ if (shuttingDown) return;
1491
+ shuttingDown = true;
1492
+ const groupPids = [];
1493
+ for (const [id, child] of processes) {
1494
+ discardProcessOutput(id, 'stdout');
1495
+ discardProcessOutput(id, 'stderr');
1496
+ if (child.pid) groupPids.push(child.pid);
1497
+ terminate(child, 'SIGTERM');
1498
+ }
1499
+ for (const socket of sockets.values()) socket.destroy();
1500
+ if (groupPids.length === 0) {
1501
+ process.exit(exitCode);
1502
+ return;
1503
+ }
1504
+ let pollTimer;
1505
+ let forceTimer;
1506
+ const finish = () => {
1507
+ if (pollTimer) clearInterval(pollTimer);
1508
+ if (forceTimer) clearTimeout(forceTimer);
1509
+ process.exit(exitCode);
1510
+ };
1511
+ pollTimer = setInterval(() => {
1512
+ if (groupPids.every((pid) => !processGroupAlive(pid))) finish();
1513
+ }, 10);
1514
+ forceTimer = setTimeout(() => {
1515
+ for (const pid of groupPids) signalProcessGroup(pid, 'SIGKILL');
1516
+ finish();
1517
+ }, childShutdownGraceMs);
1518
+ }
1519
+ const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
1520
+ process.stdout.on('drain', resumeOutputCarrier);
1521
+ input.on('line', receive);
1522
+ input.once('close', () => shutdown(0));
1523
+ for (const signal of ['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGTERM']) {
1524
+ process.once(signal, () => shutdown(processExitCode(null, signal)));
1525
+ }
1526
+ process.once('exit', () => {
1527
+ for (const child of processes.values()) terminate(child, 'SIGKILL');
1528
+ });
1529
+ emit({ type: 'ready', protocol: 1, pid: process.pid });
1530
+ `;
1531
+ var NativeRelay = class _NativeRelay {
1532
+ #websocket;
1533
+ #bootstrapReady = deferred();
1534
+ #ready = deferred();
1535
+ #websocketClosed = deferred();
1536
+ #processes = /* @__PURE__ */ new Map();
1537
+ #processInputs = /* @__PURE__ */ new Map();
1538
+ #sockets = /* @__PURE__ */ new Map();
1539
+ #closeListeners = /* @__PURE__ */ new Set();
1540
+ #buffer = "";
1541
+ #bootstrapOutput = "";
1542
+ #bootstrapReadySeen = false;
1543
+ #carrierPaused = false;
1544
+ #carrierDrainTimer;
1545
+ #closed = false;
1546
+ #readySeen = false;
1547
+ #closeError;
1548
+ #websocketClosePromise;
1549
+ constructor(websocket) {
1550
+ this.#websocket = websocket;
1551
+ websocket.on("message", (data) => this.#onData(data));
1552
+ websocket.on("error", (error) => this.#fail(toError(error)));
1553
+ websocket.on("close", (code, reason) => {
1554
+ this.#websocketClosed.resolve();
1555
+ const detail = Buffer.from(reason).toString("utf8");
1556
+ this.#fail(
1557
+ this.#closeError ?? new Error(`Coder native relay WebSocket closed (${code})${detail ? `: ${detail}` : ""}`)
1558
+ );
1559
+ });
1560
+ }
1561
+ static async connect(options) {
1562
+ if (options.signal?.aborted) throw abortError(options.signal);
1563
+ const query = new URLSearchParams({
1564
+ reconnect: randomUUID(),
1565
+ width: "80",
1566
+ height: "24",
1567
+ command: relayBootstrapCommand(options.nodeCommand),
1568
+ backend_type: "buffered"
1569
+ });
1570
+ const url = options.api.websocketUrl(
1571
+ `/api/v2/workspaceagents/${encodeURIComponent(options.agentId)}/pty?${query.toString()}`
1572
+ );
1573
+ const websocketOrigin = new URL(url).origin;
1574
+ const websocket = new WebSocket(url, {
1575
+ headers: options.api.websocketHeaders(),
1576
+ followRedirects: true,
1577
+ perMessageDeflate: false,
1578
+ finishRequest: (request, candidate) => {
1579
+ const candidateOrigin = new URL(candidate.url).origin;
1580
+ if (candidateOrigin !== websocketOrigin) {
1581
+ request.destroy(
1582
+ new Error(
1583
+ `Coder native relay refused cross-origin WebSocket redirect from ${websocketOrigin} to ${candidateOrigin}`
1584
+ )
1585
+ );
1586
+ return;
1587
+ }
1588
+ request.end();
1589
+ }
1590
+ });
1591
+ const relay = new _NativeRelay(websocket);
1592
+ const open = deferred();
1593
+ const onOpen = () => open.resolve();
1594
+ const onError = (error) => open.reject(error);
1595
+ websocket.once("open", onOpen);
1596
+ websocket.once("error", onError);
1597
+ const abort = () => {
1598
+ const error = abortError(options.signal);
1599
+ open.reject(error);
1600
+ relay.#bootstrapReady.reject(error);
1601
+ relay.#ready.reject(error);
1602
+ relay.#fail(error);
1603
+ };
1604
+ options.signal?.addEventListener("abort", abort, { once: true });
1605
+ const timer = setTimeout(() => {
1606
+ const error = relay.#connectTimeoutError(options.connectTimeoutMs);
1607
+ open.reject(error);
1608
+ relay.#bootstrapReady.reject(error);
1609
+ relay.#ready.reject(error);
1610
+ relay.#fail(error);
1611
+ }, options.connectTimeoutMs);
1612
+ timer.unref?.();
1613
+ try {
1614
+ await open.promise;
1615
+ websocket.off("error", onError);
1616
+ await relay.#bootstrapReady.promise;
1617
+ relay.#sendPtyData(`${Buffer.from(NATIVE_RELAY_SOURCE).toString("base64")}
1618
+ `);
1619
+ await relay.#ready.promise;
1620
+ return relay;
1621
+ } catch (error) {
1622
+ relay.#fail(toError(error));
1623
+ await relay.#closeWebSocket();
1624
+ throw error;
1625
+ } finally {
1626
+ clearTimeout(timer);
1627
+ options.signal?.removeEventListener("abort", abort);
1628
+ }
1629
+ }
1630
+ get closed() {
1631
+ return this.#closed;
1632
+ }
1633
+ startProcess(id, options, sink, loginShell) {
1634
+ this.#assertOpen();
1635
+ this.#processes.set(id, sink);
1636
+ if (options.stdin !== void 0) {
1637
+ this.#processInputs.set(id, {
1638
+ data: options.stdin,
1639
+ offset: 0,
1640
+ remotePaused: false,
1641
+ scheduled: false
1642
+ });
1643
+ }
1644
+ try {
1645
+ this.#send({
1646
+ type: "start",
1647
+ id,
1648
+ command: options.command,
1649
+ ...options.workingDirectory ? { cwd: options.workingDirectory } : {},
1650
+ ...options.env ? { env: options.env } : {},
1651
+ ...options.stdin !== void 0 ? { stdinMode: "stream" } : {},
1652
+ loginShell
1653
+ });
1654
+ this.#pumpProcessInput(id);
1655
+ } catch (error) {
1656
+ this.#processes.delete(id);
1657
+ this.#processInputs.delete(id);
1658
+ throw error;
1659
+ }
1660
+ }
1661
+ unregisterProcess(id) {
1662
+ this.#processes.delete(id);
1663
+ this.#processInputs.delete(id);
1664
+ }
1665
+ killProcess(id, signal = "SIGTERM") {
1666
+ if (this.#closed) return;
1667
+ this.#send({ type: "kill", id, signal, discardOutput: true });
1668
+ }
1669
+ pauseProcessOutput(id, stream) {
1670
+ if (this.#closed) return;
1671
+ this.#send({ type: "proc-pause", id, stream });
1672
+ }
1673
+ resumeProcessOutput(id, stream) {
1674
+ if (this.#closed) return;
1675
+ this.#send({ type: "proc-resume", id, stream });
1676
+ }
1677
+ discardProcessOutput(id, stream) {
1678
+ if (this.#closed) return;
1679
+ this.#send({ type: "proc-discard", id, stream });
1680
+ }
1681
+ openTcp(id, port, sink) {
1682
+ this.#assertOpen();
1683
+ this.#sockets.set(id, sink);
1684
+ try {
1685
+ if (this.#carrierPaused) sink.pause("carrier");
1686
+ this.#send({ type: "tcp-open", id, port });
1687
+ } catch (error) {
1688
+ this.#sockets.delete(id);
1689
+ throw error;
1690
+ }
1691
+ }
1692
+ tcpData(id, data) {
1693
+ if (this.#closed) return;
1694
+ this.#send({ type: "tcp-data", id, data: Buffer.from(data).toString("base64") });
1695
+ }
1696
+ tcpEnd(id) {
1697
+ if (this.#closed) return;
1698
+ this.#send({ type: "tcp-end", id });
1699
+ }
1700
+ pauseTcp(id) {
1701
+ if (this.#closed) return;
1702
+ this.#send({ type: "tcp-pause", id });
1703
+ }
1704
+ resumeTcp(id) {
1705
+ if (this.#closed) return;
1706
+ this.#send({ type: "tcp-resume", id });
1707
+ }
1708
+ closeTcp(id) {
1709
+ const existed = this.#sockets.delete(id);
1710
+ if (existed && !this.#closed) this.#send({ type: "tcp-close", id });
1711
+ }
1712
+ onClose(listener) {
1713
+ if (this.#closed) {
1714
+ queueMicrotask(() => listener(this.#closeError));
1715
+ return () => {
1716
+ };
1717
+ }
1718
+ this.#closeListeners.add(listener);
1719
+ return () => this.#closeListeners.delete(listener);
1720
+ }
1721
+ async close() {
1722
+ if (!this.#closed) {
1723
+ this.#closeError = new Error("Coder native relay closed");
1724
+ this.#fail(this.#closeError, 1e3, "transport closed");
1725
+ }
1726
+ await this.#closeWebSocket(1e3, "transport closed");
1727
+ }
1728
+ #send(message) {
1729
+ this.#sendPtyData(`${JSON.stringify({ v: RELAY_PROTOCOL_VERSION, ...message })}
1730
+ `);
1731
+ }
1732
+ #sendPtyData(data) {
1733
+ if (this.#websocket.readyState !== WebSocket.OPEN) {
1734
+ throw this.#closeError ?? new Error("Coder native relay WebSocket is not open");
1735
+ }
1736
+ this.#websocket.send(Buffer.from(JSON.stringify({ data }), "utf8"), { binary: true });
1737
+ this.#updateCarrierBackpressure();
1738
+ }
1739
+ #assertOpen() {
1740
+ if (this.#closed || !this.#readySeen) {
1741
+ throw this.#closeError ?? new Error("Coder native relay is not open");
1742
+ }
1743
+ }
1744
+ #connectTimeoutError(timeoutMs) {
1745
+ const phase = this.#websocket.readyState === WebSocket.CONNECTING ? "opening the authenticated PTY WebSocket" : !this.#bootstrapReadySeen ? "waiting for the remote PTY bootstrap" : "starting the workspace relay";
1746
+ const output = (this.#bootstrapReadySeen ? this.#bootstrapOutput : `${this.#bootstrapOutput}${this.#buffer}`.slice(-BOOTSTRAP_DIAGNOSTIC_LIMIT)).trim();
1747
+ return new Error(
1748
+ `timed out after ${timeoutMs}ms ${phase}${output ? `; PTY output: ${output}` : ""}`
1749
+ );
1750
+ }
1751
+ #onData(data) {
1752
+ const chunk = rawDataBuffer(data).toString("utf8");
1753
+ this.#buffer += chunk;
1754
+ if (!this.#bootstrapReadySeen) {
1755
+ const marker = this.#buffer.indexOf(NATIVE_RELAY_BOOTSTRAP_MARKER);
1756
+ if (marker !== -1) {
1757
+ this.#bootstrapOutput = `${this.#bootstrapOutput}${this.#buffer.slice(0, marker)}`.slice(
1758
+ -BOOTSTRAP_DIAGNOSTIC_LIMIT
1759
+ );
1760
+ this.#buffer = this.#buffer.slice(marker + NATIVE_RELAY_BOOTSTRAP_MARKER.length);
1761
+ if (this.#buffer.startsWith("\r\n")) this.#buffer = this.#buffer.slice(2);
1762
+ else if (this.#buffer.startsWith("\n")) this.#buffer = this.#buffer.slice(1);
1763
+ this.#bootstrapReadySeen = true;
1764
+ this.#bootstrapReady.resolve();
1765
+ } else {
1766
+ const markerOverlap = NATIVE_RELAY_BOOTSTRAP_MARKER.length - 1;
1767
+ const consumedLength = Math.max(0, this.#buffer.length - markerOverlap);
1768
+ if (consumedLength > 0) {
1769
+ this.#bootstrapOutput = `${this.#bootstrapOutput}${this.#buffer.slice(
1770
+ 0,
1771
+ consumedLength
1772
+ )}`.slice(-BOOTSTRAP_DIAGNOSTIC_LIMIT);
1773
+ this.#buffer = this.#buffer.slice(consumedLength);
1774
+ }
1775
+ return;
1776
+ }
1777
+ }
1778
+ for (; ; ) {
1779
+ const newline = this.#buffer.indexOf("\n");
1780
+ const frameLength = newline === -1 ? this.#buffer.length : newline;
1781
+ const frameLimit = this.#readySeen ? PROTOCOL_FRAME_LIMIT : BOOTSTRAP_FRAME_LIMIT;
1782
+ if (frameLength > frameLimit) {
1783
+ if (!this.#readySeen) {
1784
+ this.#bootstrapOutput = `${this.#bootstrapOutput}${this.#buffer.slice(
1785
+ -BOOTSTRAP_DIAGNOSTIC_LIMIT
1786
+ )}`.slice(-BOOTSTRAP_DIAGNOSTIC_LIMIT);
1787
+ }
1788
+ this.#buffer = "";
1789
+ this.#fail(
1790
+ new Error(
1791
+ this.#readySeen ? `Coder native relay protocol frame exceeded ${PROTOCOL_FRAME_LIMIT} characters` : `Coder native relay bootstrap frame exceeded ${BOOTSTRAP_FRAME_LIMIT} characters`
1792
+ )
1793
+ );
1794
+ return;
1795
+ }
1796
+ if (newline === -1) return;
1797
+ const line = this.#buffer.slice(0, newline).replace(/\r$/, "");
1798
+ this.#buffer = this.#buffer.slice(newline + 1);
1799
+ let message;
1800
+ try {
1801
+ message = JSON.parse(line);
1802
+ } catch {
1803
+ if (!this.#readySeen) {
1804
+ this.#bootstrapOutput = `${this.#bootstrapOutput}${line}
1805
+ `.slice(
1806
+ -BOOTSTRAP_DIAGNOSTIC_LIMIT
1807
+ );
1808
+ continue;
1809
+ }
1810
+ this.#fail(new Error(`invalid data from Coder native relay: ${line.slice(0, 200)}`));
1811
+ return;
1812
+ }
1813
+ this.#dispatch(message);
1814
+ }
1815
+ }
1816
+ #dispatch(message) {
1817
+ if (message.v !== RELAY_PROTOCOL_VERSION || typeof message.type !== "string") {
1818
+ this.#fail(new Error("Coder native relay returned an unsupported protocol frame"));
1819
+ return;
1820
+ }
1821
+ if (message.type === "ready") {
1822
+ if (message.pid === void 0) {
1823
+ this.#ready.reject(new Error("Coder native relay ready frame had no pid"));
1824
+ return;
1825
+ }
1826
+ this.#readySeen = true;
1827
+ this.#ready.resolve();
1828
+ return;
1829
+ }
1830
+ if (message.type === "error" && message.id === void 0) {
1831
+ this.#fail(new Error(`Coder native relay error: ${message.message ?? "unknown error"}`));
1832
+ return;
1833
+ }
1834
+ if (message.id === void 0) return;
1835
+ const process2 = this.#processes.get(message.id);
1836
+ if (process2 !== void 0) {
1837
+ switch (message.type) {
1838
+ case "started":
1839
+ if (message.pid !== void 0) process2.onStarted(message.pid);
1840
+ break;
1841
+ case "stdout":
1842
+ process2.onStdout(Buffer.from(message.data ?? "", "base64"));
1843
+ break;
1844
+ case "stderr":
1845
+ process2.onStderr(Buffer.from(message.data ?? "", "base64"));
1846
+ break;
1847
+ case "proc-stdin-pause": {
1848
+ const input = this.#processInputs.get(message.id);
1849
+ if (input !== void 0) input.remotePaused = true;
1850
+ break;
1851
+ }
1852
+ case "proc-stdin-resume": {
1853
+ const input = this.#processInputs.get(message.id);
1854
+ if (input !== void 0) {
1855
+ input.remotePaused = false;
1856
+ try {
1857
+ this.#pumpProcessInput(message.id);
1858
+ } catch (error) {
1859
+ this.#fail(toError(error));
1860
+ }
1861
+ }
1862
+ break;
1863
+ }
1864
+ case "proc-stdin-close":
1865
+ this.#processInputs.delete(message.id);
1866
+ break;
1867
+ case "exit":
1868
+ this.#processes.delete(message.id);
1869
+ this.#processInputs.delete(message.id);
1870
+ process2.onExit(message.code ?? 1);
1871
+ break;
1872
+ case "proc-error":
1873
+ this.#processes.delete(message.id);
1874
+ this.#processInputs.delete(message.id);
1875
+ process2.onError(new Error(message.message ?? "workspace process failed"));
1876
+ break;
1877
+ }
1878
+ return;
1879
+ }
1880
+ const socket = this.#sockets.get(message.id);
1881
+ if (socket === void 0) return;
1882
+ switch (message.type) {
1883
+ case "tcp-opened":
1884
+ socket.opened();
1885
+ break;
1886
+ case "tcp-data":
1887
+ socket.data(Buffer.from(message.data ?? "", "base64"));
1888
+ break;
1889
+ case "tcp-end":
1890
+ socket.end();
1891
+ break;
1892
+ case "tcp-pause":
1893
+ socket.pause("remote");
1894
+ break;
1895
+ case "tcp-resume":
1896
+ socket.resume("remote");
1897
+ break;
1898
+ case "tcp-close":
1899
+ this.#sockets.delete(message.id);
1900
+ socket.close();
1901
+ break;
1902
+ case "tcp-error":
1903
+ socket.error(new Error(message.message ?? "workspace TCP connection failed"));
1904
+ break;
1905
+ }
1906
+ }
1907
+ #fail(error, closeCode, closeReason) {
1908
+ if (this.#closed) return;
1909
+ this.#closed = true;
1910
+ this.#stopCarrierDrainTimer();
1911
+ this.#closeError = error;
1912
+ this.#bootstrapReady.reject(error);
1913
+ this.#ready.reject(error);
1914
+ for (const process2 of this.#processes.values()) process2.onError(error);
1915
+ this.#processes.clear();
1916
+ this.#processInputs.clear();
1917
+ for (const socket of this.#sockets.values()) socket.error(error);
1918
+ this.#sockets.clear();
1919
+ for (const listener of this.#closeListeners) listener(error);
1920
+ this.#closeListeners.clear();
1921
+ void this.#closeWebSocket(closeCode, closeReason);
1922
+ }
1923
+ #closeWebSocket(code, reason) {
1924
+ if (this.#websocketClosePromise !== void 0) return this.#websocketClosePromise;
1925
+ this.#websocketClosePromise = (async () => {
1926
+ if (isWebSocketClosed(this.#websocket)) return;
1927
+ try {
1928
+ if (this.#websocket.readyState === WebSocket.CONNECTING || this.#websocket.readyState === WebSocket.OPEN) {
1929
+ this.#websocket.close(code, reason);
1930
+ }
1931
+ } catch {
1932
+ this.#websocket.terminate();
1933
+ }
1934
+ if (isWebSocketClosed(this.#websocket)) return;
1935
+ const forceTimer = setTimeout(() => this.#websocket.terminate(), RELAY_CLOSE_GRACE_MS);
1936
+ try {
1937
+ await this.#websocketClosed.promise;
1938
+ } finally {
1939
+ clearTimeout(forceTimer);
1940
+ }
1941
+ })();
1942
+ return this.#websocketClosePromise;
1943
+ }
1944
+ #updateCarrierBackpressure() {
1945
+ if (this.#closed) return;
1946
+ if (!this.#carrierPaused) {
1947
+ if (this.#websocket.bufferedAmount < CARRIER_HIGH_WATER_MARK) return;
1948
+ this.#carrierPaused = true;
1949
+ for (const socket of this.#sockets.values()) socket.pause("carrier");
1950
+ this.#carrierDrainTimer = setInterval(
1951
+ () => this.#updateCarrierBackpressure(),
1952
+ CARRIER_DRAIN_POLL_INTERVAL_MS
1953
+ );
1954
+ this.#carrierDrainTimer.unref?.();
1955
+ return;
1956
+ }
1957
+ if (this.#websocket.bufferedAmount > CARRIER_LOW_WATER_MARK) return;
1958
+ this.#carrierPaused = false;
1959
+ this.#stopCarrierDrainTimer();
1960
+ for (const id of this.#processInputs.keys()) {
1961
+ try {
1962
+ this.#pumpProcessInput(id);
1963
+ } catch (error) {
1964
+ this.#fail(toError(error));
1965
+ return;
1966
+ }
1967
+ if (this.#carrierPaused) return;
1968
+ }
1969
+ for (const socket of this.#sockets.values()) {
1970
+ socket.resume("carrier");
1971
+ if (this.#carrierPaused) break;
1972
+ }
1973
+ }
1974
+ #pumpProcessInput(id) {
1975
+ let input = this.#processInputs.get(id);
1976
+ if (input?.scheduled) return;
1977
+ let sentBytes = 0;
1978
+ while (input !== void 0 && !this.#closed && !this.#carrierPaused && !input.remotePaused && sentBytes < PROCESS_STDIN_BATCH_BYTES) {
1979
+ const chunk = processInputChunk(input);
1980
+ if (chunk === void 0) {
1981
+ this.#send({ type: "proc-stdin-end", id });
1982
+ this.#processInputs.delete(id);
1983
+ return;
1984
+ }
1985
+ this.#send({ type: "proc-stdin", id, data: chunk.toString("base64") });
1986
+ sentBytes += chunk.byteLength;
1987
+ input = this.#processInputs.get(id);
1988
+ }
1989
+ if (input !== void 0 && !this.#closed && !this.#carrierPaused && !input.remotePaused) {
1990
+ input.scheduled = true;
1991
+ const immediate = setImmediate(() => {
1992
+ const scheduled = this.#processInputs.get(id);
1993
+ if (scheduled === void 0) return;
1994
+ scheduled.scheduled = false;
1995
+ try {
1996
+ this.#pumpProcessInput(id);
1997
+ } catch (error) {
1998
+ this.#fail(toError(error));
1999
+ }
2000
+ });
2001
+ immediate.unref?.();
2002
+ }
2003
+ }
2004
+ #stopCarrierDrainTimer() {
2005
+ if (this.#carrierDrainTimer === void 0) return;
2006
+ clearInterval(this.#carrierDrainTimer);
2007
+ this.#carrierDrainTimer = void 0;
2008
+ }
2009
+ };
2010
+ var NativeSpawnedProcess = class {
2011
+ stdout;
2012
+ stderr;
2013
+ #id = randomUUID();
2014
+ #completion = deferred();
2015
+ #started;
2016
+ #abortSignal;
2017
+ #onAbort;
2018
+ #stdoutController;
2019
+ #stderrController;
2020
+ #relay;
2021
+ #pid;
2022
+ #settled = false;
2023
+ #stdoutCanceled = false;
2024
+ #stderrCanceled = false;
2025
+ #stdoutPaused = false;
2026
+ #stderrPaused = false;
2027
+ constructor(relay, options, loginShell) {
2028
+ this.stdout = new ReadableStream({
2029
+ start: (controller) => {
2030
+ this.#stdoutController = controller;
2031
+ },
2032
+ pull: () => this.#resumeOutput("stdout"),
2033
+ cancel: () => this.#cancelOutput("stdout")
2034
+ });
2035
+ this.stderr = new ReadableStream({
2036
+ start: (controller) => {
2037
+ this.#stderrController = controller;
2038
+ },
2039
+ pull: () => this.#resumeOutput("stderr"),
2040
+ cancel: () => this.#cancelOutput("stderr")
2041
+ });
2042
+ this.#abortSignal = options.abortSignal;
2043
+ this.#onAbort = () => {
2044
+ const error = abortError(this.#abortSignal);
2045
+ this.#relay?.killProcess(this.#id);
2046
+ this.onError(error);
2047
+ };
2048
+ this.#abortSignal?.addEventListener("abort", this.#onAbort, { once: true });
2049
+ if (this.#abortSignal?.aborted) this.#onAbort();
2050
+ this.#started = relay.then((connected) => {
2051
+ if (this.#settled) return;
2052
+ this.#relay = connected;
2053
+ connected.startProcess(this.#id, options, this, loginShell);
2054
+ if (this.#stdoutCanceled) connected.discardProcessOutput(this.#id, "stdout");
2055
+ if (this.#stderrCanceled) connected.discardProcessOutput(this.#id, "stderr");
2056
+ }).catch((error) => this.onError(toError(error)));
2057
+ }
2058
+ get pid() {
2059
+ return this.#pid;
2060
+ }
2061
+ onStarted(pid) {
2062
+ this.#pid = pid;
2063
+ }
2064
+ onStdout(data) {
2065
+ this.#enqueueOutput("stdout", data);
2066
+ }
2067
+ onStderr(data) {
2068
+ this.#enqueueOutput("stderr", data);
2069
+ }
2070
+ onExit(code) {
2071
+ if (this.#settled) return;
2072
+ this.#settled = true;
2073
+ this.#cleanup();
2074
+ if (!this.#stdoutCanceled) this.#stdoutController.close();
2075
+ if (!this.#stderrCanceled) this.#stderrController.close();
2076
+ this.#completion.resolve({ exitCode: code });
2077
+ }
2078
+ onError(error) {
2079
+ if (this.#settled) return;
2080
+ this.#settled = true;
2081
+ this.#cleanup();
2082
+ if (!this.#stdoutCanceled) this.#stdoutController.error(error);
2083
+ if (!this.#stderrCanceled) this.#stderrController.error(error);
2084
+ this.#completion.reject(error);
2085
+ }
2086
+ wait() {
2087
+ return this.#completion.promise;
2088
+ }
2089
+ async kill() {
2090
+ if (this.#settled) return;
2091
+ await this.#started;
2092
+ this.#relay?.killProcess(this.#id);
2093
+ }
2094
+ #enqueueOutput(stream, data) {
2095
+ if (this.#settled || this.#outputCanceled(stream)) return;
2096
+ const controller = stream === "stdout" ? this.#stdoutController : this.#stderrController;
2097
+ controller.enqueue(data);
2098
+ if ((controller.desiredSize ?? 0) > 0) return;
2099
+ if (stream === "stdout") {
2100
+ if (this.#stdoutPaused) return;
2101
+ this.#stdoutPaused = true;
2102
+ } else {
2103
+ if (this.#stderrPaused) return;
2104
+ this.#stderrPaused = true;
2105
+ }
2106
+ this.#relay?.pauseProcessOutput(this.#id, stream);
2107
+ }
2108
+ #resumeOutput(stream) {
2109
+ if (this.#settled || this.#outputCanceled(stream)) return;
2110
+ if (stream === "stdout") {
2111
+ if (!this.#stdoutPaused) return;
2112
+ this.#stdoutPaused = false;
2113
+ } else {
2114
+ if (!this.#stderrPaused) return;
2115
+ this.#stderrPaused = false;
2116
+ }
2117
+ this.#relay?.resumeProcessOutput(this.#id, stream);
2118
+ }
2119
+ #cancelOutput(stream) {
2120
+ if (this.#settled || this.#outputCanceled(stream)) return;
2121
+ if (stream === "stdout") {
2122
+ this.#stdoutCanceled = true;
2123
+ this.#stdoutPaused = false;
2124
+ } else {
2125
+ this.#stderrCanceled = true;
2126
+ this.#stderrPaused = false;
2127
+ }
2128
+ this.#relay?.discardProcessOutput(this.#id, stream);
2129
+ }
2130
+ #outputCanceled(stream) {
2131
+ return stream === "stdout" ? this.#stdoutCanceled : this.#stderrCanceled;
2132
+ }
2133
+ #cleanup() {
2134
+ this.#abortSignal?.removeEventListener("abort", this.#onAbort);
2135
+ this.#relay?.unregisterProcess(this.#id);
2136
+ }
2137
+ };
2138
+ async function openNativePortForward(relay, options) {
2139
+ const server = net2.createServer({ allowHalfOpen: true });
2140
+ const sockets = /* @__PURE__ */ new Set();
2141
+ let closed = false;
2142
+ let rejectPendingBind;
2143
+ const closeForward = () => {
2144
+ if (closed) return;
2145
+ closed = true;
2146
+ for (const socket of sockets) socket.destroy();
2147
+ sockets.clear();
2148
+ if (server.listening) server.close();
2149
+ else server.once("listening", () => server.close());
2150
+ };
2151
+ const removeCloseListener = relay.onClose(closeForward);
2152
+ server.on("error", closeForward);
2153
+ server.on("connection", (socket) => {
2154
+ const id = randomUUID();
2155
+ sockets.add(socket);
2156
+ socket.pause();
2157
+ let opened = false;
2158
+ let remoteClosed = false;
2159
+ let remoteEnded = false;
2160
+ let remotePaused = false;
2161
+ const uploadPauses = /* @__PURE__ */ new Set();
2162
+ const resumeUpload = () => {
2163
+ if (opened && uploadPauses.size === 0 && !remoteClosed && !socket.destroyed) socket.resume();
2164
+ };
2165
+ const closeRemote = () => {
2166
+ if (remoteClosed) return;
2167
+ remoteClosed = true;
2168
+ relay.closeTcp(id);
2169
+ };
2170
+ socket.on("data", (data) => {
2171
+ if (!remoteClosed) relay.tcpData(id, data);
2172
+ });
2173
+ socket.on("end", () => {
2174
+ if (!remoteClosed) relay.tcpEnd(id);
2175
+ });
2176
+ socket.on("drain", () => {
2177
+ if (remoteClosed || !remotePaused) return;
2178
+ remotePaused = false;
2179
+ relay.resumeTcp(id);
2180
+ });
2181
+ socket.on("close", () => {
2182
+ sockets.delete(socket);
2183
+ closeRemote();
2184
+ });
2185
+ socket.on("error", closeRemote);
2186
+ try {
2187
+ relay.openTcp(id, options.remotePort, {
2188
+ opened: () => {
2189
+ opened = true;
2190
+ resumeUpload();
2191
+ },
2192
+ data: (data) => {
2193
+ if (!socket.destroyed && !socket.write(data) && !remotePaused) {
2194
+ remotePaused = true;
2195
+ relay.pauseTcp(id);
2196
+ }
2197
+ },
2198
+ end: () => {
2199
+ remoteEnded = true;
2200
+ socket.end();
2201
+ },
2202
+ pause: (reason) => {
2203
+ uploadPauses.add(reason);
2204
+ socket.pause();
2205
+ },
2206
+ resume: (reason) => {
2207
+ uploadPauses.delete(reason);
2208
+ resumeUpload();
2209
+ },
2210
+ close: () => {
2211
+ remoteClosed = true;
2212
+ if (socket.destroyed) return;
2213
+ if (remoteEnded) socket.destroySoon();
2214
+ else socket.destroy();
2215
+ },
2216
+ error: (error) => {
2217
+ remoteClosed = true;
2218
+ socket.destroy(error);
2219
+ }
2220
+ });
2221
+ } catch (error) {
2222
+ socket.destroy(toError(error));
2223
+ }
2224
+ });
2225
+ const onAbort = () => {
2226
+ removeCloseListener();
2227
+ rejectPendingBind?.(abortError(options.abortSignal));
2228
+ closeForward();
2229
+ };
2230
+ options.abortSignal?.addEventListener("abort", onAbort, { once: true });
2231
+ if (options.abortSignal?.aborted) {
2232
+ removeCloseListener();
2233
+ throw abortError(options.abortSignal);
2234
+ }
2235
+ try {
2236
+ await new Promise((resolve, reject) => {
2237
+ const onError = (error) => {
2238
+ rejectPendingBind = void 0;
2239
+ reject(error);
2240
+ };
2241
+ rejectPendingBind = (error) => {
2242
+ server.off("error", onError);
2243
+ rejectPendingBind = void 0;
2244
+ reject(error);
2245
+ };
2246
+ server.once("error", onError);
2247
+ server.listen(0, "127.0.0.1", () => {
2248
+ server.off("error", onError);
2249
+ rejectPendingBind = void 0;
2250
+ resolve();
2251
+ });
2252
+ });
2253
+ } catch (error) {
2254
+ closeForward();
2255
+ removeCloseListener();
2256
+ options.abortSignal?.removeEventListener("abort", onAbort);
2257
+ throw error;
2258
+ }
2259
+ const address = server.address();
2260
+ if (address === null || typeof address === "string") {
2261
+ closeForward();
2262
+ removeCloseListener();
2263
+ throw new Error("failed to allocate a local port for the Coder native forward");
2264
+ }
2265
+ return {
2266
+ localHost: "127.0.0.1",
2267
+ localPort: address.port,
2268
+ get closed() {
2269
+ return closed || relay.closed;
2270
+ },
2271
+ close: async () => {
2272
+ options.abortSignal?.removeEventListener("abort", onAbort);
2273
+ removeCloseListener();
2274
+ closeForward();
2275
+ }
2276
+ };
2277
+ }
2278
+ function relayBootstrapCommand(nodeCommand) {
2279
+ const script = `stty raw -echo; printf '%s\\n' ${shellQuote(NATIVE_RELAY_BOOTSTRAP_MARKER)}; IFS= read -r CODER_AI_SDK_RELAY_PAYLOAD; exec ${shellQuote(nodeCommand)} -e "$(printf %s "$CODER_AI_SDK_RELAY_PAYLOAD" | base64 -d)"`;
2280
+ return `exec bash -lc ${shellQuote(script)}`;
2281
+ }
2282
+ function rawDataBuffer(data) {
2283
+ if (Buffer.isBuffer(data)) return data;
2284
+ if (Array.isArray(data)) return Buffer.concat(data);
2285
+ return Buffer.from(data);
2286
+ }
2287
+ function processInputChunk(input) {
2288
+ if (typeof input.data === "string") {
2289
+ if (input.offset >= input.data.length) return void 0;
2290
+ const maxCodeUnits = Math.max(1, Math.floor(PROCESS_STDIN_CHUNK_BYTES / 4));
2291
+ let end2 = Math.min(input.data.length, input.offset + maxCodeUnits);
2292
+ if (end2 < input.data.length && isHighSurrogate(input.data.charCodeAt(end2 - 1)) && isLowSurrogate(input.data.charCodeAt(end2))) {
2293
+ end2 -= 1;
2294
+ }
2295
+ const chunk2 = Buffer.from(input.data.slice(input.offset, end2));
2296
+ input.offset = end2;
2297
+ return chunk2;
2298
+ }
2299
+ if (input.offset >= input.data.byteLength) return void 0;
2300
+ const end = Math.min(input.data.byteLength, input.offset + PROCESS_STDIN_CHUNK_BYTES);
2301
+ const chunk = Buffer.from(
2302
+ input.data.buffer,
2303
+ input.data.byteOffset + input.offset,
2304
+ end - input.offset
2305
+ );
2306
+ input.offset = end;
2307
+ return chunk;
2308
+ }
2309
+ function isHighSurrogate(value) {
2310
+ return value >= 55296 && value <= 56319;
2311
+ }
2312
+ function isLowSurrogate(value) {
2313
+ return value >= 56320 && value <= 57343;
2314
+ }
2315
+ function isWebSocketClosed(websocket) {
2316
+ return websocket.readyState === WebSocket.CLOSED;
2317
+ }
2318
+ function deferred() {
2319
+ let resolve;
2320
+ let reject;
2321
+ const promise = new Promise((res, rej) => {
2322
+ resolve = res;
2323
+ reject = rej;
2324
+ });
2325
+ void promise.catch(() => {
2326
+ });
2327
+ return { promise, resolve, reject };
2328
+ }
2329
+ function toError(error) {
2330
+ return error instanceof Error ? error : new Error(String(error));
2331
+ }
2332
+ function abortError(signal) {
2333
+ if (signal?.reason instanceof Error) return signal.reason;
2334
+ return new DOMException("The operation was aborted", "AbortError");
2335
+ }
2336
+
2337
+ // src/native-transport.ts
2338
+ var DEFAULT_RELAY_CONNECT_TIMEOUT_MS = 3e4;
2339
+ var CoderNativeTransport = class {
2340
+ #api;
2341
+ #loginShell;
2342
+ #relayNodeCommand;
2343
+ #relayConnectTimeoutMs;
2344
+ #relays = /* @__PURE__ */ new Map();
2345
+ constructor(options = {}) {
2346
+ const url = options.url ?? process.env.CODER_URL;
2347
+ const token = options.token ?? process.env.CODER_SESSION_TOKEN;
2348
+ if (!url) {
2349
+ throw new Error("CoderNativeTransport requires a Coder URL; pass { url } or set CODER_URL");
2350
+ }
2351
+ if (!token) {
2352
+ throw new Error(
2353
+ "CoderNativeTransport requires a session token; pass { token } or set CODER_SESSION_TOKEN"
2354
+ );
2355
+ }
2356
+ this.#api = new CoderApiClient({
2357
+ url,
2358
+ token,
2359
+ fetch: options.fetch,
2360
+ headers: options.headers,
2361
+ buildPollIntervalMs: options.buildPollIntervalMs,
2362
+ buildTimeoutMs: options.buildTimeoutMs
2363
+ });
2364
+ this.#loginShell = options.loginShell ?? true;
2365
+ this.#relayNodeCommand = options.relayNodeCommand ?? "node";
2366
+ this.#relayConnectTimeoutMs = options.relayConnectTimeoutMs ?? DEFAULT_RELAY_CONNECT_TIMEOUT_MS;
2367
+ }
2368
+ async exec(options) {
2369
+ const process2 = this.spawn(options);
2370
+ const [stdout, stderr, result] = await Promise.all([
2371
+ drain(process2.stdout),
2372
+ drain(process2.stderr),
2373
+ process2.wait()
2374
+ ]);
2375
+ return { exitCode: result.exitCode, stdout, stderr };
2376
+ }
2377
+ spawn(options) {
2378
+ return new NativeSpawnedProcess(
2379
+ this.#relayFor(options.workspace, options.abortSignal),
2380
+ options,
2381
+ this.#loginShell
2382
+ );
2383
+ }
2384
+ async forwardPort(options) {
2385
+ if (!Number.isInteger(options.remotePort) || options.remotePort < 1 || options.remotePort > 65535) {
2386
+ throw new Error(
2387
+ `invalid Coder workspace port ${options.remotePort}; expected an integer from 1 to 65535`
2388
+ );
2389
+ }
2390
+ const relay = await this.#relayFor(options.workspace, options.abortSignal);
2391
+ return await openNativePortForward(relay, options);
2392
+ }
2393
+ async start(workspace, options) {
2394
+ const current = await this.#api.workspace(workspace, options?.abortSignal);
2395
+ if (current?.latest_build.status !== "running") {
2396
+ await this.#closeWorkspaceRelays(
2397
+ workspace,
2398
+ current?.id,
2399
+ current?.owner_name,
2400
+ current?.owner_id,
2401
+ options?.abortSignal
2402
+ );
2403
+ }
2404
+ await this.#api.start(workspace, options);
2405
+ }
2406
+ async stop(workspace, options) {
2407
+ const current = await this.#api.workspace(workspace, options?.abortSignal);
2408
+ await this.#closeWorkspaceRelays(
2409
+ workspace,
2410
+ current?.id,
2411
+ current?.owner_name,
2412
+ current?.owner_id,
2413
+ options?.abortSignal
2414
+ );
2415
+ await this.#api.stop(workspace, options);
2416
+ }
2417
+ async destroy(workspace, options) {
2418
+ const current = await this.#api.workspace(workspace, options?.abortSignal);
2419
+ await this.#closeWorkspaceRelays(
2420
+ workspace,
2421
+ current?.id,
2422
+ current?.owner_name,
2423
+ current?.owner_id,
2424
+ options?.abortSignal
2425
+ );
2426
+ await this.#api.destroy(workspace, options);
2427
+ }
2428
+ status(workspace, options) {
2429
+ return this.#api.status(workspace, options);
2430
+ }
2431
+ create(options) {
2432
+ return this.#api.create(options);
2433
+ }
2434
+ listPresets(options) {
2435
+ return this.#api.listPresets(options);
2436
+ }
2437
+ /** Close every cached workspace relay. Existing local port-forwards close too. */
2438
+ async close() {
2439
+ const setups = [...new Set(this.#relays.values())];
2440
+ this.#relays.clear();
2441
+ const error = new Error("Coder native transport closed");
2442
+ for (const setup of setups) setup.controller.abort(error);
2443
+ const settled = await Promise.allSettled(setups.map((setup) => setup.promise));
2444
+ await Promise.all(
2445
+ settled.filter(
2446
+ (result) => result.status === "fulfilled"
2447
+ ).map((result) => result.value.relay.close())
2448
+ );
2449
+ }
2450
+ async #relayFor(workspace, signal) {
2451
+ if (signal?.aborted) throw abortError2(signal);
2452
+ const referenceKey = relayReferenceKey(workspace);
2453
+ const existing = this.#relays.get(referenceKey);
2454
+ if (existing !== void 0) {
2455
+ const entry = await waitWithAbort2(existing.promise, signal);
2456
+ if (!entry.relay.closed) return entry.relay;
2457
+ this.#removeRelaySetup(existing);
2458
+ }
2459
+ const controller = new AbortController();
2460
+ const workspaceKey = canonicalWorkspaceKey(workspace);
2461
+ let setup;
2462
+ const promise = (async () => {
2463
+ try {
2464
+ const resolved = await waitWithAbort2(
2465
+ this.#api.resolveAgent(workspace, controller.signal),
2466
+ controller.signal
2467
+ );
2468
+ setup.workspaceId = resolved.workspace.id;
2469
+ setup.workspaceKey = canonicalWorkspaceKey(workspace, resolved.workspace.owner_name);
2470
+ const agentKey = relayAgentKey(resolved.workspace.id, resolved.agent.id);
2471
+ const shared = this.#relays.get(agentKey);
2472
+ if (shared !== void 0 && shared !== setup) {
2473
+ this.#relays.set(referenceKey, shared);
2474
+ const entry = await waitWithAbort2(shared.promise, controller.signal);
2475
+ if (!entry.relay.closed) return entry;
2476
+ this.#removeRelaySetup(shared);
2477
+ this.#relays.set(referenceKey, setup);
2478
+ }
2479
+ this.#relays.set(agentKey, setup);
2480
+ if (resolved.workspace.latest_build.status !== "running") {
2481
+ throw new Error(
2482
+ `Coder workspace "${workspace}" is ${resolved.workspace.latest_build.status}; start it before connecting`
2483
+ );
2484
+ }
2485
+ if (resolved.agent.status !== "connected") {
2486
+ throw new Error(
2487
+ `Coder workspace agent "${resolved.agent.name}" is ${resolved.agent.status}; wait for it to connect`
2488
+ );
2489
+ }
2490
+ const relay = await NativeRelay.connect({
2491
+ api: this.#api,
2492
+ agentId: resolved.agent.id,
2493
+ nodeCommand: this.#relayNodeCommand,
2494
+ connectTimeoutMs: this.#relayConnectTimeoutMs,
2495
+ signal: controller.signal
2496
+ });
2497
+ relay.onClose(() => {
2498
+ this.#removeRelaySetup(setup);
2499
+ });
2500
+ return { workspaceId: resolved.workspace.id, relay };
2501
+ } catch (error) {
2502
+ this.#removeRelaySetup(setup);
2503
+ throw error;
2504
+ }
2505
+ })();
2506
+ setup = { controller, promise, workspaceKey };
2507
+ this.#relays.set(referenceKey, setup);
2508
+ return (await waitWithAbort2(promise, signal)).relay;
2509
+ }
2510
+ async #closeWorkspaceRelays(workspace, workspaceId, workspaceOwner, workspaceOwnerId, signal) {
2511
+ const parsedWorkspace = parseNativeWorkspaceRef(workspace);
2512
+ const requestedWorkspaceKey = canonicalWorkspaceKey(workspace);
2513
+ const workspaceKey = canonicalWorkspaceKey(workspace, workspaceOwner);
2514
+ const workspaceKeys = /* @__PURE__ */ new Set([requestedWorkspaceKey, workspaceKey]);
2515
+ const meWorkspaceKey = `me/${parsedWorkspace.name}`;
2516
+ const setups = [...new Set(this.#relays.values())];
2517
+ const hasUnresolvedMeSetup = setups.some(
2518
+ (setup) => setup.workspaceId === void 0 && setup.workspaceKey === meWorkspaceKey
2519
+ );
2520
+ if (parsedWorkspace.owner !== "me" && workspaceOwnerId !== void 0 && hasUnresolvedMeSetup && await waitWithAbort2(this.#api.currentUserId(signal), signal) === workspaceOwnerId) {
2521
+ workspaceKeys.add(meWorkspaceKey);
2522
+ }
2523
+ const entries = setups.filter(
2524
+ (setup) => workspaceKeys.has(setup.workspaceKey) || workspaceId !== void 0 && setup.workspaceId === workspaceId
2525
+ );
2526
+ const error = new Error(`Coder native relay closed for workspace "${workspace}" lifecycle`);
2527
+ for (const setup of entries) {
2528
+ this.#removeRelaySetup(setup);
2529
+ setup.controller.abort(error);
2530
+ }
2531
+ const closing = Promise.allSettled(entries.map((setup) => setup.promise)).then(
2532
+ async (settled) => await Promise.all(
2533
+ settled.filter(
2534
+ (result) => result.status === "fulfilled"
2535
+ ).map((result) => result.value.relay.close())
2536
+ )
2537
+ );
2538
+ await waitWithAbort2(closing, signal);
2539
+ }
2540
+ #removeRelaySetup(setup) {
2541
+ for (const [key, candidate] of this.#relays) {
2542
+ if (candidate === setup) this.#relays.delete(key);
2543
+ }
2544
+ }
2545
+ };
2546
+ function relayReferenceKey(workspace) {
2547
+ return `reference:${workspace}`;
2548
+ }
2549
+ function relayAgentKey(workspaceId, agentId) {
2550
+ return `agent:${workspaceId}:${agentId}`;
2551
+ }
2552
+ function canonicalWorkspaceKey(workspace, resolvedOwner) {
2553
+ const { owner, name } = parseNativeWorkspaceRef(workspace);
2554
+ return `${resolvedOwner ?? owner}/${name}`;
2555
+ }
2556
+ async function drain(stream) {
2557
+ const reader = stream.getReader();
2558
+ const chunks = [];
2559
+ for (; ; ) {
2560
+ const { done, value } = await reader.read();
2561
+ if (done) break;
2562
+ if (value !== void 0) chunks.push(value);
2563
+ }
2564
+ return Buffer.concat(chunks).toString("utf8");
2565
+ }
2566
+ async function waitWithAbort2(promise, signal) {
2567
+ if (signal === void 0) return await promise;
2568
+ if (signal.aborted) throw abortError2(signal);
2569
+ let onAbort;
2570
+ const aborted = new Promise((_resolve, reject) => {
2571
+ onAbort = () => reject(abortError2(signal));
2572
+ signal.addEventListener("abort", onAbort, { once: true });
2573
+ });
2574
+ try {
2575
+ return await Promise.race([promise, aborted]);
2576
+ } finally {
2577
+ signal.removeEventListener("abort", onAbort);
2578
+ }
2579
+ }
2580
+ function abortError2(signal) {
2581
+ if (signal.reason instanceof Error) return signal.reason;
2582
+ return new DOMException("The operation was aborted", "AbortError");
2583
+ }
2584
+
499
2585
  // src/coder-workspace-provider.ts
500
2586
  import { createHash } from "crypto";
501
- import { setTimeout as delay } from "timers/promises";
2587
+ import { setTimeout as delay2 } from "timers/promises";
502
2588
 
503
2589
  // src/file-io.ts
504
2590
  import path from "path";
@@ -522,7 +2608,7 @@ async function readBinaryFile(ctx, options) {
522
2608
  const base64 = result.stdout.replace(/\s+/g, "");
523
2609
  return new Uint8Array(Buffer.from(base64, "base64"));
524
2610
  }
525
- async function readFile(ctx, options) {
2611
+ async function readFile2(ctx, options) {
526
2612
  const bytes = await readBinaryFile(ctx, options);
527
2613
  if (bytes === null) return null;
528
2614
  return new ReadableStream({
@@ -648,7 +2734,7 @@ var CoderWorkspaceSession = class {
648
2734
  this.#ports = [...config.ports];
649
2735
  this.id = config.id;
650
2736
  this.defaultWorkingDirectory = config.defaultWorkingDirectory;
651
- this.description = `Coder workspace "${config.workspace}". Default working directory: ${config.defaultWorkingDirectory}. Exposed ports: ${this.#ports.length > 0 ? this.#ports.join(", ") : "none"}. Commands run inside the workspace via 'coder ssh'.`;
2737
+ this.description = `Coder workspace "${config.workspace}". Default working directory: ${config.defaultWorkingDirectory}. Exposed ports: ${this.#ports.length > 0 ? this.#ports.join(", ") : "none"}.`;
652
2738
  }
653
2739
  get ports() {
654
2740
  return this.#ports;
@@ -673,14 +2759,14 @@ var CoderWorkspaceSession = class {
673
2759
  run = (options) => this.#transport.exec(this.#execOptions(options));
674
2760
  spawn = async (options) => this.#transport.spawn(this.#execOptions(options));
675
2761
  // --- file I/O surface -----------------------------------------------------
676
- readFile = (options) => readFile(this.#fileIoContext(), options);
2762
+ readFile = (options) => readFile2(this.#fileIoContext(), options);
677
2763
  readBinaryFile = (options) => readBinaryFile(this.#fileIoContext(), options);
678
2764
  readTextFile = (options) => readTextFile(this.#fileIoContext(), options);
679
2765
  writeFile = (options) => writeFile(this.#fileIoContext(), options);
680
2766
  writeBinaryFile = (options) => writeBinaryFile(this.#fileIoContext(), options);
681
2767
  writeTextFile = (options) => writeTextFile(this.#fileIoContext(), options);
682
2768
  // --- network surface ------------------------------------------------------
683
- getPortUrl = async (options) => {
2769
+ getPortEndpoint = async (options) => {
684
2770
  if (this.#stopped) {
685
2771
  throw new Error("cannot resolve a port URL: the sandbox session is stopped");
686
2772
  }
@@ -704,8 +2790,10 @@ var CoderWorkspaceSession = class {
704
2790
  }
705
2791
  const resolved = await forward;
706
2792
  const scheme = localScheme(options.protocol ?? "ws");
707
- return `${scheme}://${resolved.localHost}:${resolved.localPort}`;
2793
+ return { url: `${scheme}://${resolved.localHost}:${resolved.localPort}` };
708
2794
  };
2795
+ /** @deprecated Kept for the `HarnessV1NetworkSandboxSession` contract; use `getPortEndpoint`. */
2796
+ getPortUrl = async (options) => (await this.getPortEndpoint(options)).url;
709
2797
  setPorts = async (ports, _options) => {
710
2798
  const next = [...ports];
711
2799
  for (const [port, forward] of this.#forwards) {
@@ -970,7 +3058,7 @@ async function waitForReady(transport, workspace, timeoutMs, caller, abortSignal
970
3058
  `${caller}: timed out after ${timeoutMs}ms waiting for workspace "${workspace}" to become ready (last status: ${last}).`
971
3059
  );
972
3060
  }
973
- await delay(READY_POLL_INTERVAL_MS, void 0, { signal: abortSignal });
3061
+ await delay2(READY_POLL_INTERVAL_MS, void 0, { signal: abortSignal });
974
3062
  }
975
3063
  }
976
3064
  function deriveWorkspaceName(prefix, sessionId) {
@@ -1004,6 +3092,8 @@ async function resolveHomeDirectory(transport, workspace, abortSignal) {
1004
3092
  export {
1005
3093
  CODER_WORKSPACE_PROVIDER_ID,
1006
3094
  CoderCliTransport,
3095
+ CoderNativeApiError,
3096
+ CoderNativeTransport,
1007
3097
  CoderWorkspaceSession,
1008
3098
  createCoderWorkspace,
1009
3099
  ensureCoderWorkspace