@spotpatch/dev-server 0.5.0 → 0.7.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
@@ -472,6 +472,1103 @@ function createAgentJobManager(options) {
472
472
  });
473
473
  }
474
474
 
475
+ // src/external-handoff/service.ts
476
+ import {
477
+ ERROR_CODES as ERROR_CODES6,
478
+ EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION as EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION3,
479
+ EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION as EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION2,
480
+ SpotPatchError as SpotPatchError6
481
+ } from "@spotpatch/shared";
482
+ import { computeExternalHandoffProjectKey as computeExternalHandoffProjectKey2 } from "@spotpatch/shared/external-agent-node";
483
+
484
+ // src/external-handoff/active-registry.ts
485
+ import { randomBytes as randomBytes2 } from "crypto";
486
+ import {
487
+ ERROR_CODES as ERROR_CODES2,
488
+ EXTERNAL_HANDOFF_LIMITS,
489
+ SpotPatchError as SpotPatchError2
490
+ } from "@spotpatch/shared";
491
+
492
+ // src/external-handoff/clock.ts
493
+ import { performance } from "perf_hooks";
494
+ var SYSTEM_EXTERNAL_HANDOFF_CLOCK = Object.freeze({
495
+ monotonicNow: () => performance.now(),
496
+ wallNow: () => Date.now()
497
+ });
498
+
499
+ // src/external-handoff/active-registry.ts
500
+ var ALLOWED_TRANSITIONS = Object.freeze({
501
+ queued: ["dispatching", "failed"],
502
+ dispatching: ["dispatched", "working", "failed", "delivery-unknown"],
503
+ dispatched: ["working", "failed", "delivery-unknown"],
504
+ working: ["completed", "failed", "delivery-unknown"],
505
+ completed: [],
506
+ failed: [],
507
+ "delivery-unknown": []
508
+ });
509
+ var TERMINAL_PHASES = /* @__PURE__ */ new Set([
510
+ "completed",
511
+ "failed",
512
+ "delivery-unknown"
513
+ ]);
514
+ function defaultRandomId() {
515
+ return randomBytes2(32).toString("base64url");
516
+ }
517
+ function requirePresent(value) {
518
+ if (value === null) throw new SpotPatchError2(ERROR_CODES2.INTERNAL_ERROR);
519
+ return value;
520
+ }
521
+ function createActiveAdapterRegistry(options = {}) {
522
+ const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
523
+ const randomId = options.randomId ?? defaultRandomId;
524
+ let blocked;
525
+ let closed = false;
526
+ let dispatch;
527
+ let lastReleasedToken;
528
+ let lease;
529
+ const nowIso = () => new Date(clock.wallNow()).toISOString();
530
+ const requireOpen = () => {
531
+ if (closed) throw new SpotPatchError2(ERROR_CODES2.SESSION_CLOSED);
532
+ };
533
+ const dispatchSummary = () => dispatch === void 0 ? null : Object.freeze({
534
+ adapterKind: dispatch.adapterKind,
535
+ revision: dispatch.revision,
536
+ phase: dispatch.phase,
537
+ updatedAt: dispatch.updatedAt
538
+ });
539
+ const activeSummary = () => {
540
+ if (blocked !== void 0) {
541
+ return Object.freeze({
542
+ kind: blocked.adapterKind,
543
+ state: "blocked",
544
+ canDispatch: false,
545
+ connectedAt: blocked.connectedAt,
546
+ updatedAt: blocked.updatedAt
547
+ });
548
+ }
549
+ if (lease === void 0) return null;
550
+ const busy = dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase);
551
+ return Object.freeze({
552
+ kind: lease.adapterKind,
553
+ state: busy ? "busy" : "ready",
554
+ canDispatch: !busy,
555
+ connectedAt: lease.connectedAt,
556
+ updatedAt: lease.updatedAt
557
+ });
558
+ };
559
+ const state = (cursor) => Object.freeze({
560
+ activeAdapter: activeSummary(),
561
+ dispatch: cursor === void 0 || dispatch?.cursor === cursor ? dispatchSummary() : null
562
+ });
563
+ const enterUnknown = (activeLease) => {
564
+ if (dispatch === void 0) return;
565
+ const updatedAt = nowIso();
566
+ dispatch.phase = "delivery-unknown";
567
+ dispatch.updatedAt = updatedAt;
568
+ blocked = Object.freeze({
569
+ adapterKind: activeLease.adapterKind,
570
+ connectedAt: activeLease.connectedAt,
571
+ updatedAt
572
+ });
573
+ };
574
+ const endLease = (activeLease) => {
575
+ if (dispatch?.phase === "queued") {
576
+ dispatch.phase = "failed";
577
+ dispatch.updatedAt = nowIso();
578
+ } else if (dispatch?.phase === "dispatching" || dispatch?.phase === "dispatched" || dispatch?.phase === "working") {
579
+ enterUnknown(activeLease);
580
+ }
581
+ lastReleasedToken = activeLease.token;
582
+ lease = void 0;
583
+ };
584
+ const sweep = () => {
585
+ if (lease === void 0) return;
586
+ const monotonicNow = clock.monotonicNow();
587
+ if (monotonicNow >= lease.expiresAtMonotonic || dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase) && monotonicNow >= dispatch.deadlineMonotonic) {
588
+ endLease(lease);
589
+ }
590
+ };
591
+ const assertPublishable = () => {
592
+ requireOpen();
593
+ sweep();
594
+ if (blocked !== void 0 || lease !== void 0 && dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase)) {
595
+ throw new SpotPatchError2(ERROR_CODES2.EXTERNAL_AGENT_BUSY);
596
+ }
597
+ };
598
+ const requireLease = (leaseToken) => {
599
+ requireOpen();
600
+ sweep();
601
+ if (lease?.token !== leaseToken) {
602
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_LEASE_INVALID);
603
+ }
604
+ return lease;
605
+ };
606
+ return Object.freeze({
607
+ assertPublishable,
608
+ claim(adapterKind, connectorInstanceId, baselineCursor) {
609
+ requireOpen();
610
+ sweep();
611
+ if (blocked !== void 0) {
612
+ throw new SpotPatchError2(ERROR_CODES2.EXTERNAL_AGENT_BUSY);
613
+ }
614
+ if (lease !== void 0) {
615
+ if (lease.adapterKind === adapterKind && lease.connectorInstanceId === connectorInstanceId) {
616
+ lease.expiresAtMonotonic = clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
617
+ lease.updatedAt = nowIso();
618
+ return Object.freeze({
619
+ leaseToken: lease.token,
620
+ heartbeatIntervalMs: EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
621
+ baselineCursor: lease.baselineCursor,
622
+ activeAdapter: requirePresent(activeSummary())
623
+ });
624
+ }
625
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_CONFLICT);
626
+ }
627
+ const timestamp = nowIso();
628
+ lease = {
629
+ adapterKind,
630
+ baselineCursor,
631
+ connectedAt: timestamp,
632
+ connectorInstanceId,
633
+ token: randomId(),
634
+ expiresAtMonotonic: clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs,
635
+ updatedAt: timestamp
636
+ };
637
+ lastReleasedToken = void 0;
638
+ return Object.freeze({
639
+ leaseToken: lease.token,
640
+ heartbeatIntervalMs: EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
641
+ baselineCursor,
642
+ activeAdapter: requirePresent(activeSummary())
643
+ });
644
+ },
645
+ heartbeat(leaseToken) {
646
+ const activeLease = requireLease(leaseToken);
647
+ activeLease.expiresAtMonotonic = clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
648
+ activeLease.updatedAt = nowIso();
649
+ return state();
650
+ },
651
+ report(leaseToken, cursor, phase) {
652
+ const activeLease = requireLease(leaseToken);
653
+ if (dispatch?.cursor !== cursor || dispatch.adapterKind !== activeLease.adapterKind) {
654
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
655
+ }
656
+ if (dispatch.phase === phase) return state(cursor);
657
+ const allowed = ALLOWED_TRANSITIONS[dispatch.phase];
658
+ if (!allowed.includes(phase)) {
659
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
660
+ }
661
+ const updatedAt = nowIso();
662
+ dispatch.phase = phase;
663
+ dispatch.updatedAt = updatedAt;
664
+ activeLease.updatedAt = updatedAt;
665
+ if (phase === "delivery-unknown") {
666
+ blocked = Object.freeze({
667
+ adapterKind: activeLease.adapterKind,
668
+ connectedAt: activeLease.connectedAt,
669
+ updatedAt
670
+ });
671
+ lastReleasedToken = activeLease.token;
672
+ lease = void 0;
673
+ }
674
+ return state(cursor);
675
+ },
676
+ release(leaseToken) {
677
+ requireOpen();
678
+ sweep();
679
+ if (lease === void 0 && lastReleasedToken === leaseToken) return state();
680
+ const activeLease = lease;
681
+ if (activeLease?.token !== leaseToken) {
682
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_LEASE_INVALID);
683
+ }
684
+ endLease(activeLease);
685
+ return state();
686
+ },
687
+ reserve(cursor, revision) {
688
+ assertPublishable();
689
+ if (lease === void 0) return Object.freeze({ mode: "inbox" });
690
+ const updatedAt = nowIso();
691
+ dispatch = {
692
+ adapterKind: lease.adapterKind,
693
+ cursor,
694
+ deadlineMonotonic: clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeDispatchTimeoutMs,
695
+ revision,
696
+ phase: "queued",
697
+ updatedAt
698
+ };
699
+ lease.updatedAt = updatedAt;
700
+ return Object.freeze({
701
+ mode: "active",
702
+ adapter: requirePresent(activeSummary()),
703
+ dispatch: requirePresent(dispatchSummary())
704
+ });
705
+ },
706
+ resolveDelivery(cursor) {
707
+ requireOpen();
708
+ sweep();
709
+ const activeDispatch = dispatch;
710
+ if (blocked === void 0 || activeDispatch?.cursor !== cursor || activeDispatch.phase !== "delivery-unknown") {
711
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
712
+ }
713
+ blocked = void 0;
714
+ return state(cursor);
715
+ },
716
+ snapshot(cursor) {
717
+ requireOpen();
718
+ sweep();
719
+ return state(cursor);
720
+ },
721
+ close() {
722
+ if (closed) return;
723
+ closed = true;
724
+ blocked = void 0;
725
+ dispatch = void 0;
726
+ lease = void 0;
727
+ lastReleasedToken = void 0;
728
+ }
729
+ });
730
+ }
731
+
732
+ // src/external-handoff/broker.ts
733
+ import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
734
+ import {
735
+ createServer
736
+ } from "http";
737
+ import {
738
+ ERROR_CODES as ERROR_CODES4,
739
+ EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
740
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS2,
741
+ SpotPatchError as SpotPatchError4
742
+ } from "@spotpatch/shared";
743
+ import {
744
+ SPOTPATCH_BRIDGE_PATHS,
745
+ SPOTPATCH_BRIDGE_TOKEN_HEADER,
746
+ bridgeAckRequestSchema,
747
+ bridgeActiveClaimRequestSchema,
748
+ bridgeActiveHeartbeatRequestSchema,
749
+ bridgeActiveReleaseRequestSchema,
750
+ bridgeActiveReportRequestSchema,
751
+ bridgeCurrentRequestSchema,
752
+ bridgeStatusRequestSchema,
753
+ bridgeWaitRequestSchema
754
+ } from "@spotpatch/shared/external-agent-node";
755
+
756
+ // src/server/request-body.ts
757
+ import { ERROR_CODES as ERROR_CODES3, SpotPatchError as SpotPatchError3 } from "@spotpatch/shared";
758
+
759
+ // src/server/constants.ts
760
+ var MAX_REQUEST_BODY_BYTES = 32 * 1024;
761
+ var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
762
+ var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
763
+
764
+ // src/server/request-body.ts
765
+ function isJsonContentType(value) {
766
+ return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
767
+ }
768
+ async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
769
+ if (!isJsonContentType(request.headers["content-type"])) {
770
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
771
+ }
772
+ const declaredLength = Number(request.headers["content-length"]);
773
+ if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
774
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
775
+ }
776
+ const chunks = [];
777
+ let byteLength = 0;
778
+ let exceededLimit = false;
779
+ for await (const rawChunk of request) {
780
+ const chunk = rawChunk;
781
+ if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
782
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
783
+ }
784
+ const buffer = Buffer.from(chunk);
785
+ byteLength += buffer.byteLength;
786
+ if (byteLength > maximumBytes) {
787
+ exceededLimit = true;
788
+ continue;
789
+ }
790
+ chunks.push(buffer);
791
+ }
792
+ if (exceededLimit || byteLength === 0) {
793
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
794
+ }
795
+ try {
796
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
797
+ } catch (error) {
798
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST, void 0, {
799
+ cause: error
800
+ });
801
+ }
802
+ }
803
+
804
+ // src/external-handoff/broker.ts
805
+ function singleHeader(request, name) {
806
+ const value = request.headers[name.toLowerCase()];
807
+ return Array.isArray(value) ? void 0 : value;
808
+ }
809
+ function tokensMatch(actual, expected) {
810
+ if (actual === void 0) return false;
811
+ const actualBytes = Buffer.from(actual);
812
+ const expectedBytes = Buffer.from(expected);
813
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
814
+ }
815
+ function writeJson(response, status, payload) {
816
+ response.statusCode = status;
817
+ response.setHeader("Cache-Control", "no-store");
818
+ response.setHeader("Connection", "close");
819
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
820
+ response.setHeader("X-Content-Type-Options", "nosniff");
821
+ response.end(JSON.stringify(payload));
822
+ }
823
+ function statusForError(code) {
824
+ if (code === ERROR_CODES4.BRIDGE_UNAUTHORIZED) return 401;
825
+ if (code === ERROR_CODES4.HANDOFF_NOT_FOUND) return 404;
826
+ if (code === ERROR_CODES4.HANDOFF_EXPIRED || code === ERROR_CODES4.SESSION_CLOSED) {
827
+ return 410;
828
+ }
829
+ if (code === ERROR_CODES4.BRIDGE_BUSY) return 429;
830
+ if (code === ERROR_CODES4.ACTIVE_ADAPTER_LEASE_INVALID) return 401;
831
+ if (code === ERROR_CODES4.HANDOFF_RESPONSE_TOO_LARGE) return 413;
832
+ if (code === ERROR_CODES4.HANDOFF_CURSOR_INVALID || code === ERROR_CODES4.BRIDGE_PROTOCOL_MISMATCH || code === ERROR_CODES4.EXTERNAL_AGENT_BUSY || code === ERROR_CODES4.ACTIVE_ADAPTER_CONFLICT || code === ERROR_CODES4.ACTIVE_DISPATCH_INVALID) {
833
+ return 409;
834
+ }
835
+ if (code === ERROR_CODES4.INVALID_REQUEST) return 400;
836
+ return 500;
837
+ }
838
+ function normalizeError2(error) {
839
+ return error instanceof SpotPatchError4 ? error : new SpotPatchError4(ERROR_CODES4.INTERNAL_ERROR, void 0, { cause: error });
840
+ }
841
+ function assertAuthorized(request, expectedHost, bridgeToken) {
842
+ if (request.socket.remoteAddress !== "127.0.0.1" || singleHeader(request, "host") !== expectedHost || !tokensMatch(singleHeader(request, SPOTPATCH_BRIDGE_TOKEN_HEADER), bridgeToken)) {
843
+ throw new SpotPatchError4(ERROR_CODES4.BRIDGE_UNAUTHORIZED);
844
+ }
845
+ }
846
+ async function closeServer(server, sockets) {
847
+ await new Promise((resolve) => {
848
+ server.close(() => {
849
+ resolve();
850
+ });
851
+ for (const socket of sockets) {
852
+ socket.destroy();
853
+ }
854
+ });
855
+ }
856
+ async function createExternalHandoffBroker(options) {
857
+ const bridgeToken = randomBytes3(32).toString("base64url");
858
+ const sockets = /* @__PURE__ */ new Set();
859
+ let expectedHost = "";
860
+ const server = createServer(
861
+ { maxHeaderSize: EXTERNAL_HANDOFF_LIMITS2.maximumBrokerHeaderBytes },
862
+ (request, response) => {
863
+ const handle = async () => {
864
+ assertAuthorized(request, expectedHost, bridgeToken);
865
+ if (request.method !== "POST") {
866
+ throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
867
+ }
868
+ const body = await readJsonRequestBody(
869
+ request,
870
+ EXTERNAL_HANDOFF_LIMITS2.maximumBrokerRequestBytes
871
+ );
872
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.status) {
873
+ const parsed = bridgeStatusRequestSchema.safeParse(body);
874
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
875
+ let current = null;
876
+ try {
877
+ current = options.store.status();
878
+ } catch (error) {
879
+ if (!(error instanceof SpotPatchError4) || error.code !== ERROR_CODES4.HANDOFF_NOT_FOUND) {
880
+ throw error;
881
+ }
882
+ }
883
+ writeJson(response, 200, {
884
+ ok: true,
885
+ data: Object.freeze({
886
+ brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
887
+ projectKey: options.projectKey,
888
+ sessionId: options.sessionId,
889
+ framework: options.framework,
890
+ current
891
+ })
892
+ });
893
+ return;
894
+ }
895
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.current) {
896
+ const parsed = bridgeCurrentRequestSchema.safeParse(body);
897
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
898
+ const snapshot2 = options.store.current(parsed.data.cursor);
899
+ writeJson(response, 200, {
900
+ ok: true,
901
+ data: Object.freeze({ outcome: "handoff", snapshot: snapshot2 })
902
+ });
903
+ return;
904
+ }
905
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.ack) {
906
+ const parsed = bridgeAckRequestSchema.safeParse(body);
907
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
908
+ const summary = options.store.ack(
909
+ parsed.data.cursor,
910
+ parsed.data.connectorInstanceId
911
+ );
912
+ writeJson(response, 200, {
913
+ ok: true,
914
+ data: Object.freeze({ summary })
915
+ });
916
+ return;
917
+ }
918
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.wait) {
919
+ const parsed = bridgeWaitRequestSchema.safeParse(body);
920
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
921
+ const controller = new AbortController();
922
+ const abort = () => {
923
+ if (!response.writableEnded) controller.abort("bridge-client-closed");
924
+ };
925
+ response.once("close", abort);
926
+ try {
927
+ const data = await options.store.wait(
928
+ parsed.data.afterCursor,
929
+ parsed.data.timeoutMs,
930
+ controller.signal
931
+ );
932
+ writeJson(response, 200, { ok: true, data });
933
+ } finally {
934
+ response.removeListener("close", abort);
935
+ }
936
+ return;
937
+ }
938
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.activeClaim) {
939
+ const parsed = bridgeActiveClaimRequestSchema.safeParse(body);
940
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
941
+ const data = options.activeRegistry.claim(
942
+ parsed.data.adapterKind,
943
+ parsed.data.connectorInstanceId,
944
+ options.store.currentCursor()
945
+ );
946
+ writeJson(response, 200, { ok: true, data });
947
+ return;
948
+ }
949
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.activeHeartbeat) {
950
+ const parsed = bridgeActiveHeartbeatRequestSchema.safeParse(body);
951
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
952
+ const data = options.activeRegistry.heartbeat(parsed.data.leaseToken);
953
+ writeJson(response, 200, { ok: true, data });
954
+ return;
955
+ }
956
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.activeReport) {
957
+ const parsed = bridgeActiveReportRequestSchema.safeParse(body);
958
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
959
+ const data = options.activeRegistry.report(
960
+ parsed.data.leaseToken,
961
+ parsed.data.cursor,
962
+ parsed.data.phase
963
+ );
964
+ writeJson(response, 200, { ok: true, data });
965
+ return;
966
+ }
967
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.activeRelease) {
968
+ const parsed = bridgeActiveReleaseRequestSchema.safeParse(body);
969
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
970
+ const data = options.activeRegistry.release(parsed.data.leaseToken);
971
+ writeJson(response, 200, { ok: true, data });
972
+ return;
973
+ }
974
+ throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
975
+ };
976
+ void handle().catch((error) => {
977
+ if (response.writableEnded || response.destroyed) return;
978
+ const normalized = normalizeError2(error);
979
+ if (normalized.code === ERROR_CODES4.BRIDGE_BUSY) {
980
+ response.setHeader("Retry-After", "1");
981
+ }
982
+ writeJson(response, statusForError(normalized.code), {
983
+ ok: false,
984
+ error: {
985
+ code: normalized.code,
986
+ message: "The local SpotPatch bridge request failed."
987
+ }
988
+ });
989
+ });
990
+ }
991
+ );
992
+ server.maxConnections = EXTERNAL_HANDOFF_LIMITS2.maximumBrokerSockets;
993
+ server.headersTimeout = 5e3;
994
+ server.requestTimeout = EXTERNAL_HANDOFF_LIMITS2.maximumWaitMs + 5e3;
995
+ server.keepAliveTimeout = 1;
996
+ server.on("connection", (socket) => {
997
+ if (sockets.size >= EXTERNAL_HANDOFF_LIMITS2.maximumBrokerSockets) {
998
+ socket.destroy();
999
+ return;
1000
+ }
1001
+ sockets.add(socket);
1002
+ socket.once("close", () => sockets.delete(socket));
1003
+ });
1004
+ await new Promise((resolve, reject) => {
1005
+ server.once("error", reject);
1006
+ server.listen(0, "127.0.0.1", resolve);
1007
+ });
1008
+ const address = server.address();
1009
+ if (address === null || typeof address === "string" || address.address !== "127.0.0.1") {
1010
+ await closeServer(server, sockets);
1011
+ throw new Error("SpotPatch external Agent broker did not bind IPv4 loopback.");
1012
+ }
1013
+ expectedHost = `127.0.0.1:${String(address.port)}`;
1014
+ let closed = false;
1015
+ let ready = true;
1016
+ server.removeAllListeners("error");
1017
+ server.on("error", () => {
1018
+ ready = false;
1019
+ for (const socket of sockets) socket.destroy();
1020
+ });
1021
+ return Object.freeze({
1022
+ bridgeToken,
1023
+ endpoint: `http://${expectedHost}`,
1024
+ isReady: () => ready && !closed,
1025
+ async close() {
1026
+ if (closed) return;
1027
+ closed = true;
1028
+ ready = false;
1029
+ await closeServer(server, sockets);
1030
+ }
1031
+ });
1032
+ }
1033
+
1034
+ // src/external-handoff/discovery.ts
1035
+ import { randomBytes as randomBytes4 } from "crypto";
1036
+ import { lstat, open, rename, unlink } from "fs/promises";
1037
+ import path from "path";
1038
+ import {
1039
+ EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION as EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION2,
1040
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS3
1041
+ } from "@spotpatch/shared";
1042
+ import {
1043
+ computeExternalHandoffProjectKey,
1044
+ externalHandoffDescriptorSchema,
1045
+ resolveExternalHandoffRuntimeDirectory
1046
+ } from "@spotpatch/shared/external-agent-node";
1047
+ async function syncDirectory(directory) {
1048
+ const handle = await open(directory, "r");
1049
+ try {
1050
+ await handle.sync();
1051
+ } catch (error) {
1052
+ const code = error.code;
1053
+ if (code !== "EINVAL" && code !== "ENOTSUP") {
1054
+ throw error;
1055
+ }
1056
+ } finally {
1057
+ await handle.close();
1058
+ }
1059
+ }
1060
+ async function publishExternalHandoffDescriptor(options) {
1061
+ const directory = await resolveExternalHandoffRuntimeDirectory(true);
1062
+ const descriptor = externalHandoffDescriptorSchema.parse({
1063
+ schemaVersion: 1,
1064
+ brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION2,
1065
+ projectKey: await computeExternalHandoffProjectKey(options.root),
1066
+ sessionId: options.sessionId,
1067
+ framework: options.framework,
1068
+ endpoint: options.endpoint,
1069
+ bridgeToken: options.bridgeToken,
1070
+ pid: process.pid,
1071
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1072
+ });
1073
+ const serialized = JSON.stringify(descriptor);
1074
+ if (Buffer.byteLength(serialized, "utf8") > EXTERNAL_HANDOFF_LIMITS3.maximumDescriptorBytes) {
1075
+ throw new RangeError("SpotPatch external Agent descriptor exceeds its limit.");
1076
+ }
1077
+ const destination = path.join(directory, `${descriptor.sessionId}.json`);
1078
+ const temporary = path.join(
1079
+ directory,
1080
+ `.${descriptor.sessionId}.${randomBytes4(8).toString("hex")}.tmp`
1081
+ );
1082
+ let temporaryExists = false;
1083
+ let published = false;
1084
+ let descriptorIdentity = Object.freeze({
1085
+ device: -1,
1086
+ inode: -1
1087
+ });
1088
+ try {
1089
+ const handle = await open(temporary, "wx", 384);
1090
+ temporaryExists = true;
1091
+ try {
1092
+ await handle.writeFile(serialized, "utf8");
1093
+ await handle.sync();
1094
+ } finally {
1095
+ await handle.close();
1096
+ }
1097
+ await rename(temporary, destination);
1098
+ temporaryExists = false;
1099
+ published = true;
1100
+ const status = await lstat(destination);
1101
+ const uid = process.getuid?.();
1102
+ if (!status.isFile() || status.isSymbolicLink() || uid === void 0 || status.uid !== uid || (status.mode & 63) !== 0) {
1103
+ throw new Error("SpotPatch external Agent descriptor is not private.");
1104
+ }
1105
+ descriptorIdentity = Object.freeze({ device: status.dev, inode: status.ino });
1106
+ await syncDirectory(directory);
1107
+ } catch (error) {
1108
+ if (temporaryExists) {
1109
+ await unlink(temporary).catch(() => void 0);
1110
+ }
1111
+ if (published) {
1112
+ await unlink(destination).catch(() => void 0);
1113
+ }
1114
+ throw error;
1115
+ }
1116
+ let closed = false;
1117
+ return Object.freeze({
1118
+ descriptor,
1119
+ async close() {
1120
+ if (closed) return;
1121
+ closed = true;
1122
+ await lstat(destination).then(async (status) => {
1123
+ if (status.dev === descriptorIdentity.device && status.ino === descriptorIdentity.inode) {
1124
+ await unlink(destination);
1125
+ }
1126
+ }).catch((error) => {
1127
+ if (error.code !== "ENOENT") {
1128
+ throw error;
1129
+ }
1130
+ });
1131
+ await syncDirectory(directory);
1132
+ }
1133
+ });
1134
+ }
1135
+
1136
+ // src/external-handoff/fingerprint.ts
1137
+ import { createHash as createHash2 } from "crypto";
1138
+ function canonicalJson(value) {
1139
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
1140
+ return JSON.stringify(value);
1141
+ }
1142
+ if (typeof value === "number") {
1143
+ if (!Number.isFinite(value)) throw new TypeError("Non-finite JSON number.");
1144
+ return JSON.stringify(value);
1145
+ }
1146
+ if (Array.isArray(value)) {
1147
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
1148
+ }
1149
+ if (typeof value === "object") {
1150
+ const record = value;
1151
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
1152
+ }
1153
+ throw new TypeError("Unsupported JSON value.");
1154
+ }
1155
+ function fingerprintExternalHandoffAnnotation(annotation) {
1156
+ return createHash2("sha256").update(canonicalJson(annotation)).digest("hex");
1157
+ }
1158
+
1159
+ // src/external-handoff/store.ts
1160
+ import { randomBytes as randomBytes5 } from "crypto";
1161
+ import {
1162
+ ERROR_CODES as ERROR_CODES5,
1163
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS4,
1164
+ EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
1165
+ SpotPatchError as SpotPatchError5
1166
+ } from "@spotpatch/shared";
1167
+ function defaultRandomId2() {
1168
+ return randomBytes5(24).toString("base64url");
1169
+ }
1170
+ function pageSummary(annotation) {
1171
+ let origin = "[unavailable]";
1172
+ try {
1173
+ const url = new URL(annotation.page.url);
1174
+ origin = url.origin === "null" ? "[unavailable]" : url.origin;
1175
+ } catch {
1176
+ }
1177
+ return Object.freeze({ origin, pathname: annotation.page.pathname });
1178
+ }
1179
+ function summaryOf(current, state) {
1180
+ const snapshot2 = current.snapshot;
1181
+ return Object.freeze({
1182
+ sessionId: snapshot2.session.id,
1183
+ framework: snapshot2.session.framework,
1184
+ revision: snapshot2.revision,
1185
+ cursor: snapshot2.cursor,
1186
+ targetCount: snapshot2.annotation.targets.length,
1187
+ page: pageSummary(snapshot2.annotation),
1188
+ publishedAt: snapshot2.publishedAt,
1189
+ expiresAt: snapshot2.expiresAt,
1190
+ state,
1191
+ pickupCount: current.receipts.size,
1192
+ ...current.pickedUpAt === void 0 ? {} : { pickedUpAt: current.pickedUpAt }
1193
+ });
1194
+ }
1195
+ function replayResult(record) {
1196
+ return Object.freeze({ ...record.result, replayed: true });
1197
+ }
1198
+ function createExternalHandoffStore(options) {
1199
+ const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
1200
+ const randomId = options.randomId ?? defaultRandomId2;
1201
+ const history = [];
1202
+ const idempotency = /* @__PURE__ */ new Map();
1203
+ const waiters = /* @__PURE__ */ new Set();
1204
+ let closed = false;
1205
+ let current;
1206
+ let revision = 0;
1207
+ const requireOpen = () => {
1208
+ if (closed) throw new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED);
1209
+ };
1210
+ const archive = (state) => {
1211
+ if (current === void 0) return;
1212
+ history.unshift(summaryOf(current, state));
1213
+ history.length = Math.min(
1214
+ history.length,
1215
+ EXTERNAL_HANDOFF_LIMITS4.maximumHistorySummaries
1216
+ );
1217
+ current = void 0;
1218
+ };
1219
+ const sweep = () => {
1220
+ const monotonicNow = clock.monotonicNow();
1221
+ if (current !== void 0 && monotonicNow >= current.expiresAtMonotonic) {
1222
+ archive("expired");
1223
+ }
1224
+ for (const [requestId, record] of idempotency) {
1225
+ if (monotonicNow >= record.expiresAtMonotonic) idempotency.delete(requestId);
1226
+ }
1227
+ };
1228
+ const knownSummary = (cursor) => {
1229
+ sweep();
1230
+ if (current?.snapshot.cursor === cursor) {
1231
+ return summaryOf(current, "available");
1232
+ }
1233
+ return history.find((summary) => summary.cursor === cursor);
1234
+ };
1235
+ const readCurrent = (cursor) => {
1236
+ requireOpen();
1237
+ sweep();
1238
+ if (current === void 0) {
1239
+ const prior = cursor === void 0 ? void 0 : knownSummary(cursor);
1240
+ throw new SpotPatchError5(
1241
+ prior?.state === "expired" ? ERROR_CODES5.HANDOFF_EXPIRED : cursor === void 0 ? ERROR_CODES5.HANDOFF_NOT_FOUND : ERROR_CODES5.HANDOFF_CURSOR_INVALID
1242
+ );
1243
+ }
1244
+ if (cursor !== void 0 && cursor !== current.snapshot.cursor) {
1245
+ const prior = knownSummary(cursor);
1246
+ throw new SpotPatchError5(
1247
+ prior?.state === "expired" ? ERROR_CODES5.HANDOFF_EXPIRED : ERROR_CODES5.HANDOFF_CURSOR_INVALID
1248
+ );
1249
+ }
1250
+ return current.snapshot;
1251
+ };
1252
+ const findReplay = (requestId, fingerprint) => {
1253
+ requireOpen();
1254
+ sweep();
1255
+ const record = idempotency.get(requestId);
1256
+ if (record === void 0) return void 0;
1257
+ if (record.fingerprint !== fingerprint) {
1258
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_VALIDATION_FAILED);
1259
+ }
1260
+ return replayResult(record);
1261
+ };
1262
+ const settleWaiters = (result) => {
1263
+ const pending = [...waiters];
1264
+ waiters.clear();
1265
+ for (const waiter of pending) waiter.resolve(result);
1266
+ };
1267
+ return Object.freeze({
1268
+ activeWaitCount: () => waiters.size,
1269
+ replay: findReplay,
1270
+ publish(input) {
1271
+ requireOpen();
1272
+ const replayed = findReplay(input.requestId, input.fingerprint);
1273
+ if (replayed !== void 0) return replayed;
1274
+ if (idempotency.size >= EXTERNAL_HANDOFF_LIMITS4.maximumRequestIdRecords) {
1275
+ throw new SpotPatchError5(ERROR_CODES5.EXTERNAL_HANDOFF_UNAVAILABLE);
1276
+ }
1277
+ const nextRevision = revision + 1;
1278
+ const publishedAtMs = clock.wallNow();
1279
+ const publishedAtMonotonic = clock.monotonicNow();
1280
+ const snapshot2 = Object.freeze({
1281
+ schemaVersion: EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
1282
+ cursor: randomId(),
1283
+ session: Object.freeze({ id: options.sessionId, framework: options.framework }),
1284
+ revision: nextRevision,
1285
+ publishedAt: new Date(publishedAtMs).toISOString(),
1286
+ expiresAt: new Date(
1287
+ publishedAtMs + EXTERNAL_HANDOFF_LIMITS4.handoffTtlMs
1288
+ ).toISOString(),
1289
+ annotation: input.annotation
1290
+ });
1291
+ if (Buffer.byteLength(JSON.stringify(snapshot2), "utf8") > EXTERNAL_HANDOFF_LIMITS4.maximumSnapshotBytes) {
1292
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_RESPONSE_TOO_LARGE);
1293
+ }
1294
+ const delivery = input.reserve(snapshot2.cursor, nextRevision);
1295
+ if (current !== void 0) archive("superseded");
1296
+ revision = nextRevision;
1297
+ current = {
1298
+ expiresAtMonotonic: publishedAtMonotonic + EXTERNAL_HANDOFF_LIMITS4.handoffTtlMs,
1299
+ receipts: /* @__PURE__ */ new Set(),
1300
+ snapshot: snapshot2
1301
+ };
1302
+ const result = Object.freeze({
1303
+ handoff: summaryOf(current, "available"),
1304
+ delivery,
1305
+ replayed: false
1306
+ });
1307
+ idempotency.set(
1308
+ input.requestId,
1309
+ Object.freeze({
1310
+ expiresAtMonotonic: publishedAtMonotonic + EXTERNAL_HANDOFF_LIMITS4.requestIdTtlMs,
1311
+ fingerprint: input.fingerprint,
1312
+ result
1313
+ })
1314
+ );
1315
+ settleWaiters(Object.freeze({ outcome: "handoff", snapshot: snapshot2 }));
1316
+ return result;
1317
+ },
1318
+ current: readCurrent,
1319
+ currentCursor() {
1320
+ requireOpen();
1321
+ sweep();
1322
+ return current?.snapshot.cursor ?? null;
1323
+ },
1324
+ status(cursor) {
1325
+ requireOpen();
1326
+ sweep();
1327
+ if (cursor === void 0) {
1328
+ if (current === void 0) {
1329
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_NOT_FOUND);
1330
+ }
1331
+ return summaryOf(current, "available");
1332
+ }
1333
+ const summary = knownSummary(cursor);
1334
+ if (summary === void 0) {
1335
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
1336
+ }
1337
+ return summary;
1338
+ },
1339
+ ack(cursor, connectorInstanceId) {
1340
+ const snapshot2 = readCurrent(cursor);
1341
+ if (snapshot2 !== current?.snapshot) {
1342
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
1343
+ }
1344
+ if (!current.receipts.has(connectorInstanceId)) {
1345
+ if (current.receipts.size >= EXTERNAL_HANDOFF_LIMITS4.maximumConnectorReceipts) {
1346
+ throw new SpotPatchError5(ERROR_CODES5.BRIDGE_BUSY);
1347
+ }
1348
+ current.receipts.add(connectorInstanceId);
1349
+ current.pickedUpAt = new Date(clock.wallNow()).toISOString();
1350
+ }
1351
+ return summaryOf(current, "available");
1352
+ },
1353
+ async wait(afterCursor, timeoutMs, signal) {
1354
+ requireOpen();
1355
+ sweep();
1356
+ if (afterCursor === void 0 && current !== void 0) {
1357
+ return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
1358
+ }
1359
+ if (afterCursor !== void 0) {
1360
+ const known = knownSummary(afterCursor);
1361
+ if (known === void 0) {
1362
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
1363
+ }
1364
+ if (current !== void 0 && current.snapshot.cursor !== afterCursor) {
1365
+ return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
1366
+ }
1367
+ }
1368
+ if (waiters.size >= EXTERNAL_HANDOFF_LIMITS4.maximumWaiters) {
1369
+ throw new SpotPatchError5(ERROR_CODES5.BRIDGE_BUSY);
1370
+ }
1371
+ if (signal.aborted) throw new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED);
1372
+ return new Promise((resolve, reject) => {
1373
+ let settled = false;
1374
+ const finish = () => {
1375
+ if (settled) return false;
1376
+ settled = true;
1377
+ waiters.delete(waiter);
1378
+ clearTimeout(timer);
1379
+ signal.removeEventListener("abort", abort);
1380
+ return true;
1381
+ };
1382
+ const waiter = {
1383
+ reject(error) {
1384
+ if (finish()) reject(error);
1385
+ },
1386
+ resolve(result) {
1387
+ if (finish()) resolve(result);
1388
+ }
1389
+ };
1390
+ const abort = () => {
1391
+ waiter.reject(new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED));
1392
+ };
1393
+ const timer = setTimeout(() => {
1394
+ waiter.resolve(Object.freeze({ outcome: "timeout" }));
1395
+ }, timeoutMs);
1396
+ timer.unref();
1397
+ signal.addEventListener("abort", abort, { once: true });
1398
+ waiters.add(waiter);
1399
+ });
1400
+ },
1401
+ close() {
1402
+ if (closed) return;
1403
+ closed = true;
1404
+ current = void 0;
1405
+ history.length = 0;
1406
+ idempotency.clear();
1407
+ const pending = [...waiters];
1408
+ waiters.clear();
1409
+ for (const waiter of pending) {
1410
+ waiter.reject(new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED));
1411
+ }
1412
+ }
1413
+ });
1414
+ }
1415
+
1416
+ // src/external-handoff/service.ts
1417
+ function asReplay(result) {
1418
+ return Object.freeze({ ...result, replayed: true });
1419
+ }
1420
+ function createExternalHandoffService(options) {
1421
+ const activeRegistry = createActiveAdapterRegistry();
1422
+ const store = createExternalHandoffStore({
1423
+ framework: options.framework,
1424
+ sessionId: options.sessionId
1425
+ });
1426
+ const inFlight = /* @__PURE__ */ new Map();
1427
+ let broker;
1428
+ let descriptor;
1429
+ let startPromise;
1430
+ let closePromise;
1431
+ let state = "idle";
1432
+ const isClosed = () => state === "closed";
1433
+ const requireReady = () => {
1434
+ if (state !== "ready" || broker?.isReady() !== true) {
1435
+ throw new SpotPatchError6(
1436
+ state === "closed" ? ERROR_CODES6.SESSION_CLOSED : ERROR_CODES6.EXTERNAL_HANDOFF_UNAVAILABLE
1437
+ );
1438
+ }
1439
+ };
1440
+ const start = async () => {
1441
+ if (state === "ready") return;
1442
+ if (state === "closed") throw new SpotPatchError6(ERROR_CODES6.SESSION_CLOSED);
1443
+ if (startPromise !== void 0) return startPromise;
1444
+ state = "starting";
1445
+ startPromise = (async () => {
1446
+ let createdBroker;
1447
+ let createdDescriptor;
1448
+ try {
1449
+ const projectKey = await computeExternalHandoffProjectKey2(options.root);
1450
+ createdBroker = await createExternalHandoffBroker({
1451
+ activeRegistry,
1452
+ framework: options.framework,
1453
+ projectKey,
1454
+ sessionId: options.sessionId,
1455
+ store
1456
+ });
1457
+ createdDescriptor = await publishExternalHandoffDescriptor({
1458
+ bridgeToken: createdBroker.bridgeToken,
1459
+ endpoint: createdBroker.endpoint,
1460
+ framework: options.framework,
1461
+ root: options.root,
1462
+ sessionId: options.sessionId
1463
+ });
1464
+ if (isClosed()) {
1465
+ await createdDescriptor.close();
1466
+ await createdBroker.close();
1467
+ throw new SpotPatchError6(ERROR_CODES6.SESSION_CLOSED);
1468
+ }
1469
+ broker = createdBroker;
1470
+ descriptor = createdDescriptor;
1471
+ state = "ready";
1472
+ } catch (error) {
1473
+ if (createdDescriptor !== void 0 && descriptor !== createdDescriptor) {
1474
+ await createdDescriptor.close().catch(() => void 0);
1475
+ }
1476
+ if (createdBroker !== void 0 && broker !== createdBroker) {
1477
+ await createdBroker.close().catch(() => void 0);
1478
+ }
1479
+ if (!isClosed()) state = "failed";
1480
+ throw error;
1481
+ }
1482
+ })();
1483
+ return startPromise;
1484
+ };
1485
+ return Object.freeze({
1486
+ start,
1487
+ capability() {
1488
+ const currentCursor = store.currentCursor();
1489
+ const active = activeRegistry.snapshot(currentCursor ?? void 0);
1490
+ return Object.freeze({
1491
+ enabled: true,
1492
+ brokerReady: state === "ready" && broker?.isReady() === true,
1493
+ activeWaitCount: store.activeWaitCount(),
1494
+ snapshotSchemaVersion: EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION2,
1495
+ brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION3,
1496
+ activeAdapter: active.activeAdapter,
1497
+ dispatch: currentCursor === null ? null : active.dispatch
1498
+ });
1499
+ },
1500
+ async publish(request, authorize) {
1501
+ requireReady();
1502
+ const fingerprint = fingerprintExternalHandoffAnnotation(request.annotation);
1503
+ const replayed = store.replay(request.requestId, fingerprint);
1504
+ if (replayed !== void 0) return replayed;
1505
+ const pending = inFlight.get(request.requestId);
1506
+ if (pending !== void 0) {
1507
+ if (pending.fingerprint !== fingerprint) {
1508
+ throw new SpotPatchError6(ERROR_CODES6.HANDOFF_VALIDATION_FAILED);
1509
+ }
1510
+ return asReplay(await pending.promise);
1511
+ }
1512
+ activeRegistry.assertPublishable();
1513
+ const promise = (async () => {
1514
+ const annotation = await authorize(request.annotation);
1515
+ return store.publish({
1516
+ annotation,
1517
+ fingerprint,
1518
+ requestId: request.requestId,
1519
+ reserve: activeRegistry.reserve
1520
+ });
1521
+ })();
1522
+ const activePublish = Object.freeze({ fingerprint, promise });
1523
+ inFlight.set(request.requestId, activePublish);
1524
+ try {
1525
+ return await promise;
1526
+ } finally {
1527
+ if (inFlight.get(request.requestId) === activePublish) {
1528
+ inFlight.delete(request.requestId);
1529
+ }
1530
+ }
1531
+ },
1532
+ status(cursor) {
1533
+ requireReady();
1534
+ const handoff = store.status(cursor);
1535
+ const active = activeRegistry.snapshot(cursor ?? handoff.cursor);
1536
+ return Object.freeze({
1537
+ handoff,
1538
+ activeAdapter: active.activeAdapter,
1539
+ dispatch: active.dispatch
1540
+ });
1541
+ },
1542
+ resolveDelivery(cursor) {
1543
+ requireReady();
1544
+ const handoff = store.status(cursor);
1545
+ const active = activeRegistry.resolveDelivery(cursor);
1546
+ return Object.freeze({
1547
+ handoff,
1548
+ activeAdapter: active.activeAdapter,
1549
+ dispatch: active.dispatch
1550
+ });
1551
+ },
1552
+ close() {
1553
+ closePromise ??= (async () => {
1554
+ if (state === "closed") return;
1555
+ state = "closed";
1556
+ activeRegistry.close();
1557
+ store.close();
1558
+ inFlight.clear();
1559
+ await startPromise?.catch(() => void 0);
1560
+ const publishedDescriptor = descriptor;
1561
+ descriptor = void 0;
1562
+ const activeBroker = broker;
1563
+ broker = void 0;
1564
+ await publishedDescriptor?.close().catch(() => void 0);
1565
+ await activeBroker?.close().catch(() => void 0);
1566
+ })();
1567
+ return closePromise;
1568
+ }
1569
+ });
1570
+ }
1571
+
475
1572
  // src/environment-ai.ts
476
1573
  var AI_ENVIRONMENT_NAMES = Object.freeze({
477
1574
  authentication: "SPOTPATCH_AI_AUTHENTICATION",
@@ -562,32 +1659,32 @@ function resolveEnvironmentAiConfiguration(environment) {
562
1659
  }
563
1660
 
564
1661
  // src/integration/file-plan.ts
565
- import { randomBytes as randomBytes2 } from "crypto";
1662
+ import { randomBytes as randomBytes6 } from "crypto";
566
1663
  import {
567
1664
  access,
568
- lstat,
1665
+ lstat as lstat2,
569
1666
  mkdir,
570
1667
  readFile,
571
1668
  realpath,
572
- rename,
1669
+ rename as rename2,
573
1670
  stat,
574
- unlink,
1671
+ unlink as unlink2,
575
1672
  writeFile
576
1673
  } from "fs/promises";
577
- import path from "path";
1674
+ import path2 from "path";
578
1675
  function isMissingPathError(error) {
579
1676
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
580
1677
  }
581
1678
  function isPathWithin(root, target) {
582
- const relative = path.relative(root, target);
583
- return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
1679
+ const relative = path2.relative(root, target);
1680
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
584
1681
  }
585
1682
  function relativePathWithin(root, target) {
586
- const relative = path.relative(root, target);
1683
+ const relative = path2.relative(root, target);
587
1684
  if (relative.length === 0 || !isPathWithin(root, target)) {
588
1685
  throw new Error("SpotPatch init refuses to modify a path outside the app root.");
589
1686
  }
590
- return relative.split(path.sep).join("/");
1687
+ return relative.split(path2.sep).join("/");
591
1688
  }
592
1689
  async function integrationPathExists(absolutePath) {
593
1690
  try {
@@ -598,10 +1695,10 @@ async function integrationPathExists(absolutePath) {
598
1695
  }
599
1696
  }
600
1697
  async function readIntegrationFile(absolutePath) {
601
- const metadata = await lstat(absolutePath);
1698
+ const metadata = await lstat2(absolutePath);
602
1699
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
603
1700
  throw new Error(
604
- `SpotPatch refuses to modify the non-regular file ${path.basename(absolutePath)}.`
1701
+ `SpotPatch refuses to modify the non-regular file ${path2.basename(absolutePath)}.`
605
1702
  );
606
1703
  }
607
1704
  return readFile(absolutePath, "utf8");
@@ -610,8 +1707,8 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
610
1707
  if (previousContent === nextContent) {
611
1708
  return void 0;
612
1709
  }
613
- const root = path.resolve(appRoot);
614
- const target = path.resolve(absolutePath);
1710
+ const root = path2.resolve(appRoot);
1711
+ const target = path2.resolve(absolutePath);
615
1712
  return Object.freeze({
616
1713
  absolutePath: target,
617
1714
  nextContent,
@@ -620,19 +1717,19 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
620
1717
  });
621
1718
  }
622
1719
  function temporaryPath(absolutePath, label) {
623
- return path.join(
624
- path.dirname(absolutePath),
625
- `.${path.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes2(8).toString("hex")}`
1720
+ return path2.join(
1721
+ path2.dirname(absolutePath),
1722
+ `.${path2.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes6(8).toString("hex")}`
626
1723
  );
627
1724
  }
628
1725
  async function writeAtomic(absolutePath, content, mode) {
629
- await mkdir(path.dirname(absolutePath), { recursive: true });
1726
+ await mkdir(path2.dirname(absolutePath), { recursive: true });
630
1727
  const stagedPath = temporaryPath(absolutePath, "stage");
631
1728
  try {
632
1729
  await writeFile(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
633
- await rename(stagedPath, absolutePath);
1730
+ await rename2(stagedPath, absolutePath);
634
1731
  } catch (error) {
635
- await unlink(stagedPath).catch(() => void 0);
1732
+ await unlink2(stagedPath).catch(() => void 0);
636
1733
  throw error;
637
1734
  }
638
1735
  }
@@ -644,21 +1741,21 @@ async function rollbackChange(change) {
644
1741
  );
645
1742
  }
646
1743
  if (change.previousContent === void 0) {
647
- await unlink(change.absolutePath);
1744
+ await unlink2(change.absolutePath);
648
1745
  return;
649
1746
  }
650
1747
  const mode = (await stat(change.absolutePath)).mode & 511;
651
1748
  await writeAtomic(change.absolutePath, change.previousContent, mode);
652
1749
  }
653
1750
  async function assertSafeTarget(appRoot, realAppRoot, change) {
654
- const target = path.resolve(change.absolutePath);
1751
+ const target = path2.resolve(change.absolutePath);
655
1752
  const relativePath = relativePathWithin(appRoot, target);
656
- if (target !== change.absolutePath || relativePath !== change.relativePath || path.dirname(target) === target) {
1753
+ if (target !== change.absolutePath || relativePath !== change.relativePath || path2.dirname(target) === target) {
657
1754
  throw new Error("SpotPatch init received an invalid integration file plan.");
658
1755
  }
659
1756
  let targetMetadata;
660
1757
  try {
661
- targetMetadata = await lstat(target);
1758
+ targetMetadata = await lstat2(target);
662
1759
  } catch (error) {
663
1760
  if (!isMissingPathError(error)) {
664
1761
  throw error;
@@ -670,7 +1767,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
670
1767
  );
671
1768
  }
672
1769
  const containmentAnchor = await realpath(
673
- targetMetadata === void 0 ? path.dirname(target) : target
1770
+ targetMetadata === void 0 ? path2.dirname(target) : target
674
1771
  );
675
1772
  if (!isPathWithin(realAppRoot, containmentAnchor)) {
676
1773
  throw new Error("SpotPatch init refuses to modify a path outside the app root.");
@@ -679,7 +1776,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
679
1776
  async function assertCurrentBaseline(change) {
680
1777
  if (change.previousContent === void 0) {
681
1778
  try {
682
- await lstat(change.absolutePath);
1779
+ await lstat2(change.absolutePath);
683
1780
  } catch (error) {
684
1781
  if (isMissingPathError(error)) {
685
1782
  return;
@@ -701,7 +1798,7 @@ async function applyIntegrationPlan(plan) {
701
1798
  if (plan.changes.length === 0) {
702
1799
  return;
703
1800
  }
704
- const appRoot = path.resolve(plan.appRoot);
1801
+ const appRoot = path2.resolve(plan.appRoot);
705
1802
  const realAppRoot = await realpath(appRoot);
706
1803
  const targets = /* @__PURE__ */ new Set();
707
1804
  for (const change of plan.changes) {
@@ -780,7 +1877,8 @@ var DEFAULT_OPTIONS = Object.freeze({
780
1877
  enabled: false,
781
1878
  runtime: "dispatch",
782
1879
  limits: DEFAULT_DATA_FLOW_LIMITS
783
- })
1880
+ }),
1881
+ externalAgent: Object.freeze({ enabled: false })
784
1882
  });
785
1883
  var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
786
1884
  var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
@@ -1106,6 +2204,9 @@ function resolveOptions(options = {}, environmentAi) {
1106
2204
  if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
1107
2205
  throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
1108
2206
  }
2207
+ if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
2208
+ throw new RangeError("SpotPatch externalAgent must be a boolean.");
2209
+ }
1109
2210
  const budget = Object.freeze({
1110
2211
  ...DEFAULT_OPTIONS.budget,
1111
2212
  ...options.budget
@@ -1138,7 +2239,10 @@ function resolveOptions(options = {}, environmentAi) {
1138
2239
  locale,
1139
2240
  maxTargets,
1140
2241
  ai: resolveAiOptions(options.ai ?? environmentAi),
1141
- dataFlow: resolveDataFlowOptions(options.dataFlow)
2242
+ dataFlow: resolveDataFlowOptions(options.dataFlow),
2243
+ externalAgent: Object.freeze({
2244
+ enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
2245
+ })
1142
2246
  };
1143
2247
  if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
1144
2248
  throw new RangeError("SpotPatch shortcut is invalid.");
@@ -1148,10 +2252,13 @@ function resolveOptions(options = {}, environmentAi) {
1148
2252
 
1149
2253
  // src/project-validation.ts
1150
2254
  import { execFile } from "child_process";
1151
- import { access as access2, lstat as lstat2, readFile as readFile2, realpath as realpath2 } from "fs/promises";
2255
+ import { access as access2, lstat as lstat3, readFile as readFile2, realpath as realpath2 } from "fs/promises";
1152
2256
  import { createRequire } from "module";
1153
- import path2 from "path";
2257
+ import path3 from "path";
1154
2258
  import { promisify } from "util";
2259
+ import {
2260
+ DEFAULT_AGENT_LIMITS as DEFAULT_AGENT_LIMITS2
2261
+ } from "@spotpatch/shared";
1155
2262
  var execFileAsync = promisify(execFile);
1156
2263
  var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
1157
2264
  var TYPESCRIPT_CHECK_LABEL = "TypeScript";
@@ -1160,14 +2267,14 @@ function isRecord(value) {
1160
2267
  }
1161
2268
  async function isRegularFile(absolutePath) {
1162
2269
  try {
1163
- const metadata = await lstat2(absolutePath);
2270
+ const metadata = await lstat3(absolutePath);
1164
2271
  return metadata.isFile() && !metadata.isSymbolicLink();
1165
2272
  } catch {
1166
2273
  return false;
1167
2274
  }
1168
2275
  }
1169
2276
  async function readManifest(appRoot) {
1170
- const manifestPath = path2.join(appRoot, "package.json");
2277
+ const manifestPath = path3.join(appRoot, "package.json");
1171
2278
  if (!await isRegularFile(manifestPath)) {
1172
2279
  return void 0;
1173
2280
  }
@@ -1196,8 +2303,8 @@ async function findGitRoot(appRoot) {
1196
2303
  windowsHide: true
1197
2304
  });
1198
2305
  const root = await realpath2(result.stdout.trim());
1199
- const relative = path2.relative(root, appRoot);
1200
- if (relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative)) {
2306
+ const relative = path3.relative(root, appRoot);
2307
+ if (relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative)) {
1201
2308
  return root;
1202
2309
  }
1203
2310
  } catch {
@@ -1206,10 +2313,10 @@ async function findGitRoot(appRoot) {
1206
2313
  return void 0;
1207
2314
  }
1208
2315
  async function resolveTypeScriptCli(appRoot) {
1209
- const resolveFromApplication = createRequire(path2.join(appRoot, "package.json"));
2316
+ const resolveFromApplication = createRequire(path3.join(appRoot, "package.json"));
1210
2317
  try {
1211
2318
  const packagePath = resolveFromApplication.resolve("typescript/package.json");
1212
- const cliPath = path2.join(path2.dirname(packagePath), "bin", "tsc");
2319
+ const cliPath = path3.join(path3.dirname(packagePath), "bin", "tsc");
1213
2320
  await access2(cliPath);
1214
2321
  return await realpath2(cliPath);
1215
2322
  } catch {
@@ -1217,11 +2324,20 @@ async function resolveTypeScriptCli(appRoot) {
1217
2324
  }
1218
2325
  }
1219
2326
  function portableRelativePath(from, to) {
1220
- return path2.relative(from, to).split(path2.sep).join("/");
2327
+ return path3.relative(from, to).split(path3.sep).join("/");
2328
+ }
2329
+ function hasRequiredCheck(checks) {
2330
+ return Object.values(checks).some((check) => check.required);
2331
+ }
2332
+ function availableCheckId(checks, preferred) {
2333
+ if (checks[preferred] === void 0) return preferred;
2334
+ let suffix = 2;
2335
+ while (checks[`${preferred}-${String(suffix)}`] !== void 0) suffix += 1;
2336
+ return `${preferred}-${String(suffix)}`;
1221
2337
  }
1222
2338
  async function discoverProjectValidationCheck(options) {
1223
2339
  const appRoot = await realpath2(options.appRoot);
1224
- const tsconfigPath = path2.join(appRoot, "tsconfig.json");
2340
+ const tsconfigPath = path3.join(appRoot, "tsconfig.json");
1225
2341
  const [manifest, projectRoot, hasTsconfig] = await Promise.all([
1226
2342
  readManifest(appRoot),
1227
2343
  findGitRoot(appRoot),
@@ -1247,6 +2363,8 @@ async function discoverProjectValidationCheck(options) {
1247
2363
  "--noEmit",
1248
2364
  "--pretty",
1249
2365
  "false",
2366
+ "--incremental",
2367
+ "false",
1250
2368
  "--project",
1251
2369
  projectPath
1252
2370
  ]),
@@ -1254,21 +2372,30 @@ async function discoverProjectValidationCheck(options) {
1254
2372
  timeoutMs: options.timeoutMs
1255
2373
  });
1256
2374
  }
1257
-
1258
- // src/project-options.ts
1259
- function hasRequiredCheck(ai) {
1260
- return Object.values(ai.execution.checks).some((check) => check.required);
2375
+ async function resolveProjectValidationChecks(options) {
2376
+ if (hasRequiredCheck(options.checks)) return options.checks;
2377
+ const discovered = await discoverProjectValidationCheck(options);
2378
+ if (discovered === void 0) return options.checks;
2379
+ const id = availableCheckId(options.checks, discovered.id);
2380
+ return Object.freeze({
2381
+ ...options.checks,
2382
+ [id]: Object.freeze({ ...discovered, id })
2383
+ });
1261
2384
  }
1262
- function availableCheckId(checks, preferred) {
1263
- if (checks[preferred] === void 0) {
1264
- return preferred;
1265
- }
1266
- let suffix = 2;
1267
- while (checks[`${preferred}-${String(suffix)}`] !== void 0) {
1268
- suffix += 1;
1269
- }
1270
- return `${preferred}-${String(suffix)}`;
2385
+ async function resolveManagedExecutionValidation(options) {
2386
+ const checks = options.ai === false ? Object.freeze({}) : options.ai.execution.checks;
2387
+ const limits = options.ai === false ? DEFAULT_AGENT_LIMITS2 : options.ai.execution.limits;
2388
+ return Object.freeze({
2389
+ checks: await resolveProjectValidationChecks({
2390
+ appRoot: options.appRoot,
2391
+ checks,
2392
+ timeoutMs: limits.checkTimeoutMs
2393
+ }),
2394
+ limits
2395
+ });
1271
2396
  }
2397
+
2398
+ // src/project-options.ts
1272
2399
  async function resolveProjectOptions(input) {
1273
2400
  const userOptions = input.options ?? {};
1274
2401
  const resolved = resolveOptions(userOptions, input.environmentAi);
@@ -1280,22 +2407,15 @@ async function resolveProjectOptions(input) {
1280
2407
  "SpotPatch trustedFastMode cannot be combined with applyMode auto."
1281
2408
  );
1282
2409
  }
1283
- let checks = resolved.ai.execution.checks;
1284
- if (!hasRequiredCheck(resolved.ai)) {
1285
- const discovered = await discoverProjectValidationCheck({
1286
- appRoot: input.appRoot,
1287
- timeoutMs: resolved.ai.execution.limits.checkTimeoutMs
1288
- });
1289
- if (discovered === void 0) {
1290
- throw new RangeError(
1291
- "SpotPatch trustedFastMode requires a configured required check or a local TypeScript project with tsconfig.json."
1292
- );
1293
- }
1294
- const id = availableCheckId(checks, discovered.id);
1295
- checks = Object.freeze({
1296
- ...checks,
1297
- [id]: Object.freeze({ ...discovered, id })
1298
- });
2410
+ const checks = await resolveProjectValidationChecks({
2411
+ appRoot: input.appRoot,
2412
+ checks: resolved.ai.execution.checks,
2413
+ timeoutMs: resolved.ai.execution.limits.checkTimeoutMs
2414
+ });
2415
+ if (!Object.values(checks).some((check) => check.required)) {
2416
+ throw new RangeError(
2417
+ "SpotPatch trustedFastMode requires a configured required check or a local TypeScript project with tsconfig.json."
2418
+ );
1299
2419
  }
1300
2420
  const ai = Object.freeze({
1301
2421
  ...resolved.ai,
@@ -1309,16 +2429,16 @@ async function resolveProjectOptions(input) {
1309
2429
  }
1310
2430
 
1311
2431
  // src/registry/source-registry.ts
1312
- import path3 from "path";
2432
+ import path4 from "path";
1313
2433
 
1314
2434
  // src/registry/source-id.ts
1315
- import { randomBytes as randomBytes3 } from "crypto";
2435
+ import { randomBytes as randomBytes7 } from "crypto";
1316
2436
  var SOURCE_ID_BYTES = 8;
1317
- var createRandomSourceId = () => randomBytes3(SOURCE_ID_BYTES).toString("base64url");
2437
+ var createRandomSourceId = () => randomBytes7(SOURCE_ID_BYTES).toString("base64url");
1318
2438
 
1319
2439
  // src/registry/source-registry.ts
1320
2440
  function normalizeAbsolutePath(absolutePath) {
1321
- return path3.normalize(path3.resolve(absolutePath));
2441
+ return path4.normalize(path4.resolve(absolutePath));
1322
2442
  }
1323
2443
  function createSourceRegistry(options = {}) {
1324
2444
  const createId = options.createId ?? createRandomSourceId;
@@ -1376,39 +2496,42 @@ function createSourceRegistry(options = {}) {
1376
2496
 
1377
2497
  // src/server/middleware.ts
1378
2498
  import {
1379
- ERROR_CODES as ERROR_CODES10,
2499
+ ERROR_CODES as ERROR_CODES16,
1380
2500
  SPOTPATCH_API_BASE,
1381
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
1382
- SpotPatchError as SpotPatchError10,
2501
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS4,
2502
+ SpotPatchError as SpotPatchError16,
1383
2503
  openEditorRequestSchema,
1384
2504
  sourceContextRequestSchema
1385
2505
  } from "@spotpatch/shared";
1386
2506
 
1387
- // src/server/agent-http.ts
2507
+ // src/external-handoff/browser-http.ts
1388
2508
  import {
1389
- ERROR_CODES as ERROR_CODES6,
2509
+ ERROR_CODES as ERROR_CODES10,
2510
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
1390
2511
  SPOTPATCH_ENDPOINTS,
1391
- SpotPatchError as SpotPatchError6,
1392
- agentCapabilityRequestSchema,
1393
- agentJobActionRequestSchema,
1394
- agentJobCreateRequestSchema,
1395
- agentWorkspaceHealthRequestSchema
2512
+ SpotPatchError as SpotPatchError10,
2513
+ externalHandoffCapabilityRequestSchema,
2514
+ externalHandoffPublishRequestSchema,
2515
+ externalHandoffResolveDeliveryRequestSchema,
2516
+ externalHandoffStatusRequestSchema
1396
2517
  } from "@spotpatch/shared";
1397
2518
 
1398
- // src/server/agent-request.ts
2519
+ // src/server/annotation-authorizer.ts
1399
2520
  import { realpath as realpath5 } from "fs/promises";
1400
- import path6 from "path";
2521
+ import path7 from "path";
1401
2522
  import {
1402
- ERROR_CODES as ERROR_CODES4,
1403
- SpotPatchError as SpotPatchError4
2523
+ ERROR_CODES as ERROR_CODES9,
2524
+ SpotPatchError as SpotPatchError9,
2525
+ redactSensitiveText,
2526
+ sanitizeUrl
1404
2527
  } from "@spotpatch/shared";
1405
2528
 
1406
2529
  // src/server/source-context.ts
1407
2530
  import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
1408
- import path5 from "path";
2531
+ import path6 from "path";
1409
2532
  import {
1410
- ERROR_CODES as ERROR_CODES3,
1411
- SpotPatchError as SpotPatchError3
2533
+ ERROR_CODES as ERROR_CODES8,
2534
+ SpotPatchError as SpotPatchError8
1412
2535
  } from "@spotpatch/shared";
1413
2536
 
1414
2537
  // src/server/extract-code-context.ts
@@ -1642,15 +2765,8 @@ function extractCodeContext(options) {
1642
2765
 
1643
2766
  // src/server/source-file.ts
1644
2767
  import { realpath as realpath3, stat as stat2 } from "fs/promises";
1645
- import path4 from "path";
1646
- import { ERROR_CODES as ERROR_CODES2, SpotPatchError as SpotPatchError2 } from "@spotpatch/shared";
1647
-
1648
- // src/server/constants.ts
1649
- var MAX_REQUEST_BODY_BYTES = 32 * 1024;
1650
- var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
1651
- var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
1652
-
1653
- // src/server/source-file.ts
2768
+ import path5 from "path";
2769
+ import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
1654
2770
  var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
1655
2771
  function isMissingFileError(error) {
1656
2772
  return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
@@ -1665,51 +2781,51 @@ async function assertInsideRoot(root, candidate) {
1665
2781
  ]);
1666
2782
  } catch (error) {
1667
2783
  if (isMissingFileError(error)) {
1668
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
2784
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
1669
2785
  cause: error
1670
2786
  });
1671
2787
  }
1672
2788
  throw error;
1673
2789
  }
1674
- const relative = path4.relative(realRoot, realCandidate);
1675
- const outside = relative.startsWith(`..${path4.sep}`) || relative === ".." || path4.isAbsolute(relative);
2790
+ const relative = path5.relative(realRoot, realCandidate);
2791
+ const outside = relative.startsWith(`..${path5.sep}`) || relative === ".." || path5.isAbsolute(relative);
1676
2792
  if (outside) {
1677
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_OUTSIDE_ROOT);
2793
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
1678
2794
  }
1679
2795
  return realCandidate;
1680
2796
  }
1681
2797
  async function resolveSourceFile(options) {
1682
2798
  const registeredPath = options.registry.resolve(options.fileId);
1683
2799
  if (registeredPath === void 0) {
1684
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2800
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1685
2801
  }
1686
2802
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
1687
- if (!ALLOWED_EXTENSIONS.has(path4.extname(sourcePath).toLowerCase())) {
1688
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2803
+ if (!ALLOWED_EXTENSIONS.has(path5.extname(sourcePath).toLowerCase())) {
2804
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1689
2805
  }
1690
2806
  let sourceStat;
1691
2807
  try {
1692
2808
  sourceStat = await stat2(sourcePath);
1693
2809
  } catch (error) {
1694
2810
  if (isMissingFileError(error)) {
1695
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
2811
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
1696
2812
  cause: error
1697
2813
  });
1698
2814
  }
1699
2815
  throw error;
1700
2816
  }
1701
2817
  if (!sourceStat.isFile()) {
1702
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2818
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1703
2819
  }
1704
2820
  if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
1705
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_TOO_LARGE);
2821
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_TOO_LARGE);
1706
2822
  }
1707
2823
  return sourcePath;
1708
2824
  }
1709
2825
 
1710
2826
  // src/server/source-context.ts
1711
2827
  function toDisplayPath(root, sourcePath) {
1712
- return path5.relative(root, sourcePath).split(path5.sep).join("/");
2828
+ return path6.relative(root, sourcePath).split(path6.sep).join("/");
1713
2829
  }
1714
2830
  async function readSourceContext(options) {
1715
2831
  const sourcePath = await resolveSourceFile({
@@ -1722,7 +2838,7 @@ async function readSourceContext(options) {
1722
2838
  source = await readFile3(sourcePath, "utf8");
1723
2839
  } catch (error) {
1724
2840
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1725
- throw new SpotPatchError3(ERROR_CODES3.SOURCE_NOT_FOUND, void 0, {
2841
+ throw new SpotPatchError8(ERROR_CODES8.SOURCE_NOT_FOUND, void 0, {
1726
2842
  cause: error
1727
2843
  });
1728
2844
  }
@@ -1730,9 +2846,9 @@ async function readSourceContext(options) {
1730
2846
  }
1731
2847
  const lines = source.split(/\r?\n/);
1732
2848
  if (options.request.line > lines.length) {
1733
- throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
2849
+ throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
1734
2850
  }
1735
- const extension = path5.extname(sourcePath).toLowerCase();
2851
+ const extension = path6.extname(sourcePath).toLowerCase();
1736
2852
  return extractCodeContext({
1737
2853
  source,
1738
2854
  sourcePath,
@@ -1745,7 +2861,17 @@ async function readSourceContext(options) {
1745
2861
  });
1746
2862
  }
1747
2863
 
1748
- // src/server/agent-request.ts
2864
+ // src/server/annotation-authorizer.ts
2865
+ function sanitizePageContext(page) {
2866
+ return Object.freeze({
2867
+ url: sanitizeUrl(page.url, page.url),
2868
+ pathname: redactSensitiveText(page.pathname),
2869
+ title: redactSensitiveText(page.title),
2870
+ viewportWidth: page.viewportWidth,
2871
+ viewportHeight: page.viewportHeight,
2872
+ devicePixelRatio: page.devicePixelRatio
2873
+ });
2874
+ }
1749
2875
  function compactSourceRef(source) {
1750
2876
  return Object.freeze({
1751
2877
  origin: source.origin,
@@ -1759,27 +2885,20 @@ function compactSourceRef(source) {
1759
2885
  async function authorizeSourceRef(source, registry, root) {
1760
2886
  const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
1761
2887
  if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
1762
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2888
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1763
2889
  }
1764
2890
  if (source.fileId === void 0) {
1765
2891
  if (source.origin === "none" && source.relativePath !== void 0) {
1766
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2892
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1767
2893
  }
1768
2894
  return compactSourceRef(source);
1769
2895
  }
1770
- const sourcePath = await resolveSourceFile({
1771
- fileId: source.fileId,
1772
- registry,
1773
- root
1774
- });
1775
- const relativePath = path6.relative(await realpath5(root), sourcePath).split(path6.sep).join("/");
2896
+ const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
2897
+ const relativePath = path7.relative(await realpath5(root), sourcePath).split(path7.sep).join("/");
1776
2898
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
1777
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2899
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1778
2900
  }
1779
- return Object.freeze({
1780
- ...compactSourceRef(source),
1781
- relativePath
1782
- });
2901
+ return Object.freeze({ ...compactSourceRef(source), relativePath });
1783
2902
  }
1784
2903
  function freezeMatchedRule(rule) {
1785
2904
  return Object.freeze({
@@ -1813,7 +2932,7 @@ async function authorizeTarget(target, input) {
1813
2932
  maxLines: input.options.budget.maxCodeLines
1814
2933
  });
1815
2934
  if (marker === void 0 && target.code !== void 0) {
1816
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2935
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1817
2936
  }
1818
2937
  const code = marker === void 0 ? void 0 : await readSourceContext({
1819
2938
  request: marker,
@@ -1823,11 +2942,11 @@ async function authorizeTarget(target, input) {
1823
2942
  maxLines: input.options.budget.maxCodeLines
1824
2943
  });
1825
2944
  if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
1826
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2945
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1827
2946
  }
1828
2947
  return Object.freeze({
1829
2948
  instruction: target.instruction,
1830
- ...target.page === void 0 ? {} : { page: Object.freeze({ ...target.page }) },
2949
+ ...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
1831
2950
  source,
1832
2951
  react: Object.freeze({
1833
2952
  supported: target.react.supported,
@@ -1855,76 +2974,298 @@ async function authorizeTarget(target, input) {
1855
2974
  warnings: Object.freeze([...target.warnings])
1856
2975
  });
1857
2976
  }
1858
- async function authorizeAgentJobRequest(input) {
1859
- const requestedTargets = input.request.annotation.targets;
2977
+ async function authorizeAnnotation(input) {
2978
+ const requestedTargets = input.annotation.targets;
1860
2979
  if (requestedTargets.length > input.options.maxTargets) {
1861
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2980
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1862
2981
  }
1863
2982
  const identities = requestedTargets.map(targetIdentity);
1864
2983
  if (new Set(identities).size !== identities.length) {
1865
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2984
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1866
2985
  }
1867
2986
  const targets = Object.freeze(
1868
2987
  await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
1869
2988
  );
1870
- const annotation = Object.freeze({
2989
+ return Object.freeze({
1871
2990
  schemaVersion: 3,
1872
- id: input.request.annotation.id,
1873
- locale: input.request.annotation.locale,
1874
- page: Object.freeze({ ...input.request.annotation.page }),
2991
+ id: input.annotation.id,
2992
+ locale: input.annotation.locale,
2993
+ page: sanitizePageContext(input.annotation.page),
1875
2994
  targets,
1876
- createdAt: input.request.annotation.createdAt
1877
- });
1878
- return Object.freeze({
1879
- annotation,
1880
- ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
1881
- providerProfileId: input.request.providerProfileId,
1882
- modelProfileId: input.request.modelProfileId,
1883
- providerDataConsent: true,
1884
- ...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
1885
- workingTreeMode: input.request.workingTreeMode
2995
+ createdAt: input.annotation.createdAt
1886
2996
  });
1887
2997
  }
1888
2998
 
1889
- // src/server/request-body.ts
1890
- import { ERROR_CODES as ERROR_CODES5, SpotPatchError as SpotPatchError5 } from "@spotpatch/shared";
1891
- function isJsonContentType(value) {
1892
- return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
1893
- }
1894
- async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
1895
- if (!isJsonContentType(request.headers["content-type"])) {
1896
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
2999
+ // src/external-handoff/browser-http.ts
3000
+ function matchExternalHandoffBrowserPath(path9) {
3001
+ if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
3002
+ if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
3003
+ if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
3004
+ if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
3005
+ return "resolve-delivery";
1897
3006
  }
1898
- const declaredLength = Number(request.headers["content-length"]);
1899
- if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
1900
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
3007
+ return void 0;
3008
+ }
3009
+ function requireService(options) {
3010
+ if (!options.options.externalAgent.enabled || options.service === void 0) {
3011
+ throw new SpotPatchError10(ERROR_CODES10.EXTERNAL_HANDOFF_DISABLED);
1901
3012
  }
1902
- const chunks = [];
1903
- let byteLength = 0;
1904
- let exceededLimit = false;
1905
- for await (const rawChunk of request) {
1906
- const chunk = rawChunk;
1907
- if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
1908
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
3013
+ return options.service;
3014
+ }
3015
+ function remapAuthorizationError(error) {
3016
+ if (error instanceof SpotPatchError10) {
3017
+ if (error.code === ERROR_CODES10.SOURCE_NOT_FOUND || error.code === ERROR_CODES10.SOURCE_OUTSIDE_ROOT || error.code === ERROR_CODES10.SOURCE_TOO_LARGE) {
3018
+ throw new SpotPatchError10(ERROR_CODES10.HANDOFF_SOURCE_STALE, void 0, {
3019
+ cause: error
3020
+ });
1909
3021
  }
1910
- const buffer = Buffer.from(chunk);
1911
- byteLength += buffer.byteLength;
1912
- if (byteLength > maximumBytes) {
1913
- exceededLimit = true;
1914
- continue;
3022
+ if (error.code === ERROR_CODES10.INVALID_REQUEST) {
3023
+ throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED, void 0, {
3024
+ cause: error
3025
+ });
1915
3026
  }
1916
- chunks.push(buffer);
1917
3027
  }
1918
- if (exceededLimit || byteLength === 0) {
1919
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
3028
+ throw error;
3029
+ }
3030
+ async function handleExternalHandoffBrowserRequest(request, response, route, options, writeSuccess) {
3031
+ if (request.method !== "POST") {
3032
+ throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
1920
3033
  }
1921
- try {
1922
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1923
- } catch (error) {
1924
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST, void 0, {
1925
- cause: error
1926
- });
3034
+ const service = requireService(options);
3035
+ if (route === "capability") {
3036
+ const parsed2 = externalHandoffCapabilityRequestSchema.safeParse(
3037
+ await readJsonRequestBody(request)
3038
+ );
3039
+ if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3040
+ writeSuccess(response, 200, service.capability());
3041
+ return;
3042
+ }
3043
+ if (route === "status") {
3044
+ const parsed2 = externalHandoffStatusRequestSchema.safeParse(
3045
+ await readJsonRequestBody(request)
3046
+ );
3047
+ if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3048
+ writeSuccess(response, 200, service.status(parsed2.data.cursor));
3049
+ return;
3050
+ }
3051
+ if (route === "resolve-delivery") {
3052
+ const parsed2 = externalHandoffResolveDeliveryRequestSchema.safeParse(
3053
+ await readJsonRequestBody(request)
3054
+ );
3055
+ if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3056
+ writeSuccess(response, 200, service.resolveDelivery(parsed2.data.cursor));
3057
+ return;
1927
3058
  }
3059
+ const parsed = externalHandoffPublishRequestSchema.safeParse(
3060
+ await readJsonRequestBody(request, EXTERNAL_HANDOFF_LIMITS5.maximumPublishBodyBytes)
3061
+ );
3062
+ if (!parsed.success) {
3063
+ throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED);
3064
+ }
3065
+ const result = await service.publish(parsed.data, async (annotation) => {
3066
+ try {
3067
+ return await authorizeAnnotation({
3068
+ annotation,
3069
+ options: options.options,
3070
+ registry: options.registry,
3071
+ root: options.root
3072
+ });
3073
+ } catch (error) {
3074
+ remapAuthorizationError(error);
3075
+ }
3076
+ });
3077
+ writeSuccess(response, result.replayed ? 200 : 201, result);
3078
+ }
3079
+
3080
+ // src/external-agent/browser-http.ts
3081
+ import {
3082
+ ERROR_CODES as ERROR_CODES11,
3083
+ EXTERNAL_AGENT_CONTROL_LIMITS,
3084
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
3085
+ SpotPatchError as SpotPatchError11,
3086
+ externalAgentControlCancelRequestSchema,
3087
+ externalAgentControlConnectRequestSchema,
3088
+ externalAgentControlDisconnectRequestSchema,
3089
+ externalAgentControlStatusRequestSchema,
3090
+ externalAgentControlStatusSchema,
3091
+ externalAgentEventSchema,
3092
+ externalAgentEventsRequestSchema,
3093
+ externalAgentManagedResultSchema,
3094
+ externalAgentResultRequestSchema
3095
+ } from "@spotpatch/shared";
3096
+ function matchExternalAgentBrowserPath(path9) {
3097
+ if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlStatus) return "status";
3098
+ if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlConnect) return "connect";
3099
+ if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlDisconnect) {
3100
+ return "disconnect";
3101
+ }
3102
+ if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlCancel) return "cancel";
3103
+ if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentEvents) return "events";
3104
+ if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentResult) return "result";
3105
+ return void 0;
3106
+ }
3107
+ function writeEvent(response, event) {
3108
+ response.write(`${JSON.stringify(externalAgentEventSchema.parse(event))}
3109
+ `);
3110
+ }
3111
+ function createExternalAgentBrowserController(port) {
3112
+ const streams = /* @__PURE__ */ new Set();
3113
+ let disposed = false;
3114
+ const handleEvents = async (request, response) => {
3115
+ const parsed = externalAgentEventsRequestSchema.safeParse(
3116
+ await readJsonRequestBody(request)
3117
+ );
3118
+ if (!parsed.success) {
3119
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
3120
+ }
3121
+ if (disposed || streams.size >= EXTERNAL_AGENT_CONTROL_LIMITS.maximumEventSubscribers) {
3122
+ throw new SpotPatchError11(ERROR_CODES11.BRIDGE_BUSY);
3123
+ }
3124
+ response.statusCode = 200;
3125
+ response.setHeader("Cache-Control", "no-store");
3126
+ response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
3127
+ response.setHeader("X-Content-Type-Options", "nosniff");
3128
+ streams.add(response);
3129
+ const initial = externalAgentControlStatusSchema.parse(port.getStatus());
3130
+ writeEvent(response, { type: "status", data: initial });
3131
+ const unsubscribe = port.subscribe((status) => {
3132
+ if (!response.destroyed && status.sequence > (parsed.data.afterSequence ?? -1)) {
3133
+ writeEvent(response, { type: "status", data: status });
3134
+ }
3135
+ });
3136
+ const heartbeat = setInterval(() => {
3137
+ if (!response.destroyed) {
3138
+ const status = port.getStatus();
3139
+ writeEvent(response, {
3140
+ type: "heartbeat",
3141
+ sequence: status.sequence,
3142
+ emittedAt: (/* @__PURE__ */ new Date()).toISOString()
3143
+ });
3144
+ }
3145
+ }, EXTERNAL_AGENT_CONTROL_LIMITS.eventHeartbeatMs);
3146
+ heartbeat.unref();
3147
+ await new Promise((resolve) => {
3148
+ const close = () => {
3149
+ response.off("close", close);
3150
+ clearInterval(heartbeat);
3151
+ unsubscribe();
3152
+ streams.delete(response);
3153
+ resolve();
3154
+ };
3155
+ response.once("close", close);
3156
+ });
3157
+ };
3158
+ return Object.freeze({
3159
+ async handle(request, response, route, writeSuccess) {
3160
+ if (request.method !== "POST" || disposed) {
3161
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
3162
+ }
3163
+ if (route === "events") {
3164
+ await handleEvents(request, response);
3165
+ return;
3166
+ }
3167
+ if (route === "status") {
3168
+ const parsed2 = externalAgentControlStatusRequestSchema.safeParse(
3169
+ await readJsonRequestBody(request)
3170
+ );
3171
+ if (!parsed2.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
3172
+ writeSuccess(
3173
+ response,
3174
+ 200,
3175
+ externalAgentControlStatusSchema.parse(port.getStatus())
3176
+ );
3177
+ return;
3178
+ }
3179
+ if (route === "connect") {
3180
+ const parsed2 = externalAgentControlConnectRequestSchema.safeParse(
3181
+ await readJsonRequestBody(request)
3182
+ );
3183
+ if (!parsed2.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
3184
+ const controller = new AbortController();
3185
+ response.once("close", () => {
3186
+ if (!response.writableEnded) controller.abort("browser-disconnected");
3187
+ });
3188
+ writeSuccess(
3189
+ response,
3190
+ 200,
3191
+ externalAgentControlStatusSchema.parse(
3192
+ await port.connect(parsed2.data, controller.signal)
3193
+ )
3194
+ );
3195
+ return;
3196
+ }
3197
+ if (route === "disconnect") {
3198
+ const parsed2 = externalAgentControlDisconnectRequestSchema.safeParse(
3199
+ await readJsonRequestBody(request)
3200
+ );
3201
+ if (!parsed2.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
3202
+ writeSuccess(
3203
+ response,
3204
+ 200,
3205
+ externalAgentControlStatusSchema.parse(await port.disconnect(parsed2.data))
3206
+ );
3207
+ return;
3208
+ }
3209
+ if (route === "cancel") {
3210
+ const parsed2 = externalAgentControlCancelRequestSchema.safeParse(
3211
+ await readJsonRequestBody(request)
3212
+ );
3213
+ if (!parsed2.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
3214
+ writeSuccess(
3215
+ response,
3216
+ 200,
3217
+ externalAgentControlStatusSchema.parse(await port.cancel(parsed2.data))
3218
+ );
3219
+ return;
3220
+ }
3221
+ const parsed = externalAgentResultRequestSchema.safeParse(
3222
+ await readJsonRequestBody(request)
3223
+ );
3224
+ if (!parsed.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
3225
+ const result = port.getResult(parsed.data.revision);
3226
+ if (result === void 0) {
3227
+ throw new SpotPatchError11(ERROR_CODES11.HANDOFF_NOT_FOUND);
3228
+ }
3229
+ writeSuccess(response, 200, externalAgentManagedResultSchema.parse(result));
3230
+ },
3231
+ dispose() {
3232
+ if (disposed) return;
3233
+ disposed = true;
3234
+ for (const response of streams) response.end();
3235
+ streams.clear();
3236
+ }
3237
+ });
3238
+ }
3239
+
3240
+ // src/server/agent-http.ts
3241
+ import {
3242
+ ERROR_CODES as ERROR_CODES12,
3243
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
3244
+ SpotPatchError as SpotPatchError12,
3245
+ agentCapabilityRequestSchema,
3246
+ agentJobActionRequestSchema,
3247
+ agentJobCreateRequestSchema,
3248
+ agentWorkspaceHealthRequestSchema
3249
+ } from "@spotpatch/shared";
3250
+
3251
+ // src/server/agent-request.ts
3252
+ import "@spotpatch/shared";
3253
+ async function authorizeAgentJobRequest(input) {
3254
+ const annotation = await authorizeAnnotation({
3255
+ annotation: input.request.annotation,
3256
+ options: input.options,
3257
+ registry: input.registry,
3258
+ root: input.root
3259
+ });
3260
+ return Object.freeze({
3261
+ annotation,
3262
+ ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
3263
+ providerProfileId: input.request.providerProfileId,
3264
+ modelProfileId: input.request.modelProfileId,
3265
+ providerDataConsent: true,
3266
+ ...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
3267
+ workingTreeMode: input.request.workingTreeMode
3268
+ });
1928
3269
  }
1929
3270
 
1930
3271
  // src/server/agent-http.ts
@@ -1944,21 +3285,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1944
3285
  "reverted",
1945
3286
  "failed"
1946
3287
  ]);
1947
- function matchAgentRequestPath(path8) {
1948
- if (path8 === SPOTPATCH_ENDPOINTS.agentCapability) {
3288
+ function matchAgentRequestPath(path9) {
3289
+ if (path9 === SPOTPATCH_ENDPOINTS3.agentCapability) {
1949
3290
  return Object.freeze({ kind: "capability" });
1950
3291
  }
1951
- if (path8 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
3292
+ if (path9 === SPOTPATCH_ENDPOINTS3.agentWorkspaceHealth) {
1952
3293
  return Object.freeze({ kind: "workspace-health" });
1953
3294
  }
1954
- if (path8 === SPOTPATCH_ENDPOINTS.agentJobs) {
3295
+ if (path9 === SPOTPATCH_ENDPOINTS3.agentJobs) {
1955
3296
  return Object.freeze({ kind: "create-job" });
1956
3297
  }
1957
- const prefix = `${SPOTPATCH_ENDPOINTS.agentJobs}/`;
1958
- if (!path8.startsWith(prefix)) {
3298
+ const prefix = `${SPOTPATCH_ENDPOINTS3.agentJobs}/`;
3299
+ if (!path9.startsWith(prefix)) {
1959
3300
  return void 0;
1960
3301
  }
1961
- const segments = path8.slice(prefix.length).split("/");
3302
+ const segments = path9.slice(prefix.length).split("/");
1962
3303
  const jobId = segments[0];
1963
3304
  const action = segments[1];
1964
3305
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1972,7 +3313,7 @@ function matchAgentRequestPath(path8) {
1972
3313
  }
1973
3314
  function requireAgentManager(options) {
1974
3315
  if (options.agentManager === void 0 || options.options.ai === false) {
1975
- throw new SpotPatchError6(ERROR_CODES6.AI_DISABLED);
3316
+ throw new SpotPatchError12(ERROR_CODES12.AI_DISABLED);
1976
3317
  }
1977
3318
  return options.agentManager;
1978
3319
  }
@@ -2025,13 +3366,13 @@ function streamAgentJobEvents(response, manager, jobId) {
2025
3366
  }
2026
3367
  async function handleCapability(request, response, options, writeSuccess) {
2027
3368
  if (request.method !== "POST") {
2028
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3369
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2029
3370
  }
2030
3371
  const parsed = agentCapabilityRequestSchema.safeParse(
2031
3372
  await readJsonRequestBody(request)
2032
3373
  );
2033
3374
  if (!parsed.success) {
2034
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3375
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2035
3376
  }
2036
3377
  const controller = new AbortController();
2037
3378
  const abort = () => {
@@ -2050,13 +3391,13 @@ async function handleCapability(request, response, options, writeSuccess) {
2050
3391
  }
2051
3392
  async function handleCreateJob(request, response, options, writeSuccess) {
2052
3393
  if (request.method !== "POST") {
2053
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3394
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2054
3395
  }
2055
3396
  const parsed = agentJobCreateRequestSchema.safeParse(
2056
3397
  await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
2057
3398
  );
2058
3399
  if (!parsed.success) {
2059
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3400
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2060
3401
  }
2061
3402
  const authorizedRequest = await authorizeAgentJobRequest({
2062
3403
  request: parsed.data,
@@ -2069,13 +3410,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
2069
3410
  }
2070
3411
  async function handleWorkspaceHealth(request, response, options, writeSuccess) {
2071
3412
  if (request.method !== "POST") {
2072
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3413
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2073
3414
  }
2074
3415
  const parsed = agentWorkspaceHealthRequestSchema.safeParse(
2075
3416
  await readJsonRequestBody(request)
2076
3417
  );
2077
3418
  if (!parsed.success) {
2078
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3419
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2079
3420
  }
2080
3421
  const controller = new AbortController();
2081
3422
  const abort = () => {
@@ -2092,13 +3433,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
2092
3433
  async function handleJobAction(request, response, options, route, writeSuccess) {
2093
3434
  const manager = requireAgentManager(options);
2094
3435
  if (request.method !== "POST") {
2095
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3436
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2096
3437
  }
2097
3438
  const parsed = agentJobActionRequestSchema.safeParse(
2098
3439
  await readJsonRequestBody(request)
2099
3440
  );
2100
3441
  if (!parsed.success) {
2101
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3442
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2102
3443
  }
2103
3444
  if (route.action === "events") {
2104
3445
  streamAgentJobEvents(response, manager, route.jobId);
@@ -2213,14 +3554,14 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
2213
3554
  var launchConfiguredEditor = createEditorLauncher();
2214
3555
 
2215
3556
  // src/server/data-flow-http.ts
2216
- import { createHash as createHash2 } from "crypto";
3557
+ import { createHash as createHash3 } from "crypto";
2217
3558
  import {
2218
3559
  createStaticDataFlowAnalyzer
2219
3560
  } from "@spotpatch/analyzer";
2220
3561
  import {
2221
3562
  DATA_FLOW_SCHEMA_VERSION,
2222
- ERROR_CODES as ERROR_CODES7,
2223
- SpotPatchError as SpotPatchError7,
3563
+ ERROR_CODES as ERROR_CODES13,
3564
+ SpotPatchError as SpotPatchError13,
2224
3565
  dataFlowComponentReportRequestSchema,
2225
3566
  dataFlowPageReportRequestSchema,
2226
3567
  limitDataFlowReportCollections
@@ -2239,7 +3580,7 @@ function limitDataFlowReportToBytes(report, maximumBytes) {
2239
3580
  truncatedBy: "bytes"
2240
3581
  });
2241
3582
  if (envelopeBytes(limited) > maximumBytes) {
2242
- throw new SpotPatchError7(ERROR_CODES7.INTERNAL_ERROR);
3583
+ throw new SpotPatchError13(ERROR_CODES13.INTERNAL_ERROR);
2243
3584
  }
2244
3585
  for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
2245
3586
  const candidate = limitDataFlowReportCollections(structurallyLimited, {
@@ -2268,7 +3609,7 @@ async function analyzeTarget(request, analyzer, options) {
2268
3609
  request.componentSourceId
2269
3610
  );
2270
3611
  if (anchor?.sourceVersion !== request.sourceVersion) {
2271
- throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_SOURCE_STALE);
3612
+ throw new SpotPatchError13(ERROR_CODES13.DATA_FLOW_SOURCE_STALE);
2272
3613
  }
2273
3614
  return anchor;
2274
3615
  }
@@ -2285,7 +3626,7 @@ async function analyzeTarget(request, analyzer, options) {
2285
3626
  column: resolvedRequest.column
2286
3627
  });
2287
3628
  if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
2288
- throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_SOURCE_STALE);
3629
+ throw new SpotPatchError13(ERROR_CODES13.DATA_FLOW_SOURCE_STALE);
2289
3630
  }
2290
3631
  return limitDataFlowReportToBytes(
2291
3632
  report,
@@ -2294,7 +3635,7 @@ async function analyzeTarget(request, analyzer, options) {
2294
3635
  }
2295
3636
  function requireAnalyzer(analyzer) {
2296
3637
  if (analyzer === void 0) {
2297
- throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_DISABLED);
3638
+ throw new SpotPatchError13(ERROR_CODES13.DATA_FLOW_DISABLED);
2298
3639
  }
2299
3640
  return analyzer;
2300
3641
  }
@@ -2306,7 +3647,7 @@ async function handleComponentDataFlowReport(request, analyzer, options) {
2306
3647
  )
2307
3648
  );
2308
3649
  if (!parsed.success) {
2309
- throw new SpotPatchError7(ERROR_CODES7.INVALID_REQUEST);
3650
+ throw new SpotPatchError13(ERROR_CODES13.INVALID_REQUEST);
2310
3651
  }
2311
3652
  return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
2312
3653
  }
@@ -2318,7 +3659,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
2318
3659
  )
2319
3660
  );
2320
3661
  if (!parsed.success) {
2321
- throw new SpotPatchError7(ERROR_CODES7.INVALID_REQUEST);
3662
+ throw new SpotPatchError13(ERROR_CODES13.INVALID_REQUEST);
2322
3663
  }
2323
3664
  const activeAnalyzer = requireAnalyzer(analyzer);
2324
3665
  const componentReports = await Promise.all(
@@ -2342,7 +3683,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
2342
3683
  const analyzedVersions = new Set(
2343
3684
  componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
2344
3685
  );
2345
- const reportId = `page_${createHash2("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
3686
+ const reportId = `page_${createHash3("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
2346
3687
  const complete = componentReports.every((report2) => report2.completeness.complete);
2347
3688
  const report = Object.freeze({
2348
3689
  schemaVersion: DATA_FLOW_SCHEMA_VERSION,
@@ -2389,20 +3730,20 @@ async function handlePageDataFlowReport(request, analyzer, options) {
2389
3730
  }
2390
3731
 
2391
3732
  // src/server/request-security.ts
2392
- import { timingSafeEqual } from "crypto";
3733
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2393
3734
  import { isIP } from "net";
2394
- import { ERROR_CODES as ERROR_CODES8, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError8 } from "@spotpatch/shared";
3735
+ import { ERROR_CODES as ERROR_CODES14, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError14 } from "@spotpatch/shared";
2395
3736
  function getSingleHeader(request, name) {
2396
3737
  const value = request.headers[name.toLowerCase()];
2397
3738
  return Array.isArray(value) ? value[0] : value;
2398
3739
  }
2399
- function tokensMatch(actual, expected) {
3740
+ function tokensMatch2(actual, expected) {
2400
3741
  if (actual === void 0) {
2401
3742
  return false;
2402
3743
  }
2403
3744
  const actualBytes = Buffer.from(actual);
2404
3745
  const expectedBytes = Buffer.from(expected);
2405
- return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
3746
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
2406
3747
  }
2407
3748
  function isLoopbackHostname(hostname) {
2408
3749
  const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
@@ -2437,33 +3778,33 @@ function parseOrigin(value) {
2437
3778
  }
2438
3779
  function assertRequestAuthorized(request, options) {
2439
3780
  const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
2440
- if (!tokensMatch(actualToken, options.sessionToken)) {
2441
- throw new SpotPatchError8(ERROR_CODES8.INVALID_TOKEN);
3781
+ if (!tokensMatch2(actualToken, options.sessionToken)) {
3782
+ throw new SpotPatchError14(ERROR_CODES14.INVALID_TOKEN);
2442
3783
  }
2443
3784
  const hostHeader = getSingleHeader(request, "host");
2444
3785
  const originHeader = getSingleHeader(request, "origin");
2445
3786
  const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
2446
3787
  const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
2447
3788
  if (host === void 0 || origin === void 0) {
2448
- throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
3789
+ throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
2449
3790
  }
2450
3791
  const hostIsLoopback = isLoopbackHostname(host.hostname);
2451
3792
  const originIsLoopback = isLoopbackHostname(origin.hostname);
2452
3793
  if (!options.allowLan) {
2453
3794
  if (!hostIsLoopback || !originIsLoopback) {
2454
- throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
3795
+ throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
2455
3796
  }
2456
3797
  return;
2457
3798
  }
2458
3799
  if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
2459
- throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
3800
+ throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
2460
3801
  }
2461
3802
  }
2462
3803
 
2463
3804
  // src/server/runtime-bootstrap.ts
2464
3805
  import {
2465
- ERROR_CODES as ERROR_CODES9,
2466
- SpotPatchError as SpotPatchError9,
3806
+ ERROR_CODES as ERROR_CODES15,
3807
+ SpotPatchError as SpotPatchError15,
2467
3808
  runtimeBootstrapRequestSchema,
2468
3809
  runtimeConfigSchema
2469
3810
  } from "@spotpatch/shared";
@@ -2493,7 +3834,7 @@ function resolveRuntimeBootstrapOptions(options) {
2493
3834
  function assertRuntimeBootstrapRequest(request, expectedOrigin) {
2494
3835
  const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
2495
3836
  if (request.method !== "POST" || contentType !== "application/json") {
2496
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3837
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2497
3838
  }
2498
3839
  const host = getSingleHeader2(request, "host");
2499
3840
  let hostIsLoopback = false;
@@ -2505,7 +3846,7 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
2505
3846
  }
2506
3847
  }
2507
3848
  if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
2508
- throw new SpotPatchError9(ERROR_CODES9.ORIGIN_NOT_ALLOWED);
3849
+ throw new SpotPatchError15(ERROR_CODES15.ORIGIN_NOT_ALLOWED);
2509
3850
  }
2510
3851
  }
2511
3852
  async function readRuntimeBootstrap(request, options) {
@@ -2514,103 +3855,139 @@ async function readRuntimeBootstrap(request, options) {
2514
3855
  await readJsonRequestBody(request)
2515
3856
  );
2516
3857
  if (!parsedBody.success) {
2517
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3858
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2518
3859
  }
2519
3860
  return options.runtimeConfig;
2520
3861
  }
2521
3862
 
2522
3863
  // src/server/middleware.ts
2523
3864
  var STATUS_BY_ERROR = Object.freeze({
2524
- [ERROR_CODES10.INVALID_REQUEST]: 400,
2525
- [ERROR_CODES10.INVALID_TOKEN]: 401,
2526
- [ERROR_CODES10.ORIGIN_NOT_ALLOWED]: 403,
2527
- [ERROR_CODES10.SOURCE_NOT_FOUND]: 404,
2528
- [ERROR_CODES10.SOURCE_OUTSIDE_ROOT]: 403,
2529
- [ERROR_CODES10.SOURCE_TOO_LARGE]: 413,
2530
- [ERROR_CODES10.EDITOR_OPEN_FAILED]: 500,
2531
- [ERROR_CODES10.DATA_FLOW_DISABLED]: 404,
2532
- [ERROR_CODES10.DATA_FLOW_SOURCE_STALE]: 409,
2533
- [ERROR_CODES10.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
2534
- [ERROR_CODES10.AI_DISABLED]: 404,
2535
- [ERROR_CODES10.PROVIDER_NOT_CONFIGURED]: 503,
2536
- [ERROR_CODES10.PROVIDER_AUTH_FAILED]: 502,
2537
- [ERROR_CODES10.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
2538
- [ERROR_CODES10.MODEL_NOT_ALLOWED]: 400,
2539
- [ERROR_CODES10.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
2540
- [ERROR_CODES10.PROVIDER_RATE_LIMITED]: 429,
2541
- [ERROR_CODES10.AGENT_BUSY]: 409,
2542
- [ERROR_CODES10.AGENT_LIMIT_EXCEEDED]: 413,
2543
- [ERROR_CODES10.AGENT_CANCELLED]: 409,
2544
- [ERROR_CODES10.WORKTREE_DIRTY]: 409,
2545
- [ERROR_CODES10.WORKTREE_NOT_REPOSITORY]: 409,
2546
- [ERROR_CODES10.WORKTREE_OPERATION_IN_PROGRESS]: 409,
2547
- [ERROR_CODES10.WORKTREE_CONFLICTED]: 409,
2548
- [ERROR_CODES10.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
2549
- [ERROR_CODES10.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
2550
- [ERROR_CODES10.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
2551
- [ERROR_CODES10.TOOL_DENIED]: 403,
2552
- [ERROR_CODES10.TOOL_INPUT_INVALID]: 422,
2553
- [ERROR_CODES10.TOOL_ARGUMENTS_INVALID]: 422,
2554
- [ERROR_CODES10.TOOL_CALL_ID_CONFLICT]: 422,
2555
- [ERROR_CODES10.TOOL_PATH_DENIED]: 403,
2556
- [ERROR_CODES10.PATCH_REJECTED]: 422,
2557
- [ERROR_CODES10.VALIDATION_FAILED]: 422,
2558
- [ERROR_CODES10.APPLY_CONFLICT]: 409,
2559
- [ERROR_CODES10.INTERNAL_ERROR]: 500
3865
+ [ERROR_CODES16.INVALID_REQUEST]: 400,
3866
+ [ERROR_CODES16.INVALID_TOKEN]: 401,
3867
+ [ERROR_CODES16.ORIGIN_NOT_ALLOWED]: 403,
3868
+ [ERROR_CODES16.SOURCE_NOT_FOUND]: 404,
3869
+ [ERROR_CODES16.SOURCE_OUTSIDE_ROOT]: 403,
3870
+ [ERROR_CODES16.SOURCE_TOO_LARGE]: 413,
3871
+ [ERROR_CODES16.EDITOR_OPEN_FAILED]: 500,
3872
+ [ERROR_CODES16.DATA_FLOW_DISABLED]: 404,
3873
+ [ERROR_CODES16.DATA_FLOW_SOURCE_STALE]: 409,
3874
+ [ERROR_CODES16.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
3875
+ [ERROR_CODES16.AI_DISABLED]: 404,
3876
+ [ERROR_CODES16.PROVIDER_NOT_CONFIGURED]: 503,
3877
+ [ERROR_CODES16.PROVIDER_AUTH_FAILED]: 502,
3878
+ [ERROR_CODES16.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
3879
+ [ERROR_CODES16.MODEL_NOT_ALLOWED]: 400,
3880
+ [ERROR_CODES16.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
3881
+ [ERROR_CODES16.PROVIDER_RATE_LIMITED]: 429,
3882
+ [ERROR_CODES16.AGENT_BUSY]: 409,
3883
+ [ERROR_CODES16.AGENT_LIMIT_EXCEEDED]: 413,
3884
+ [ERROR_CODES16.AGENT_CANCELLED]: 409,
3885
+ [ERROR_CODES16.EXTERNAL_HANDOFF_DISABLED]: 404,
3886
+ [ERROR_CODES16.EXTERNAL_HANDOFF_UNAVAILABLE]: 503,
3887
+ [ERROR_CODES16.HANDOFF_VALIDATION_FAILED]: 422,
3888
+ [ERROR_CODES16.HANDOFF_SOURCE_STALE]: 409,
3889
+ [ERROR_CODES16.HANDOFF_NOT_FOUND]: 404,
3890
+ [ERROR_CODES16.HANDOFF_EXPIRED]: 410,
3891
+ [ERROR_CODES16.HANDOFF_CURSOR_INVALID]: 409,
3892
+ [ERROR_CODES16.HANDOFF_RESPONSE_TOO_LARGE]: 413,
3893
+ [ERROR_CODES16.BRIDGE_UNAUTHORIZED]: 401,
3894
+ [ERROR_CODES16.BRIDGE_PROTOCOL_MISMATCH]: 409,
3895
+ [ERROR_CODES16.BRIDGE_BUSY]: 429,
3896
+ [ERROR_CODES16.EXTERNAL_AGENT_BUSY]: 409,
3897
+ [ERROR_CODES16.ACTIVE_ADAPTER_CONFLICT]: 409,
3898
+ [ERROR_CODES16.ACTIVE_ADAPTER_LEASE_INVALID]: 409,
3899
+ [ERROR_CODES16.ACTIVE_DISPATCH_INVALID]: 409,
3900
+ [ERROR_CODES16.SESSION_NOT_FOUND]: 404,
3901
+ [ERROR_CODES16.SESSION_AMBIGUOUS]: 409,
3902
+ [ERROR_CODES16.SESSION_CLOSED]: 410,
3903
+ [ERROR_CODES16.WORKTREE_DIRTY]: 409,
3904
+ [ERROR_CODES16.WORKTREE_NOT_REPOSITORY]: 409,
3905
+ [ERROR_CODES16.WORKTREE_OPERATION_IN_PROGRESS]: 409,
3906
+ [ERROR_CODES16.WORKTREE_CONFLICTED]: 409,
3907
+ [ERROR_CODES16.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
3908
+ [ERROR_CODES16.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
3909
+ [ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
3910
+ [ERROR_CODES16.TOOL_DENIED]: 403,
3911
+ [ERROR_CODES16.TOOL_INPUT_INVALID]: 422,
3912
+ [ERROR_CODES16.TOOL_ARGUMENTS_INVALID]: 422,
3913
+ [ERROR_CODES16.TOOL_CALL_ID_CONFLICT]: 422,
3914
+ [ERROR_CODES16.TOOL_PATH_DENIED]: 403,
3915
+ [ERROR_CODES16.PATCH_REJECTED]: 422,
3916
+ [ERROR_CODES16.VALIDATION_FAILED]: 422,
3917
+ [ERROR_CODES16.APPLY_CONFLICT]: 409,
3918
+ [ERROR_CODES16.INTERNAL_ERROR]: 500
2560
3919
  });
2561
3920
  var PUBLIC_MESSAGES = Object.freeze({
2562
- [ERROR_CODES10.INVALID_REQUEST]: "The request is invalid.",
2563
- [ERROR_CODES10.INVALID_TOKEN]: "The session token is invalid.",
2564
- [ERROR_CODES10.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
2565
- [ERROR_CODES10.SOURCE_NOT_FOUND]: "The source file is unavailable.",
2566
- [ERROR_CODES10.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
2567
- [ERROR_CODES10.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
2568
- [ERROR_CODES10.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
2569
- [ERROR_CODES10.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
2570
- [ERROR_CODES10.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
2571
- [ERROR_CODES10.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
2572
- [ERROR_CODES10.AI_DISABLED]: "AI execution is not enabled.",
2573
- [ERROR_CODES10.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
2574
- [ERROR_CODES10.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
2575
- [ERROR_CODES10.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
2576
- [ERROR_CODES10.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
2577
- [ERROR_CODES10.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
2578
- [ERROR_CODES10.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
2579
- [ERROR_CODES10.AGENT_BUSY]: "Another Agent job is already running.",
2580
- [ERROR_CODES10.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
2581
- [ERROR_CODES10.AGENT_CANCELLED]: "The Agent job was cancelled.",
2582
- [ERROR_CODES10.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
2583
- [ERROR_CODES10.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
2584
- [ERROR_CODES10.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
2585
- [ERROR_CODES10.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
2586
- [ERROR_CODES10.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
2587
- [ERROR_CODES10.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
2588
- [ERROR_CODES10.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
2589
- [ERROR_CODES10.TOOL_DENIED]: "The Agent tool request was denied.",
2590
- [ERROR_CODES10.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
2591
- [ERROR_CODES10.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
2592
- [ERROR_CODES10.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
2593
- [ERROR_CODES10.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
2594
- [ERROR_CODES10.PATCH_REJECTED]: "The proposed patch was rejected.",
2595
- [ERROR_CODES10.VALIDATION_FAILED]: "The proposed change failed validation.",
2596
- [ERROR_CODES10.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
2597
- [ERROR_CODES10.INTERNAL_ERROR]: "The request could not be completed."
3921
+ [ERROR_CODES16.INVALID_REQUEST]: "The request is invalid.",
3922
+ [ERROR_CODES16.INVALID_TOKEN]: "The session token is invalid.",
3923
+ [ERROR_CODES16.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
3924
+ [ERROR_CODES16.SOURCE_NOT_FOUND]: "The source file is unavailable.",
3925
+ [ERROR_CODES16.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
3926
+ [ERROR_CODES16.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
3927
+ [ERROR_CODES16.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
3928
+ [ERROR_CODES16.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
3929
+ [ERROR_CODES16.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
3930
+ [ERROR_CODES16.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
3931
+ [ERROR_CODES16.AI_DISABLED]: "AI execution is not enabled.",
3932
+ [ERROR_CODES16.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
3933
+ [ERROR_CODES16.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
3934
+ [ERROR_CODES16.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
3935
+ [ERROR_CODES16.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
3936
+ [ERROR_CODES16.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
3937
+ [ERROR_CODES16.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
3938
+ [ERROR_CODES16.AGENT_BUSY]: "Another Agent job is already running.",
3939
+ [ERROR_CODES16.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
3940
+ [ERROR_CODES16.AGENT_CANCELLED]: "The Agent job was cancelled.",
3941
+ [ERROR_CODES16.EXTERNAL_HANDOFF_DISABLED]: "External Agent handoff is not enabled.",
3942
+ [ERROR_CODES16.EXTERNAL_HANDOFF_UNAVAILABLE]: "External Agent handoff is temporarily unavailable.",
3943
+ [ERROR_CODES16.HANDOFF_VALIDATION_FAILED]: "The handoff content is invalid.",
3944
+ [ERROR_CODES16.HANDOFF_SOURCE_STALE]: "The selected source is stale.",
3945
+ [ERROR_CODES16.HANDOFF_NOT_FOUND]: "No current handoff is available.",
3946
+ [ERROR_CODES16.HANDOFF_EXPIRED]: "The handoff has expired.",
3947
+ [ERROR_CODES16.HANDOFF_CURSOR_INVALID]: "The handoff cursor is invalid.",
3948
+ [ERROR_CODES16.HANDOFF_RESPONSE_TOO_LARGE]: "The handoff exceeds the size limit.",
3949
+ [ERROR_CODES16.BRIDGE_UNAUTHORIZED]: "The local bridge request is unauthorized.",
3950
+ [ERROR_CODES16.BRIDGE_PROTOCOL_MISMATCH]: "The local bridge protocol is incompatible.",
3951
+ [ERROR_CODES16.BRIDGE_BUSY]: "The local bridge is busy.",
3952
+ [ERROR_CODES16.EXTERNAL_AGENT_BUSY]: "The connected external Agent is busy.",
3953
+ [ERROR_CODES16.ACTIVE_ADAPTER_CONFLICT]: "Another active Agent adapter is connected.",
3954
+ [ERROR_CODES16.ACTIVE_ADAPTER_LEASE_INVALID]: "The active Agent adapter lease is invalid.",
3955
+ [ERROR_CODES16.ACTIVE_DISPATCH_INVALID]: "The active Agent dispatch transition is invalid.",
3956
+ [ERROR_CODES16.SESSION_NOT_FOUND]: "No active SpotPatch session was found.",
3957
+ [ERROR_CODES16.SESSION_AMBIGUOUS]: "More than one SpotPatch session matches.",
3958
+ [ERROR_CODES16.SESSION_CLOSED]: "The SpotPatch session has closed.",
3959
+ [ERROR_CODES16.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
3960
+ [ERROR_CODES16.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
3961
+ [ERROR_CODES16.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
3962
+ [ERROR_CODES16.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
3963
+ [ERROR_CODES16.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
3964
+ [ERROR_CODES16.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
3965
+ [ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
3966
+ [ERROR_CODES16.TOOL_DENIED]: "The Agent tool request was denied.",
3967
+ [ERROR_CODES16.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
3968
+ [ERROR_CODES16.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
3969
+ [ERROR_CODES16.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
3970
+ [ERROR_CODES16.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
3971
+ [ERROR_CODES16.PATCH_REJECTED]: "The proposed patch was rejected.",
3972
+ [ERROR_CODES16.VALIDATION_FAILED]: "The proposed change failed validation.",
3973
+ [ERROR_CODES16.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
3974
+ [ERROR_CODES16.INTERNAL_ERROR]: "The request could not be completed."
2598
3975
  });
2599
- function writeJson(response, status, payload) {
3976
+ function writeJson2(response, status, payload) {
2600
3977
  response.statusCode = status;
2601
3978
  response.setHeader("Cache-Control", "no-store");
2602
3979
  response.setHeader("Content-Type", "application/json; charset=utf-8");
2603
3980
  response.end(JSON.stringify(payload));
2604
3981
  }
2605
3982
  function asSpotPatchError(error) {
2606
- return error instanceof SpotPatchError10 ? error : new SpotPatchError10(ERROR_CODES10.INTERNAL_ERROR, void 0, { cause: error });
3983
+ return error instanceof SpotPatchError16 ? error : new SpotPatchError16(ERROR_CODES16.INTERNAL_ERROR, void 0, { cause: error });
2607
3984
  }
2608
3985
  function writeError(response, error, logger) {
2609
3986
  const normalized = asSpotPatchError(error);
2610
- if (normalized.code === ERROR_CODES10.INTERNAL_ERROR) {
3987
+ if (normalized.code === ERROR_CODES16.INTERNAL_ERROR) {
2611
3988
  logger?.warn("[spotpatch:server] Internal request failure.");
2612
3989
  }
2613
- writeJson(response, STATUS_BY_ERROR[normalized.code], {
3990
+ writeJson2(response, STATUS_BY_ERROR[normalized.code], {
2614
3991
  ok: false,
2615
3992
  error: {
2616
3993
  code: normalized.code,
@@ -2630,7 +4007,7 @@ async function handleSourceContext(request, options) {
2630
4007
  await readJsonRequestBody(request)
2631
4008
  );
2632
4009
  if (!parsed.success) {
2633
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
4010
+ throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
2634
4011
  }
2635
4012
  return readSourceContext({
2636
4013
  request: parsed.data,
@@ -2643,7 +4020,7 @@ async function handleSourceContext(request, options) {
2643
4020
  async function handleOpenEditor(request, options) {
2644
4021
  const parsed = openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
2645
4022
  if (!parsed.success) {
2646
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
4023
+ throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
2647
4024
  }
2648
4025
  const body = parsed.data;
2649
4026
  const sourcePath = await resolveSourceFile({
@@ -2660,7 +4037,7 @@ async function handleOpenEditor(request, options) {
2660
4037
  options.logger?.warn(
2661
4038
  `[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
2662
4039
  );
2663
- throw new SpotPatchError10(ERROR_CODES10.EDITOR_OPEN_FAILED, void 0, {
4040
+ throw new SpotPatchError16(ERROR_CODES16.EDITOR_OPEN_FAILED, void 0, {
2664
4041
  cause: error
2665
4042
  });
2666
4043
  }
@@ -2668,64 +4045,98 @@ async function handleOpenEditor(request, options) {
2668
4045
  function createSpotPatchMiddleware(options) {
2669
4046
  const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
2670
4047
  const dataFlowAnalyzer = createDataFlowAnalyzer(options);
2671
- return (request, response, next) => {
2672
- const path8 = requestPath(request);
2673
- const agentRoute = matchAgentRequestPath(path8);
2674
- if (path8 !== SPOTPATCH_ENDPOINTS2.sourceContext && path8 !== SPOTPATCH_ENDPOINTS2.openEditor && path8 !== SPOTPATCH_ENDPOINTS2.dataFlowComponentReport && path8 !== SPOTPATCH_ENDPOINTS2.dataFlowPageReport && agentRoute === void 0 && !path8.startsWith(`${SPOTPATCH_API_BASE}/`)) {
4048
+ const externalAgentController = options.externalAgentControl === void 0 ? void 0 : createExternalAgentBrowserController(options.externalAgentControl);
4049
+ const middleware = (request, response, next) => {
4050
+ const path9 = requestPath(request);
4051
+ const agentRoute = matchAgentRequestPath(path9);
4052
+ const externalAgentRoute = matchExternalAgentBrowserPath(path9);
4053
+ const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
4054
+ if (path9 !== SPOTPATCH_ENDPOINTS4.sourceContext && path9 !== SPOTPATCH_ENDPOINTS4.openEditor && path9 !== SPOTPATCH_ENDPOINTS4.dataFlowComponentReport && path9 !== SPOTPATCH_ENDPOINTS4.dataFlowPageReport && agentRoute === void 0 && externalAgentRoute === void 0 && externalHandoffRoute === void 0 && !path9.startsWith(`${SPOTPATCH_API_BASE}/`)) {
2675
4055
  next();
2676
4056
  return;
2677
4057
  }
2678
4058
  const handle = async () => {
2679
- if (path8 === SPOTPATCH_ENDPOINTS2.bootstrap && bootstrap !== void 0) {
4059
+ if (path9 === SPOTPATCH_ENDPOINTS4.bootstrap && bootstrap !== void 0) {
2680
4060
  const data = await readRuntimeBootstrap(
2681
4061
  request,
2682
4062
  bootstrap
2683
4063
  );
2684
- writeJson(response, 200, { ok: true, data });
4064
+ writeJson2(response, 200, { ok: true, data });
2685
4065
  return;
2686
4066
  }
2687
4067
  assertRequestAuthorized(request, {
2688
4068
  allowLan: options.options.allowLan,
2689
4069
  sessionToken: options.session.token
2690
4070
  });
2691
- if (path8 === SPOTPATCH_ENDPOINTS2.sourceContext) {
4071
+ if (path9 === SPOTPATCH_ENDPOINTS4.sourceContext) {
2692
4072
  if (request.method !== "POST") {
2693
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
4073
+ throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
2694
4074
  }
2695
4075
  const data = await handleSourceContext(request, options);
2696
- writeJson(response, 200, { ok: true, data });
4076
+ writeJson2(response, 200, { ok: true, data });
2697
4077
  return;
2698
4078
  }
2699
- if (path8 === SPOTPATCH_ENDPOINTS2.openEditor) {
4079
+ if (path9 === SPOTPATCH_ENDPOINTS4.openEditor) {
2700
4080
  if (request.method !== "POST") {
2701
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
4081
+ throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
2702
4082
  }
2703
4083
  const data = await handleOpenEditor(request, options);
2704
- writeJson(response, 200, { ok: true, data });
4084
+ writeJson2(response, 200, { ok: true, data });
2705
4085
  return;
2706
4086
  }
2707
- if (path8 === SPOTPATCH_ENDPOINTS2.dataFlowComponentReport) {
4087
+ if (path9 === SPOTPATCH_ENDPOINTS4.dataFlowComponentReport) {
2708
4088
  if (request.method !== "POST") {
2709
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
4089
+ throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
2710
4090
  }
2711
4091
  const data = await handleComponentDataFlowReport(
2712
4092
  request,
2713
4093
  dataFlowAnalyzer,
2714
4094
  options
2715
4095
  );
2716
- writeJson(response, 200, { ok: true, data });
4096
+ writeJson2(response, 200, { ok: true, data });
2717
4097
  return;
2718
4098
  }
2719
- if (path8 === SPOTPATCH_ENDPOINTS2.dataFlowPageReport) {
4099
+ if (path9 === SPOTPATCH_ENDPOINTS4.dataFlowPageReport) {
2720
4100
  if (request.method !== "POST") {
2721
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
4101
+ throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
2722
4102
  }
2723
4103
  const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
2724
- writeJson(response, 200, { ok: true, data });
4104
+ writeJson2(response, 200, { ok: true, data });
4105
+ return;
4106
+ }
4107
+ if (externalAgentRoute !== void 0) {
4108
+ if (!options.options.externalAgent.enabled || externalAgentController === void 0) {
4109
+ throw new SpotPatchError16(ERROR_CODES16.EXTERNAL_HANDOFF_DISABLED);
4110
+ }
4111
+ await externalAgentController.handle(
4112
+ request,
4113
+ response,
4114
+ externalAgentRoute,
4115
+ (target, status, data) => {
4116
+ writeJson2(target, status, { ok: true, data });
4117
+ }
4118
+ );
4119
+ return;
4120
+ }
4121
+ if (externalHandoffRoute !== void 0) {
4122
+ await handleExternalHandoffBrowserRequest(
4123
+ request,
4124
+ response,
4125
+ externalHandoffRoute,
4126
+ {
4127
+ options: options.options,
4128
+ registry: options.registry,
4129
+ root: options.root,
4130
+ ...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
4131
+ },
4132
+ (target, status, data) => {
4133
+ writeJson2(target, status, { ok: true, data });
4134
+ }
4135
+ );
2725
4136
  return;
2726
4137
  }
2727
4138
  if (agentRoute === void 0) {
2728
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
4139
+ throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
2729
4140
  }
2730
4141
  await handleAgentRequest(
2731
4142
  request,
@@ -2733,7 +4144,7 @@ function createSpotPatchMiddleware(options) {
2733
4144
  options,
2734
4145
  agentRoute,
2735
4146
  (target, status, data) => {
2736
- writeJson(target, status, { ok: true, data });
4147
+ writeJson2(target, status, { ok: true, data });
2737
4148
  }
2738
4149
  );
2739
4150
  };
@@ -2741,12 +4152,17 @@ function createSpotPatchMiddleware(options) {
2741
4152
  writeError(response, error, options.logger);
2742
4153
  });
2743
4154
  };
4155
+ return Object.assign(middleware, {
4156
+ dispose() {
4157
+ externalAgentController?.dispose();
4158
+ }
4159
+ });
2744
4160
  }
2745
4161
 
2746
4162
  // src/server/source-registration.ts
2747
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2748
- import { lstat as lstat3, realpath as realpath6 } from "fs/promises";
2749
- import path7 from "path";
4163
+ import { timingSafeEqual as timingSafeEqual3 } from "crypto";
4164
+ import { lstat as lstat4, realpath as realpath6 } from "fs/promises";
4165
+ import path8 from "path";
2750
4166
  import { createSourceFilter } from "@spotpatch/compiler";
2751
4167
  import { z as z2 } from "zod";
2752
4168
  var REGISTRATION_BODY_LIMIT_BYTES = 4096;
@@ -2767,16 +4183,16 @@ function identitiesMatch(actual, expected) {
2767
4183
  }
2768
4184
  const actualBytes = Buffer.from(actual);
2769
4185
  const expectedBytes = Buffer.from(expected);
2770
- return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
4186
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
2771
4187
  }
2772
4188
  function isWithinRoot(root, candidate) {
2773
- const relative = path7.relative(root, candidate);
2774
- return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
4189
+ const relative = path8.relative(root, candidate);
4190
+ return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
2775
4191
  }
2776
4192
  function hasForbiddenSegment(root, candidate) {
2777
- return path7.relative(root, candidate).split(path7.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
4193
+ return path8.relative(root, candidate).split(path8.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2778
4194
  }
2779
- function writeJson2(response, statusCode, payload) {
4195
+ function writeJson3(response, statusCode, payload) {
2780
4196
  const body = JSON.stringify(payload);
2781
4197
  response.statusCode = statusCode;
2782
4198
  response.setHeader("Cache-Control", "no-store");
@@ -2796,11 +4212,11 @@ function requestComesFromLoopbackWorker(request) {
2796
4212
  }
2797
4213
  }
2798
4214
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
2799
- if (!path7.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
4215
+ if (!path8.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2800
4216
  return void 0;
2801
4217
  }
2802
4218
  try {
2803
- const sourceStat = await lstat3(requestedPath);
4219
+ const sourceStat = await lstat4(requestedPath);
2804
4220
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
2805
4221
  return void 0;
2806
4222
  }
@@ -2826,14 +4242,14 @@ async function createSourceRegistrationService(input) {
2826
4242
  getSingleHeader3(request, INTERNAL_SECRET_HEADER),
2827
4243
  input.internalSecret
2828
4244
  )) {
2829
- writeJson2(response, 403, { ok: false });
4245
+ writeJson3(response, 403, { ok: false });
2830
4246
  return;
2831
4247
  }
2832
4248
  const parsed = registrationRequestSchema.safeParse(
2833
4249
  await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
2834
4250
  );
2835
4251
  if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
2836
- writeJson2(response, 400, { ok: false });
4252
+ writeJson3(response, 400, { ok: false });
2837
4253
  return;
2838
4254
  }
2839
4255
  const sourcePath = await resolveAuthorizedSource(
@@ -2842,17 +4258,17 @@ async function createSourceRegistrationService(input) {
2842
4258
  (absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
2843
4259
  );
2844
4260
  if (sourcePath === void 0) {
2845
- writeJson2(response, 403, { ok: false });
4261
+ writeJson3(response, 403, { ok: false });
2846
4262
  return;
2847
4263
  }
2848
- writeJson2(response, 200, {
4264
+ writeJson3(response, 200, {
2849
4265
  epoch: input.registryEpoch,
2850
4266
  fileId: input.registry.register(sourcePath)
2851
4267
  });
2852
4268
  };
2853
4269
  void handle().catch(() => {
2854
4270
  if (!response.headersSent) {
2855
- writeJson2(response, 400, { ok: false });
4271
+ writeJson3(response, 400, { ok: false });
2856
4272
  } else {
2857
4273
  response.destroy();
2858
4274
  }
@@ -2862,11 +4278,11 @@ async function createSourceRegistrationService(input) {
2862
4278
  }
2863
4279
 
2864
4280
  // src/session/session.ts
2865
- import { randomBytes as randomBytes4 } from "crypto";
4281
+ import { randomBytes as randomBytes8 } from "crypto";
2866
4282
  function createSession() {
2867
4283
  return Object.freeze({
2868
- id: randomBytes4(16).toString("base64url"),
2869
- token: randomBytes4(16).toString("base64url")
4284
+ id: randomBytes8(16).toString("base64url"),
4285
+ token: randomBytes8(16).toString("base64url")
2870
4286
  });
2871
4287
  }
2872
4288
 
@@ -2879,6 +4295,7 @@ var OPTION_KEYS = Object.freeze([
2879
4295
  "dataFlow",
2880
4296
  "editor",
2881
4297
  "enabled",
4298
+ "externalAgent",
2882
4299
  "exclude",
2883
4300
  "include",
2884
4301
  "locale",
@@ -2969,6 +4386,7 @@ function serializeResolvedSpotPatchOptions(options) {
2969
4386
  }) : false,
2970
4387
  editor: options.editor,
2971
4388
  enabled: options.enabled,
4389
+ externalAgent: options.externalAgent.enabled,
2972
4390
  exclude: Object.freeze(options.exclude.map(serializeFilter)),
2973
4391
  include: Object.freeze(options.include.map(serializeFilter)),
2974
4392
  locale: options.locale,
@@ -3022,7 +4440,7 @@ function parseSerializedSpotPatchOptions(value) {
3022
4440
  if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
3023
4441
  throw new TypeError("The SpotPatch options transport is invalid.");
3024
4442
  }
3025
- if (typeof value.enabled !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord2(value.ai)) {
4443
+ if (typeof value.enabled !== "boolean" || typeof value.externalAgent !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord2(value.ai)) {
3026
4444
  throw new TypeError("The SpotPatch options transport is invalid.");
3027
4445
  }
3028
4446
  try {
@@ -3034,6 +4452,7 @@ function parseSerializedSpotPatchOptions(value) {
3034
4452
  dataFlow: parseDataFlow(value.dataFlow),
3035
4453
  editor: value.editor,
3036
4454
  enabled: value.enabled,
4455
+ externalAgent: value.externalAgent,
3037
4456
  exclude: parseFilterList(value.exclude),
3038
4457
  include: parseFilterList(value.include),
3039
4458
  locale: value.locale,
@@ -3052,6 +4471,7 @@ export {
3052
4471
  DEFAULT_OPTIONS,
3053
4472
  applyIntegrationPlan,
3054
4473
  createAgentJobManager,
4474
+ createExternalHandoffService,
3055
4475
  createIntegrationFileChange,
3056
4476
  createRuntimeAiConfig,
3057
4477
  createRuntimeDataFlowConfig,
@@ -3068,8 +4488,10 @@ export {
3068
4488
  readRuntimeBootstrap,
3069
4489
  resolveCredentialEnvironment,
3070
4490
  resolveEnvironmentAiConfiguration,
4491
+ resolveManagedExecutionValidation,
3071
4492
  resolveOptions,
3072
4493
  resolveProjectOptions,
4494
+ resolveProjectValidationChecks,
3073
4495
  resolveRuntimeBootstrapOptions,
3074
4496
  serializeResolvedSpotPatchOptions
3075
4497
  };