@spotpatch/dev-server 0.4.0 → 0.6.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.cjs CHANGED
@@ -34,8 +34,10 @@ __export(index_exports, {
34
34
  DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
35
35
  applyIntegrationPlan: () => applyIntegrationPlan,
36
36
  createAgentJobManager: () => createAgentJobManager,
37
+ createExternalHandoffService: () => createExternalHandoffService,
37
38
  createIntegrationFileChange: () => createIntegrationFileChange,
38
39
  createRuntimeAiConfig: () => createRuntimeAiConfig,
40
+ createRuntimeDataFlowConfig: () => createRuntimeDataFlowConfig,
39
41
  createSession: () => createSession,
40
42
  createSourceRegistrationService: () => createSourceRegistrationService,
41
43
  createSourceRegistry: () => createSourceRegistry,
@@ -520,6 +522,1064 @@ function createAgentJobManager(options) {
520
522
  });
521
523
  }
522
524
 
525
+ // src/external-handoff/service.ts
526
+ var import_shared7 = require("@spotpatch/shared");
527
+ var import_external_agent_node3 = require("@spotpatch/shared/external-agent-node");
528
+
529
+ // src/external-handoff/active-registry.ts
530
+ var import_node_crypto2 = require("crypto");
531
+ var import_shared2 = require("@spotpatch/shared");
532
+
533
+ // src/external-handoff/clock.ts
534
+ var import_node_perf_hooks = require("perf_hooks");
535
+ var SYSTEM_EXTERNAL_HANDOFF_CLOCK = Object.freeze({
536
+ monotonicNow: () => import_node_perf_hooks.performance.now(),
537
+ wallNow: () => Date.now()
538
+ });
539
+
540
+ // src/external-handoff/active-registry.ts
541
+ var ALLOWED_TRANSITIONS = Object.freeze({
542
+ queued: ["dispatching", "failed"],
543
+ dispatching: ["dispatched", "working", "failed", "delivery-unknown"],
544
+ dispatched: ["working", "failed", "delivery-unknown"],
545
+ working: ["completed", "failed", "delivery-unknown"],
546
+ completed: [],
547
+ failed: [],
548
+ "delivery-unknown": []
549
+ });
550
+ var TERMINAL_PHASES = /* @__PURE__ */ new Set([
551
+ "completed",
552
+ "failed",
553
+ "delivery-unknown"
554
+ ]);
555
+ function defaultRandomId() {
556
+ return (0, import_node_crypto2.randomBytes)(32).toString("base64url");
557
+ }
558
+ function requirePresent(value) {
559
+ if (value === null) throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.INTERNAL_ERROR);
560
+ return value;
561
+ }
562
+ function createActiveAdapterRegistry(options = {}) {
563
+ const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
564
+ const randomId = options.randomId ?? defaultRandomId;
565
+ let blocked;
566
+ let closed = false;
567
+ let dispatch;
568
+ let lastReleasedToken;
569
+ let lease;
570
+ const nowIso = () => new Date(clock.wallNow()).toISOString();
571
+ const requireOpen = () => {
572
+ if (closed) throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.SESSION_CLOSED);
573
+ };
574
+ const dispatchSummary = () => dispatch === void 0 ? null : Object.freeze({
575
+ adapterKind: dispatch.adapterKind,
576
+ revision: dispatch.revision,
577
+ phase: dispatch.phase,
578
+ updatedAt: dispatch.updatedAt
579
+ });
580
+ const activeSummary = () => {
581
+ if (blocked !== void 0) {
582
+ return Object.freeze({
583
+ kind: blocked.adapterKind,
584
+ state: "blocked",
585
+ canDispatch: false,
586
+ connectedAt: blocked.connectedAt,
587
+ updatedAt: blocked.updatedAt
588
+ });
589
+ }
590
+ if (lease === void 0) return null;
591
+ const busy = dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase);
592
+ return Object.freeze({
593
+ kind: lease.adapterKind,
594
+ state: busy ? "busy" : "ready",
595
+ canDispatch: !busy,
596
+ connectedAt: lease.connectedAt,
597
+ updatedAt: lease.updatedAt
598
+ });
599
+ };
600
+ const state = (cursor) => Object.freeze({
601
+ activeAdapter: activeSummary(),
602
+ dispatch: cursor === void 0 || dispatch?.cursor === cursor ? dispatchSummary() : null
603
+ });
604
+ const enterUnknown = (activeLease) => {
605
+ if (dispatch === void 0) return;
606
+ const updatedAt = nowIso();
607
+ dispatch.phase = "delivery-unknown";
608
+ dispatch.updatedAt = updatedAt;
609
+ blocked = Object.freeze({
610
+ adapterKind: activeLease.adapterKind,
611
+ connectedAt: activeLease.connectedAt,
612
+ updatedAt
613
+ });
614
+ };
615
+ const endLease = (activeLease) => {
616
+ if (dispatch?.phase === "queued") {
617
+ dispatch.phase = "failed";
618
+ dispatch.updatedAt = nowIso();
619
+ } else if (dispatch?.phase === "dispatching" || dispatch?.phase === "dispatched" || dispatch?.phase === "working") {
620
+ enterUnknown(activeLease);
621
+ }
622
+ lastReleasedToken = activeLease.token;
623
+ lease = void 0;
624
+ };
625
+ const sweep = () => {
626
+ if (lease === void 0) return;
627
+ const monotonicNow = clock.monotonicNow();
628
+ if (monotonicNow >= lease.expiresAtMonotonic || dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase) && monotonicNow >= dispatch.deadlineMonotonic) {
629
+ endLease(lease);
630
+ }
631
+ };
632
+ const assertPublishable = () => {
633
+ requireOpen();
634
+ sweep();
635
+ if (blocked !== void 0 || lease !== void 0 && dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase)) {
636
+ throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.EXTERNAL_AGENT_BUSY);
637
+ }
638
+ };
639
+ const requireLease = (leaseToken) => {
640
+ requireOpen();
641
+ sweep();
642
+ if (lease?.token !== leaseToken) {
643
+ throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID);
644
+ }
645
+ return lease;
646
+ };
647
+ return Object.freeze({
648
+ assertPublishable,
649
+ claim(adapterKind, connectorInstanceId, baselineCursor) {
650
+ requireOpen();
651
+ sweep();
652
+ if (blocked !== void 0) {
653
+ throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.EXTERNAL_AGENT_BUSY);
654
+ }
655
+ if (lease !== void 0) {
656
+ if (lease.adapterKind === adapterKind && lease.connectorInstanceId === connectorInstanceId) {
657
+ lease.expiresAtMonotonic = clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
658
+ lease.updatedAt = nowIso();
659
+ return Object.freeze({
660
+ leaseToken: lease.token,
661
+ heartbeatIntervalMs: import_shared2.EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
662
+ baselineCursor: lease.baselineCursor,
663
+ activeAdapter: requirePresent(activeSummary())
664
+ });
665
+ }
666
+ throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT);
667
+ }
668
+ const timestamp = nowIso();
669
+ lease = {
670
+ adapterKind,
671
+ baselineCursor,
672
+ connectedAt: timestamp,
673
+ connectorInstanceId,
674
+ token: randomId(),
675
+ expiresAtMonotonic: clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs,
676
+ updatedAt: timestamp
677
+ };
678
+ lastReleasedToken = void 0;
679
+ return Object.freeze({
680
+ leaseToken: lease.token,
681
+ heartbeatIntervalMs: import_shared2.EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
682
+ baselineCursor,
683
+ activeAdapter: requirePresent(activeSummary())
684
+ });
685
+ },
686
+ heartbeat(leaseToken) {
687
+ const activeLease = requireLease(leaseToken);
688
+ activeLease.expiresAtMonotonic = clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
689
+ activeLease.updatedAt = nowIso();
690
+ return state();
691
+ },
692
+ report(leaseToken, cursor, phase) {
693
+ const activeLease = requireLease(leaseToken);
694
+ if (dispatch?.cursor !== cursor || dispatch.adapterKind !== activeLease.adapterKind) {
695
+ throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
696
+ }
697
+ if (dispatch.phase === phase) return state(cursor);
698
+ const allowed = ALLOWED_TRANSITIONS[dispatch.phase];
699
+ if (!allowed.includes(phase)) {
700
+ throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
701
+ }
702
+ const updatedAt = nowIso();
703
+ dispatch.phase = phase;
704
+ dispatch.updatedAt = updatedAt;
705
+ activeLease.updatedAt = updatedAt;
706
+ if (phase === "delivery-unknown") {
707
+ blocked = Object.freeze({
708
+ adapterKind: activeLease.adapterKind,
709
+ connectedAt: activeLease.connectedAt,
710
+ updatedAt
711
+ });
712
+ lastReleasedToken = activeLease.token;
713
+ lease = void 0;
714
+ }
715
+ return state(cursor);
716
+ },
717
+ release(leaseToken) {
718
+ requireOpen();
719
+ sweep();
720
+ if (lease === void 0 && lastReleasedToken === leaseToken) return state();
721
+ const activeLease = lease;
722
+ if (activeLease?.token !== leaseToken) {
723
+ throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID);
724
+ }
725
+ endLease(activeLease);
726
+ return state();
727
+ },
728
+ reserve(cursor, revision) {
729
+ assertPublishable();
730
+ if (lease === void 0) return Object.freeze({ mode: "inbox" });
731
+ const updatedAt = nowIso();
732
+ dispatch = {
733
+ adapterKind: lease.adapterKind,
734
+ cursor,
735
+ deadlineMonotonic: clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeDispatchTimeoutMs,
736
+ revision,
737
+ phase: "queued",
738
+ updatedAt
739
+ };
740
+ lease.updatedAt = updatedAt;
741
+ return Object.freeze({
742
+ mode: "active",
743
+ adapter: requirePresent(activeSummary()),
744
+ dispatch: requirePresent(dispatchSummary())
745
+ });
746
+ },
747
+ resolveDelivery(cursor) {
748
+ requireOpen();
749
+ sweep();
750
+ const activeDispatch = dispatch;
751
+ if (blocked === void 0 || activeDispatch?.cursor !== cursor || activeDispatch.phase !== "delivery-unknown") {
752
+ throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
753
+ }
754
+ blocked = void 0;
755
+ return state(cursor);
756
+ },
757
+ snapshot(cursor) {
758
+ requireOpen();
759
+ sweep();
760
+ return state(cursor);
761
+ },
762
+ close() {
763
+ if (closed) return;
764
+ closed = true;
765
+ blocked = void 0;
766
+ dispatch = void 0;
767
+ lease = void 0;
768
+ lastReleasedToken = void 0;
769
+ }
770
+ });
771
+ }
772
+
773
+ // src/external-handoff/broker.ts
774
+ var import_node_crypto3 = require("crypto");
775
+ var import_node_http = require("http");
776
+ var import_shared4 = require("@spotpatch/shared");
777
+ var import_external_agent_node = require("@spotpatch/shared/external-agent-node");
778
+
779
+ // src/server/request-body.ts
780
+ var import_shared3 = require("@spotpatch/shared");
781
+
782
+ // src/server/constants.ts
783
+ var MAX_REQUEST_BODY_BYTES = 32 * 1024;
784
+ var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
785
+ var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
786
+
787
+ // src/server/request-body.ts
788
+ function isJsonContentType(value) {
789
+ return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
790
+ }
791
+ async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
792
+ if (!isJsonContentType(request.headers["content-type"])) {
793
+ throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
794
+ }
795
+ const declaredLength = Number(request.headers["content-length"]);
796
+ if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
797
+ throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
798
+ }
799
+ const chunks = [];
800
+ let byteLength = 0;
801
+ let exceededLimit = false;
802
+ for await (const rawChunk of request) {
803
+ const chunk = rawChunk;
804
+ if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
805
+ throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
806
+ }
807
+ const buffer = Buffer.from(chunk);
808
+ byteLength += buffer.byteLength;
809
+ if (byteLength > maximumBytes) {
810
+ exceededLimit = true;
811
+ continue;
812
+ }
813
+ chunks.push(buffer);
814
+ }
815
+ if (exceededLimit || byteLength === 0) {
816
+ throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
817
+ }
818
+ try {
819
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
820
+ } catch (error) {
821
+ throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST, void 0, {
822
+ cause: error
823
+ });
824
+ }
825
+ }
826
+
827
+ // src/external-handoff/broker.ts
828
+ function singleHeader(request, name) {
829
+ const value = request.headers[name.toLowerCase()];
830
+ return Array.isArray(value) ? void 0 : value;
831
+ }
832
+ function tokensMatch(actual, expected) {
833
+ if (actual === void 0) return false;
834
+ const actualBytes = Buffer.from(actual);
835
+ const expectedBytes = Buffer.from(expected);
836
+ return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto3.timingSafeEqual)(actualBytes, expectedBytes);
837
+ }
838
+ function writeJson(response, status, payload) {
839
+ response.statusCode = status;
840
+ response.setHeader("Cache-Control", "no-store");
841
+ response.setHeader("Connection", "close");
842
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
843
+ response.setHeader("X-Content-Type-Options", "nosniff");
844
+ response.end(JSON.stringify(payload));
845
+ }
846
+ function statusForError(code) {
847
+ if (code === import_shared4.ERROR_CODES.BRIDGE_UNAUTHORIZED) return 401;
848
+ if (code === import_shared4.ERROR_CODES.HANDOFF_NOT_FOUND) return 404;
849
+ if (code === import_shared4.ERROR_CODES.HANDOFF_EXPIRED || code === import_shared4.ERROR_CODES.SESSION_CLOSED) {
850
+ return 410;
851
+ }
852
+ if (code === import_shared4.ERROR_CODES.BRIDGE_BUSY) return 429;
853
+ if (code === import_shared4.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID) return 401;
854
+ if (code === import_shared4.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE) return 413;
855
+ if (code === import_shared4.ERROR_CODES.HANDOFF_CURSOR_INVALID || code === import_shared4.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH || code === import_shared4.ERROR_CODES.EXTERNAL_AGENT_BUSY || code === import_shared4.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT || code === import_shared4.ERROR_CODES.ACTIVE_DISPATCH_INVALID) {
856
+ return 409;
857
+ }
858
+ if (code === import_shared4.ERROR_CODES.INVALID_REQUEST) return 400;
859
+ return 500;
860
+ }
861
+ function normalizeError2(error) {
862
+ return error instanceof import_shared4.SpotPatchError ? error : new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
863
+ }
864
+ function assertAuthorized(request, expectedHost, bridgeToken) {
865
+ if (request.socket.remoteAddress !== "127.0.0.1" || singleHeader(request, "host") !== expectedHost || !tokensMatch(singleHeader(request, import_external_agent_node.SPOTPATCH_BRIDGE_TOKEN_HEADER), bridgeToken)) {
866
+ throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.BRIDGE_UNAUTHORIZED);
867
+ }
868
+ }
869
+ async function closeServer(server, sockets) {
870
+ await new Promise((resolve) => {
871
+ server.close(() => {
872
+ resolve();
873
+ });
874
+ for (const socket of sockets) {
875
+ socket.destroy();
876
+ }
877
+ });
878
+ }
879
+ async function createExternalHandoffBroker(options) {
880
+ const bridgeToken = (0, import_node_crypto3.randomBytes)(32).toString("base64url");
881
+ const sockets = /* @__PURE__ */ new Set();
882
+ let expectedHost = "";
883
+ const server = (0, import_node_http.createServer)(
884
+ { maxHeaderSize: import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerHeaderBytes },
885
+ (request, response) => {
886
+ const handle = async () => {
887
+ assertAuthorized(request, expectedHost, bridgeToken);
888
+ if (request.method !== "POST") {
889
+ throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
890
+ }
891
+ const body = await readJsonRequestBody(
892
+ request,
893
+ import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerRequestBytes
894
+ );
895
+ if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.status) {
896
+ const parsed = import_external_agent_node.bridgeStatusRequestSchema.safeParse(body);
897
+ if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
898
+ let current = null;
899
+ try {
900
+ current = options.store.status();
901
+ } catch (error) {
902
+ if (!(error instanceof import_shared4.SpotPatchError) || error.code !== import_shared4.ERROR_CODES.HANDOFF_NOT_FOUND) {
903
+ throw error;
904
+ }
905
+ }
906
+ writeJson(response, 200, {
907
+ ok: true,
908
+ data: Object.freeze({
909
+ brokerProtocolVersion: import_shared4.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
910
+ projectKey: options.projectKey,
911
+ sessionId: options.sessionId,
912
+ framework: options.framework,
913
+ current
914
+ })
915
+ });
916
+ return;
917
+ }
918
+ if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.current) {
919
+ const parsed = import_external_agent_node.bridgeCurrentRequestSchema.safeParse(body);
920
+ if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
921
+ const snapshot2 = options.store.current(parsed.data.cursor);
922
+ writeJson(response, 200, {
923
+ ok: true,
924
+ data: Object.freeze({ outcome: "handoff", snapshot: snapshot2 })
925
+ });
926
+ return;
927
+ }
928
+ if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.ack) {
929
+ const parsed = import_external_agent_node.bridgeAckRequestSchema.safeParse(body);
930
+ if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
931
+ const summary = options.store.ack(
932
+ parsed.data.cursor,
933
+ parsed.data.connectorInstanceId
934
+ );
935
+ writeJson(response, 200, {
936
+ ok: true,
937
+ data: Object.freeze({ summary })
938
+ });
939
+ return;
940
+ }
941
+ if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.wait) {
942
+ const parsed = import_external_agent_node.bridgeWaitRequestSchema.safeParse(body);
943
+ if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
944
+ const controller = new AbortController();
945
+ const abort = () => {
946
+ if (!response.writableEnded) controller.abort("bridge-client-closed");
947
+ };
948
+ response.once("close", abort);
949
+ try {
950
+ const data = await options.store.wait(
951
+ parsed.data.afterCursor,
952
+ parsed.data.timeoutMs,
953
+ controller.signal
954
+ );
955
+ writeJson(response, 200, { ok: true, data });
956
+ } finally {
957
+ response.removeListener("close", abort);
958
+ }
959
+ return;
960
+ }
961
+ if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeClaim) {
962
+ const parsed = import_external_agent_node.bridgeActiveClaimRequestSchema.safeParse(body);
963
+ if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
964
+ const data = options.activeRegistry.claim(
965
+ parsed.data.adapterKind,
966
+ parsed.data.connectorInstanceId,
967
+ options.store.currentCursor()
968
+ );
969
+ writeJson(response, 200, { ok: true, data });
970
+ return;
971
+ }
972
+ if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeHeartbeat) {
973
+ const parsed = import_external_agent_node.bridgeActiveHeartbeatRequestSchema.safeParse(body);
974
+ if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
975
+ const data = options.activeRegistry.heartbeat(parsed.data.leaseToken);
976
+ writeJson(response, 200, { ok: true, data });
977
+ return;
978
+ }
979
+ if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeReport) {
980
+ const parsed = import_external_agent_node.bridgeActiveReportRequestSchema.safeParse(body);
981
+ if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
982
+ const data = options.activeRegistry.report(
983
+ parsed.data.leaseToken,
984
+ parsed.data.cursor,
985
+ parsed.data.phase
986
+ );
987
+ writeJson(response, 200, { ok: true, data });
988
+ return;
989
+ }
990
+ if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeRelease) {
991
+ const parsed = import_external_agent_node.bridgeActiveReleaseRequestSchema.safeParse(body);
992
+ if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
993
+ const data = options.activeRegistry.release(parsed.data.leaseToken);
994
+ writeJson(response, 200, { ok: true, data });
995
+ return;
996
+ }
997
+ throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
998
+ };
999
+ void handle().catch((error) => {
1000
+ if (response.writableEnded || response.destroyed) return;
1001
+ const normalized = normalizeError2(error);
1002
+ if (normalized.code === import_shared4.ERROR_CODES.BRIDGE_BUSY) {
1003
+ response.setHeader("Retry-After", "1");
1004
+ }
1005
+ writeJson(response, statusForError(normalized.code), {
1006
+ ok: false,
1007
+ error: {
1008
+ code: normalized.code,
1009
+ message: "The local SpotPatch bridge request failed."
1010
+ }
1011
+ });
1012
+ });
1013
+ }
1014
+ );
1015
+ server.maxConnections = import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerSockets;
1016
+ server.headersTimeout = 5e3;
1017
+ server.requestTimeout = import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumWaitMs + 5e3;
1018
+ server.keepAliveTimeout = 1;
1019
+ server.on("connection", (socket) => {
1020
+ if (sockets.size >= import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerSockets) {
1021
+ socket.destroy();
1022
+ return;
1023
+ }
1024
+ sockets.add(socket);
1025
+ socket.once("close", () => sockets.delete(socket));
1026
+ });
1027
+ await new Promise((resolve, reject) => {
1028
+ server.once("error", reject);
1029
+ server.listen(0, "127.0.0.1", resolve);
1030
+ });
1031
+ const address = server.address();
1032
+ if (address === null || typeof address === "string" || address.address !== "127.0.0.1") {
1033
+ await closeServer(server, sockets);
1034
+ throw new Error("SpotPatch external Agent broker did not bind IPv4 loopback.");
1035
+ }
1036
+ expectedHost = `127.0.0.1:${String(address.port)}`;
1037
+ let closed = false;
1038
+ let ready = true;
1039
+ server.removeAllListeners("error");
1040
+ server.on("error", () => {
1041
+ ready = false;
1042
+ for (const socket of sockets) socket.destroy();
1043
+ });
1044
+ return Object.freeze({
1045
+ bridgeToken,
1046
+ endpoint: `http://${expectedHost}`,
1047
+ isReady: () => ready && !closed,
1048
+ async close() {
1049
+ if (closed) return;
1050
+ closed = true;
1051
+ ready = false;
1052
+ await closeServer(server, sockets);
1053
+ }
1054
+ });
1055
+ }
1056
+
1057
+ // src/external-handoff/discovery.ts
1058
+ var import_node_crypto4 = require("crypto");
1059
+ var import_promises = require("fs/promises");
1060
+ var import_node_path = __toESM(require("path"), 1);
1061
+ var import_shared5 = require("@spotpatch/shared");
1062
+ var import_external_agent_node2 = require("@spotpatch/shared/external-agent-node");
1063
+ async function syncDirectory(directory) {
1064
+ const handle = await (0, import_promises.open)(directory, "r");
1065
+ try {
1066
+ await handle.sync();
1067
+ } catch (error) {
1068
+ const code = error.code;
1069
+ if (code !== "EINVAL" && code !== "ENOTSUP") {
1070
+ throw error;
1071
+ }
1072
+ } finally {
1073
+ await handle.close();
1074
+ }
1075
+ }
1076
+ async function publishExternalHandoffDescriptor(options) {
1077
+ const directory = await (0, import_external_agent_node2.resolveExternalHandoffRuntimeDirectory)(true);
1078
+ const descriptor = import_external_agent_node2.externalHandoffDescriptorSchema.parse({
1079
+ schemaVersion: 1,
1080
+ brokerProtocolVersion: import_shared5.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
1081
+ projectKey: await (0, import_external_agent_node2.computeExternalHandoffProjectKey)(options.root),
1082
+ sessionId: options.sessionId,
1083
+ framework: options.framework,
1084
+ endpoint: options.endpoint,
1085
+ bridgeToken: options.bridgeToken,
1086
+ pid: process.pid,
1087
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1088
+ });
1089
+ const serialized = JSON.stringify(descriptor);
1090
+ if (Buffer.byteLength(serialized, "utf8") > import_shared5.EXTERNAL_HANDOFF_LIMITS.maximumDescriptorBytes) {
1091
+ throw new RangeError("SpotPatch external Agent descriptor exceeds its limit.");
1092
+ }
1093
+ const destination = import_node_path.default.join(directory, `${descriptor.sessionId}.json`);
1094
+ const temporary = import_node_path.default.join(
1095
+ directory,
1096
+ `.${descriptor.sessionId}.${(0, import_node_crypto4.randomBytes)(8).toString("hex")}.tmp`
1097
+ );
1098
+ let temporaryExists = false;
1099
+ let published = false;
1100
+ let descriptorIdentity = Object.freeze({
1101
+ device: -1,
1102
+ inode: -1
1103
+ });
1104
+ try {
1105
+ const handle = await (0, import_promises.open)(temporary, "wx", 384);
1106
+ temporaryExists = true;
1107
+ try {
1108
+ await handle.writeFile(serialized, "utf8");
1109
+ await handle.sync();
1110
+ } finally {
1111
+ await handle.close();
1112
+ }
1113
+ await (0, import_promises.rename)(temporary, destination);
1114
+ temporaryExists = false;
1115
+ published = true;
1116
+ const status = await (0, import_promises.lstat)(destination);
1117
+ const uid = process.getuid?.();
1118
+ if (!status.isFile() || status.isSymbolicLink() || uid === void 0 || status.uid !== uid || (status.mode & 63) !== 0) {
1119
+ throw new Error("SpotPatch external Agent descriptor is not private.");
1120
+ }
1121
+ descriptorIdentity = Object.freeze({ device: status.dev, inode: status.ino });
1122
+ await syncDirectory(directory);
1123
+ } catch (error) {
1124
+ if (temporaryExists) {
1125
+ await (0, import_promises.unlink)(temporary).catch(() => void 0);
1126
+ }
1127
+ if (published) {
1128
+ await (0, import_promises.unlink)(destination).catch(() => void 0);
1129
+ }
1130
+ throw error;
1131
+ }
1132
+ let closed = false;
1133
+ return Object.freeze({
1134
+ descriptor,
1135
+ async close() {
1136
+ if (closed) return;
1137
+ closed = true;
1138
+ await (0, import_promises.lstat)(destination).then(async (status) => {
1139
+ if (status.dev === descriptorIdentity.device && status.ino === descriptorIdentity.inode) {
1140
+ await (0, import_promises.unlink)(destination);
1141
+ }
1142
+ }).catch((error) => {
1143
+ if (error.code !== "ENOENT") {
1144
+ throw error;
1145
+ }
1146
+ });
1147
+ await syncDirectory(directory);
1148
+ }
1149
+ });
1150
+ }
1151
+
1152
+ // src/external-handoff/fingerprint.ts
1153
+ var import_node_crypto5 = require("crypto");
1154
+ function canonicalJson(value) {
1155
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
1156
+ return JSON.stringify(value);
1157
+ }
1158
+ if (typeof value === "number") {
1159
+ if (!Number.isFinite(value)) throw new TypeError("Non-finite JSON number.");
1160
+ return JSON.stringify(value);
1161
+ }
1162
+ if (Array.isArray(value)) {
1163
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
1164
+ }
1165
+ if (typeof value === "object") {
1166
+ const record = value;
1167
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
1168
+ }
1169
+ throw new TypeError("Unsupported JSON value.");
1170
+ }
1171
+ function fingerprintExternalHandoffAnnotation(annotation) {
1172
+ return (0, import_node_crypto5.createHash)("sha256").update(canonicalJson(annotation)).digest("hex");
1173
+ }
1174
+
1175
+ // src/external-handoff/store.ts
1176
+ var import_node_crypto6 = require("crypto");
1177
+ var import_shared6 = require("@spotpatch/shared");
1178
+ function defaultRandomId2() {
1179
+ return (0, import_node_crypto6.randomBytes)(24).toString("base64url");
1180
+ }
1181
+ function pageSummary(annotation) {
1182
+ let origin = "[unavailable]";
1183
+ try {
1184
+ const url = new URL(annotation.page.url);
1185
+ origin = url.origin === "null" ? "[unavailable]" : url.origin;
1186
+ } catch {
1187
+ }
1188
+ return Object.freeze({ origin, pathname: annotation.page.pathname });
1189
+ }
1190
+ function summaryOf(current, state) {
1191
+ const snapshot2 = current.snapshot;
1192
+ return Object.freeze({
1193
+ sessionId: snapshot2.session.id,
1194
+ framework: snapshot2.session.framework,
1195
+ revision: snapshot2.revision,
1196
+ cursor: snapshot2.cursor,
1197
+ targetCount: snapshot2.annotation.targets.length,
1198
+ page: pageSummary(snapshot2.annotation),
1199
+ publishedAt: snapshot2.publishedAt,
1200
+ expiresAt: snapshot2.expiresAt,
1201
+ state,
1202
+ pickupCount: current.receipts.size,
1203
+ ...current.pickedUpAt === void 0 ? {} : { pickedUpAt: current.pickedUpAt }
1204
+ });
1205
+ }
1206
+ function replayResult(record) {
1207
+ return Object.freeze({ ...record.result, replayed: true });
1208
+ }
1209
+ function createExternalHandoffStore(options) {
1210
+ const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
1211
+ const randomId = options.randomId ?? defaultRandomId2;
1212
+ const history = [];
1213
+ const idempotency = /* @__PURE__ */ new Map();
1214
+ const waiters = /* @__PURE__ */ new Set();
1215
+ let closed = false;
1216
+ let current;
1217
+ let revision = 0;
1218
+ const requireOpen = () => {
1219
+ if (closed) throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED);
1220
+ };
1221
+ const archive = (state) => {
1222
+ if (current === void 0) return;
1223
+ history.unshift(summaryOf(current, state));
1224
+ history.length = Math.min(
1225
+ history.length,
1226
+ import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumHistorySummaries
1227
+ );
1228
+ current = void 0;
1229
+ };
1230
+ const sweep = () => {
1231
+ const monotonicNow = clock.monotonicNow();
1232
+ if (current !== void 0 && monotonicNow >= current.expiresAtMonotonic) {
1233
+ archive("expired");
1234
+ }
1235
+ for (const [requestId, record] of idempotency) {
1236
+ if (monotonicNow >= record.expiresAtMonotonic) idempotency.delete(requestId);
1237
+ }
1238
+ };
1239
+ const knownSummary = (cursor) => {
1240
+ sweep();
1241
+ if (current?.snapshot.cursor === cursor) {
1242
+ return summaryOf(current, "available");
1243
+ }
1244
+ return history.find((summary) => summary.cursor === cursor);
1245
+ };
1246
+ const readCurrent = (cursor) => {
1247
+ requireOpen();
1248
+ sweep();
1249
+ if (current === void 0) {
1250
+ const prior = cursor === void 0 ? void 0 : knownSummary(cursor);
1251
+ throw new import_shared6.SpotPatchError(
1252
+ prior?.state === "expired" ? import_shared6.ERROR_CODES.HANDOFF_EXPIRED : cursor === void 0 ? import_shared6.ERROR_CODES.HANDOFF_NOT_FOUND : import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID
1253
+ );
1254
+ }
1255
+ if (cursor !== void 0 && cursor !== current.snapshot.cursor) {
1256
+ const prior = knownSummary(cursor);
1257
+ throw new import_shared6.SpotPatchError(
1258
+ prior?.state === "expired" ? import_shared6.ERROR_CODES.HANDOFF_EXPIRED : import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID
1259
+ );
1260
+ }
1261
+ return current.snapshot;
1262
+ };
1263
+ const findReplay = (requestId, fingerprint) => {
1264
+ requireOpen();
1265
+ sweep();
1266
+ const record = idempotency.get(requestId);
1267
+ if (record === void 0) return void 0;
1268
+ if (record.fingerprint !== fingerprint) {
1269
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
1270
+ }
1271
+ return replayResult(record);
1272
+ };
1273
+ const settleWaiters = (result) => {
1274
+ const pending = [...waiters];
1275
+ waiters.clear();
1276
+ for (const waiter of pending) waiter.resolve(result);
1277
+ };
1278
+ return Object.freeze({
1279
+ activeWaitCount: () => waiters.size,
1280
+ replay: findReplay,
1281
+ publish(input) {
1282
+ requireOpen();
1283
+ const replayed = findReplay(input.requestId, input.fingerprint);
1284
+ if (replayed !== void 0) return replayed;
1285
+ if (idempotency.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumRequestIdRecords) {
1286
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE);
1287
+ }
1288
+ const nextRevision = revision + 1;
1289
+ const publishedAtMs = clock.wallNow();
1290
+ const publishedAtMonotonic = clock.monotonicNow();
1291
+ const snapshot2 = Object.freeze({
1292
+ schemaVersion: import_shared6.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
1293
+ cursor: randomId(),
1294
+ session: Object.freeze({ id: options.sessionId, framework: options.framework }),
1295
+ revision: nextRevision,
1296
+ publishedAt: new Date(publishedAtMs).toISOString(),
1297
+ expiresAt: new Date(
1298
+ publishedAtMs + import_shared6.EXTERNAL_HANDOFF_LIMITS.handoffTtlMs
1299
+ ).toISOString(),
1300
+ annotation: input.annotation
1301
+ });
1302
+ if (Buffer.byteLength(JSON.stringify(snapshot2), "utf8") > import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumSnapshotBytes) {
1303
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE);
1304
+ }
1305
+ const delivery = input.reserve(snapshot2.cursor, nextRevision);
1306
+ if (current !== void 0) archive("superseded");
1307
+ revision = nextRevision;
1308
+ current = {
1309
+ expiresAtMonotonic: publishedAtMonotonic + import_shared6.EXTERNAL_HANDOFF_LIMITS.handoffTtlMs,
1310
+ receipts: /* @__PURE__ */ new Set(),
1311
+ snapshot: snapshot2
1312
+ };
1313
+ const result = Object.freeze({
1314
+ handoff: summaryOf(current, "available"),
1315
+ delivery,
1316
+ replayed: false
1317
+ });
1318
+ idempotency.set(
1319
+ input.requestId,
1320
+ Object.freeze({
1321
+ expiresAtMonotonic: publishedAtMonotonic + import_shared6.EXTERNAL_HANDOFF_LIMITS.requestIdTtlMs,
1322
+ fingerprint: input.fingerprint,
1323
+ result
1324
+ })
1325
+ );
1326
+ settleWaiters(Object.freeze({ outcome: "handoff", snapshot: snapshot2 }));
1327
+ return result;
1328
+ },
1329
+ current: readCurrent,
1330
+ currentCursor() {
1331
+ requireOpen();
1332
+ sweep();
1333
+ return current?.snapshot.cursor ?? null;
1334
+ },
1335
+ status(cursor) {
1336
+ requireOpen();
1337
+ sweep();
1338
+ if (cursor === void 0) {
1339
+ if (current === void 0) {
1340
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_NOT_FOUND);
1341
+ }
1342
+ return summaryOf(current, "available");
1343
+ }
1344
+ const summary = knownSummary(cursor);
1345
+ if (summary === void 0) {
1346
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
1347
+ }
1348
+ return summary;
1349
+ },
1350
+ ack(cursor, connectorInstanceId) {
1351
+ const snapshot2 = readCurrent(cursor);
1352
+ if (snapshot2 !== current?.snapshot) {
1353
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
1354
+ }
1355
+ if (!current.receipts.has(connectorInstanceId)) {
1356
+ if (current.receipts.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumConnectorReceipts) {
1357
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.BRIDGE_BUSY);
1358
+ }
1359
+ current.receipts.add(connectorInstanceId);
1360
+ current.pickedUpAt = new Date(clock.wallNow()).toISOString();
1361
+ }
1362
+ return summaryOf(current, "available");
1363
+ },
1364
+ async wait(afterCursor, timeoutMs, signal) {
1365
+ requireOpen();
1366
+ sweep();
1367
+ if (afterCursor === void 0 && current !== void 0) {
1368
+ return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
1369
+ }
1370
+ if (afterCursor !== void 0) {
1371
+ const known = knownSummary(afterCursor);
1372
+ if (known === void 0) {
1373
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
1374
+ }
1375
+ if (current !== void 0 && current.snapshot.cursor !== afterCursor) {
1376
+ return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
1377
+ }
1378
+ }
1379
+ if (waiters.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumWaiters) {
1380
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.BRIDGE_BUSY);
1381
+ }
1382
+ if (signal.aborted) throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED);
1383
+ return new Promise((resolve, reject) => {
1384
+ let settled = false;
1385
+ const finish = () => {
1386
+ if (settled) return false;
1387
+ settled = true;
1388
+ waiters.delete(waiter);
1389
+ clearTimeout(timer);
1390
+ signal.removeEventListener("abort", abort);
1391
+ return true;
1392
+ };
1393
+ const waiter = {
1394
+ reject(error) {
1395
+ if (finish()) reject(error);
1396
+ },
1397
+ resolve(result) {
1398
+ if (finish()) resolve(result);
1399
+ }
1400
+ };
1401
+ const abort = () => {
1402
+ waiter.reject(new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED));
1403
+ };
1404
+ const timer = setTimeout(() => {
1405
+ waiter.resolve(Object.freeze({ outcome: "timeout" }));
1406
+ }, timeoutMs);
1407
+ timer.unref();
1408
+ signal.addEventListener("abort", abort, { once: true });
1409
+ waiters.add(waiter);
1410
+ });
1411
+ },
1412
+ close() {
1413
+ if (closed) return;
1414
+ closed = true;
1415
+ current = void 0;
1416
+ history.length = 0;
1417
+ idempotency.clear();
1418
+ const pending = [...waiters];
1419
+ waiters.clear();
1420
+ for (const waiter of pending) {
1421
+ waiter.reject(new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED));
1422
+ }
1423
+ }
1424
+ });
1425
+ }
1426
+
1427
+ // src/external-handoff/service.ts
1428
+ function asReplay(result) {
1429
+ return Object.freeze({ ...result, replayed: true });
1430
+ }
1431
+ function createExternalHandoffService(options) {
1432
+ const activeRegistry = createActiveAdapterRegistry();
1433
+ const store = createExternalHandoffStore({
1434
+ framework: options.framework,
1435
+ sessionId: options.sessionId
1436
+ });
1437
+ const inFlight = /* @__PURE__ */ new Map();
1438
+ let broker;
1439
+ let descriptor;
1440
+ let startPromise;
1441
+ let closePromise;
1442
+ let state = "idle";
1443
+ const isClosed = () => state === "closed";
1444
+ const requireReady = () => {
1445
+ if (state !== "ready" || broker?.isReady() !== true) {
1446
+ throw new import_shared7.SpotPatchError(
1447
+ state === "closed" ? import_shared7.ERROR_CODES.SESSION_CLOSED : import_shared7.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE
1448
+ );
1449
+ }
1450
+ };
1451
+ const start = async () => {
1452
+ if (state === "ready") return;
1453
+ if (state === "closed") throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.SESSION_CLOSED);
1454
+ if (startPromise !== void 0) return startPromise;
1455
+ state = "starting";
1456
+ startPromise = (async () => {
1457
+ let createdBroker;
1458
+ let createdDescriptor;
1459
+ try {
1460
+ const projectKey = await (0, import_external_agent_node3.computeExternalHandoffProjectKey)(options.root);
1461
+ createdBroker = await createExternalHandoffBroker({
1462
+ activeRegistry,
1463
+ framework: options.framework,
1464
+ projectKey,
1465
+ sessionId: options.sessionId,
1466
+ store
1467
+ });
1468
+ createdDescriptor = await publishExternalHandoffDescriptor({
1469
+ bridgeToken: createdBroker.bridgeToken,
1470
+ endpoint: createdBroker.endpoint,
1471
+ framework: options.framework,
1472
+ root: options.root,
1473
+ sessionId: options.sessionId
1474
+ });
1475
+ if (isClosed()) {
1476
+ await createdDescriptor.close();
1477
+ await createdBroker.close();
1478
+ throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.SESSION_CLOSED);
1479
+ }
1480
+ broker = createdBroker;
1481
+ descriptor = createdDescriptor;
1482
+ state = "ready";
1483
+ } catch (error) {
1484
+ if (createdDescriptor !== void 0 && descriptor !== createdDescriptor) {
1485
+ await createdDescriptor.close().catch(() => void 0);
1486
+ }
1487
+ if (createdBroker !== void 0 && broker !== createdBroker) {
1488
+ await createdBroker.close().catch(() => void 0);
1489
+ }
1490
+ if (!isClosed()) state = "failed";
1491
+ throw error;
1492
+ }
1493
+ })();
1494
+ return startPromise;
1495
+ };
1496
+ return Object.freeze({
1497
+ start,
1498
+ capability() {
1499
+ const currentCursor = store.currentCursor();
1500
+ const active = activeRegistry.snapshot(currentCursor ?? void 0);
1501
+ return Object.freeze({
1502
+ enabled: true,
1503
+ brokerReady: state === "ready" && broker?.isReady() === true,
1504
+ activeWaitCount: store.activeWaitCount(),
1505
+ snapshotSchemaVersion: import_shared7.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
1506
+ brokerProtocolVersion: import_shared7.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
1507
+ activeAdapter: active.activeAdapter,
1508
+ dispatch: currentCursor === null ? null : active.dispatch
1509
+ });
1510
+ },
1511
+ async publish(request, authorize) {
1512
+ requireReady();
1513
+ const fingerprint = fingerprintExternalHandoffAnnotation(request.annotation);
1514
+ const replayed = store.replay(request.requestId, fingerprint);
1515
+ if (replayed !== void 0) return replayed;
1516
+ const pending = inFlight.get(request.requestId);
1517
+ if (pending !== void 0) {
1518
+ if (pending.fingerprint !== fingerprint) {
1519
+ throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
1520
+ }
1521
+ return asReplay(await pending.promise);
1522
+ }
1523
+ activeRegistry.assertPublishable();
1524
+ const promise = (async () => {
1525
+ const annotation = await authorize(request.annotation);
1526
+ return store.publish({
1527
+ annotation,
1528
+ fingerprint,
1529
+ requestId: request.requestId,
1530
+ reserve: activeRegistry.reserve
1531
+ });
1532
+ })();
1533
+ const activePublish = Object.freeze({ fingerprint, promise });
1534
+ inFlight.set(request.requestId, activePublish);
1535
+ try {
1536
+ return await promise;
1537
+ } finally {
1538
+ if (inFlight.get(request.requestId) === activePublish) {
1539
+ inFlight.delete(request.requestId);
1540
+ }
1541
+ }
1542
+ },
1543
+ status(cursor) {
1544
+ requireReady();
1545
+ const handoff = store.status(cursor);
1546
+ const active = activeRegistry.snapshot(cursor ?? handoff.cursor);
1547
+ return Object.freeze({
1548
+ handoff,
1549
+ activeAdapter: active.activeAdapter,
1550
+ dispatch: active.dispatch
1551
+ });
1552
+ },
1553
+ resolveDelivery(cursor) {
1554
+ requireReady();
1555
+ const handoff = store.status(cursor);
1556
+ const active = activeRegistry.resolveDelivery(cursor);
1557
+ return Object.freeze({
1558
+ handoff,
1559
+ activeAdapter: active.activeAdapter,
1560
+ dispatch: active.dispatch
1561
+ });
1562
+ },
1563
+ close() {
1564
+ closePromise ??= (async () => {
1565
+ if (state === "closed") return;
1566
+ state = "closed";
1567
+ activeRegistry.close();
1568
+ store.close();
1569
+ inFlight.clear();
1570
+ await startPromise?.catch(() => void 0);
1571
+ const publishedDescriptor = descriptor;
1572
+ descriptor = void 0;
1573
+ const activeBroker = broker;
1574
+ broker = void 0;
1575
+ await publishedDescriptor?.close().catch(() => void 0);
1576
+ await activeBroker?.close().catch(() => void 0);
1577
+ })();
1578
+ return closePromise;
1579
+ }
1580
+ });
1581
+ }
1582
+
523
1583
  // src/environment-ai.ts
524
1584
  var AI_ENVIRONMENT_NAMES = Object.freeze({
525
1585
  authentication: "SPOTPATCH_AI_AUTHENTICATION",
@@ -610,46 +1670,46 @@ function resolveEnvironmentAiConfiguration(environment) {
610
1670
  }
611
1671
 
612
1672
  // src/integration/file-plan.ts
613
- var import_node_crypto2 = require("crypto");
614
- var import_promises = require("fs/promises");
615
- var import_node_path = __toESM(require("path"), 1);
1673
+ var import_node_crypto7 = require("crypto");
1674
+ var import_promises2 = require("fs/promises");
1675
+ var import_node_path2 = __toESM(require("path"), 1);
616
1676
  function isMissingPathError(error) {
617
1677
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
618
1678
  }
619
1679
  function isPathWithin(root, target) {
620
- const relative = import_node_path.default.relative(root, target);
621
- return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
1680
+ const relative = import_node_path2.default.relative(root, target);
1681
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path2.default.sep}`) && !import_node_path2.default.isAbsolute(relative);
622
1682
  }
623
1683
  function relativePathWithin(root, target) {
624
- const relative = import_node_path.default.relative(root, target);
1684
+ const relative = import_node_path2.default.relative(root, target);
625
1685
  if (relative.length === 0 || !isPathWithin(root, target)) {
626
1686
  throw new Error("SpotPatch init refuses to modify a path outside the app root.");
627
1687
  }
628
- return relative.split(import_node_path.default.sep).join("/");
1688
+ return relative.split(import_node_path2.default.sep).join("/");
629
1689
  }
630
1690
  async function integrationPathExists(absolutePath) {
631
1691
  try {
632
- await (0, import_promises.access)(absolutePath);
1692
+ await (0, import_promises2.access)(absolutePath);
633
1693
  return true;
634
1694
  } catch {
635
1695
  return false;
636
1696
  }
637
1697
  }
638
1698
  async function readIntegrationFile(absolutePath) {
639
- const metadata = await (0, import_promises.lstat)(absolutePath);
1699
+ const metadata = await (0, import_promises2.lstat)(absolutePath);
640
1700
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
641
1701
  throw new Error(
642
- `SpotPatch refuses to modify the non-regular file ${import_node_path.default.basename(absolutePath)}.`
1702
+ `SpotPatch refuses to modify the non-regular file ${import_node_path2.default.basename(absolutePath)}.`
643
1703
  );
644
1704
  }
645
- return (0, import_promises.readFile)(absolutePath, "utf8");
1705
+ return (0, import_promises2.readFile)(absolutePath, "utf8");
646
1706
  }
647
1707
  function createIntegrationFileChange(appRoot, absolutePath, nextContent, previousContent) {
648
1708
  if (previousContent === nextContent) {
649
1709
  return void 0;
650
1710
  }
651
- const root = import_node_path.default.resolve(appRoot);
652
- const target = import_node_path.default.resolve(absolutePath);
1711
+ const root = import_node_path2.default.resolve(appRoot);
1712
+ const target = import_node_path2.default.resolve(absolutePath);
653
1713
  return Object.freeze({
654
1714
  absolutePath: target,
655
1715
  nextContent,
@@ -658,19 +1718,19 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
658
1718
  });
659
1719
  }
660
1720
  function temporaryPath(absolutePath, label) {
661
- return import_node_path.default.join(
662
- import_node_path.default.dirname(absolutePath),
663
- `.${import_node_path.default.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${(0, import_node_crypto2.randomBytes)(8).toString("hex")}`
1721
+ return import_node_path2.default.join(
1722
+ import_node_path2.default.dirname(absolutePath),
1723
+ `.${import_node_path2.default.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${(0, import_node_crypto7.randomBytes)(8).toString("hex")}`
664
1724
  );
665
1725
  }
666
1726
  async function writeAtomic(absolutePath, content, mode) {
667
- await (0, import_promises.mkdir)(import_node_path.default.dirname(absolutePath), { recursive: true });
1727
+ await (0, import_promises2.mkdir)(import_node_path2.default.dirname(absolutePath), { recursive: true });
668
1728
  const stagedPath = temporaryPath(absolutePath, "stage");
669
1729
  try {
670
- await (0, import_promises.writeFile)(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
671
- await (0, import_promises.rename)(stagedPath, absolutePath);
1730
+ await (0, import_promises2.writeFile)(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
1731
+ await (0, import_promises2.rename)(stagedPath, absolutePath);
672
1732
  } catch (error) {
673
- await (0, import_promises.unlink)(stagedPath).catch(() => void 0);
1733
+ await (0, import_promises2.unlink)(stagedPath).catch(() => void 0);
674
1734
  throw error;
675
1735
  }
676
1736
  }
@@ -682,21 +1742,21 @@ async function rollbackChange(change) {
682
1742
  );
683
1743
  }
684
1744
  if (change.previousContent === void 0) {
685
- await (0, import_promises.unlink)(change.absolutePath);
1745
+ await (0, import_promises2.unlink)(change.absolutePath);
686
1746
  return;
687
1747
  }
688
- const mode = (await (0, import_promises.stat)(change.absolutePath)).mode & 511;
1748
+ const mode = (await (0, import_promises2.stat)(change.absolutePath)).mode & 511;
689
1749
  await writeAtomic(change.absolutePath, change.previousContent, mode);
690
1750
  }
691
1751
  async function assertSafeTarget(appRoot, realAppRoot, change) {
692
- const target = import_node_path.default.resolve(change.absolutePath);
1752
+ const target = import_node_path2.default.resolve(change.absolutePath);
693
1753
  const relativePath = relativePathWithin(appRoot, target);
694
- if (target !== change.absolutePath || relativePath !== change.relativePath || import_node_path.default.dirname(target) === target) {
1754
+ if (target !== change.absolutePath || relativePath !== change.relativePath || import_node_path2.default.dirname(target) === target) {
695
1755
  throw new Error("SpotPatch init received an invalid integration file plan.");
696
1756
  }
697
1757
  let targetMetadata;
698
1758
  try {
699
- targetMetadata = await (0, import_promises.lstat)(target);
1759
+ targetMetadata = await (0, import_promises2.lstat)(target);
700
1760
  } catch (error) {
701
1761
  if (!isMissingPathError(error)) {
702
1762
  throw error;
@@ -707,8 +1767,8 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
707
1767
  `SpotPatch refuses to modify the symbolic link ${change.relativePath}.`
708
1768
  );
709
1769
  }
710
- const containmentAnchor = await (0, import_promises.realpath)(
711
- targetMetadata === void 0 ? import_node_path.default.dirname(target) : target
1770
+ const containmentAnchor = await (0, import_promises2.realpath)(
1771
+ targetMetadata === void 0 ? import_node_path2.default.dirname(target) : target
712
1772
  );
713
1773
  if (!isPathWithin(realAppRoot, containmentAnchor)) {
714
1774
  throw new Error("SpotPatch init refuses to modify a path outside the app root.");
@@ -717,7 +1777,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
717
1777
  async function assertCurrentBaseline(change) {
718
1778
  if (change.previousContent === void 0) {
719
1779
  try {
720
- await (0, import_promises.lstat)(change.absolutePath);
1780
+ await (0, import_promises2.lstat)(change.absolutePath);
721
1781
  } catch (error) {
722
1782
  if (isMissingPathError(error)) {
723
1783
  return;
@@ -739,8 +1799,8 @@ async function applyIntegrationPlan(plan) {
739
1799
  if (plan.changes.length === 0) {
740
1800
  return;
741
1801
  }
742
- const appRoot = import_node_path.default.resolve(plan.appRoot);
743
- const realAppRoot = await (0, import_promises.realpath)(appRoot);
1802
+ const appRoot = import_node_path2.default.resolve(plan.appRoot);
1803
+ const realAppRoot = await (0, import_promises2.realpath)(appRoot);
744
1804
  const targets = /* @__PURE__ */ new Set();
745
1805
  for (const change of plan.changes) {
746
1806
  if (targets.has(change.absolutePath)) {
@@ -754,7 +1814,7 @@ async function applyIntegrationPlan(plan) {
754
1814
  try {
755
1815
  for (const change of plan.changes) {
756
1816
  await assertCurrentBaseline(change);
757
- const mode = change.previousContent === void 0 ? 384 : (await (0, import_promises.stat)(change.absolutePath)).mode & 511;
1817
+ const mode = change.previousContent === void 0 ? 384 : (await (0, import_promises2.stat)(change.absolutePath)).mode & 511;
758
1818
  await writeAtomic(change.absolutePath, change.nextContent, mode);
759
1819
  applied.push(change);
760
1820
  }
@@ -775,7 +1835,7 @@ async function applyIntegrationPlan(plan) {
775
1835
  }
776
1836
 
777
1837
  // src/options.ts
778
- var import_shared2 = require("@spotpatch/shared");
1838
+ var import_shared8 = require("@spotpatch/shared");
779
1839
  var import_zod = require("zod");
780
1840
  var DEFAULT_EXCLUDE = Object.freeze([
781
1841
  /node_modules/,
@@ -785,7 +1845,7 @@ var DEFAULT_EXCLUDE = Object.freeze([
785
1845
  /(?:^|\/)dist(?:\/|$)/,
786
1846
  /(?:^|\/)coverage(?:\/|$)/
787
1847
  ]);
788
- var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:jsx|tsx)$/]);
1848
+ var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:js|jsx|ts|tsx)$/]);
789
1849
  var DEFAULT_BUDGET = Object.freeze({
790
1850
  totalCharacters: 16e3,
791
1851
  domCharacters: 3e3,
@@ -806,7 +1866,13 @@ var DEFAULT_OPTIONS = Object.freeze({
806
1866
  debug: false,
807
1867
  locale: "auto",
808
1868
  maxTargets: 8,
809
- ai: false
1869
+ ai: false,
1870
+ dataFlow: Object.freeze({
1871
+ enabled: false,
1872
+ runtime: "dispatch",
1873
+ limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
1874
+ }),
1875
+ externalAgent: Object.freeze({ enabled: false })
810
1876
  });
811
1877
  var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
812
1878
  var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
@@ -851,7 +1917,7 @@ var aiOptionsSchema = import_zod.z.strictObject({
851
1917
  defaultProvider: import_zod.z.string(),
852
1918
  execution: import_zod.z.strictObject({
853
1919
  isolation: import_zod.z.literal("git-worktree").optional(),
854
- applyMode: import_zod.z.enum(import_shared2.AGENT_APPLY_MODES).optional(),
1920
+ applyMode: import_zod.z.enum(import_shared8.AGENT_APPLY_MODES).optional(),
855
1921
  checks: import_zod.z.record(import_zod.z.string(), agentCheckSchema).optional(),
856
1922
  limits: agentLimitsSchema
857
1923
  }).optional()
@@ -897,18 +1963,18 @@ function normalizeProviderBaseURL(value) {
897
1963
  }
898
1964
  function resolveLimits(limits) {
899
1965
  const resolved = Object.freeze({
900
- maxTurns: limits?.maxTurns ?? import_shared2.DEFAULT_AGENT_LIMITS.maxTurns,
901
- maxToolCalls: limits?.maxToolCalls ?? import_shared2.DEFAULT_AGENT_LIMITS.maxToolCalls,
902
- maxChangedFiles: limits?.maxChangedFiles ?? import_shared2.DEFAULT_AGENT_LIMITS.maxChangedFiles,
903
- maxDiffBytes: limits?.maxDiffBytes ?? import_shared2.DEFAULT_AGENT_LIMITS.maxDiffBytes,
904
- maxReadBytesPerFile: limits?.maxReadBytesPerFile ?? import_shared2.DEFAULT_AGENT_LIMITS.maxReadBytesPerFile,
905
- maxToolOutputCharacters: limits?.maxToolOutputCharacters ?? import_shared2.DEFAULT_AGENT_LIMITS.maxToolOutputCharacters,
906
- maxProviderResponseBytes: limits?.maxProviderResponseBytes ?? import_shared2.DEFAULT_AGENT_LIMITS.maxProviderResponseBytes,
907
- providerConnectTimeoutMs: limits?.providerConnectTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.providerConnectTimeoutMs,
908
- providerFirstByteTimeoutMs: limits?.providerFirstByteTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.providerFirstByteTimeoutMs,
909
- providerIdleTimeoutMs: limits?.providerIdleTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.providerIdleTimeoutMs,
910
- checkTimeoutMs: limits?.checkTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.checkTimeoutMs,
911
- jobTimeoutMs: limits?.jobTimeoutMs ?? import_shared2.DEFAULT_AGENT_LIMITS.jobTimeoutMs
1966
+ maxTurns: limits?.maxTurns ?? import_shared8.DEFAULT_AGENT_LIMITS.maxTurns,
1967
+ maxToolCalls: limits?.maxToolCalls ?? import_shared8.DEFAULT_AGENT_LIMITS.maxToolCalls,
1968
+ maxChangedFiles: limits?.maxChangedFiles ?? import_shared8.DEFAULT_AGENT_LIMITS.maxChangedFiles,
1969
+ maxDiffBytes: limits?.maxDiffBytes ?? import_shared8.DEFAULT_AGENT_LIMITS.maxDiffBytes,
1970
+ maxReadBytesPerFile: limits?.maxReadBytesPerFile ?? import_shared8.DEFAULT_AGENT_LIMITS.maxReadBytesPerFile,
1971
+ maxToolOutputCharacters: limits?.maxToolOutputCharacters ?? import_shared8.DEFAULT_AGENT_LIMITS.maxToolOutputCharacters,
1972
+ maxProviderResponseBytes: limits?.maxProviderResponseBytes ?? import_shared8.DEFAULT_AGENT_LIMITS.maxProviderResponseBytes,
1973
+ providerConnectTimeoutMs: limits?.providerConnectTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerConnectTimeoutMs,
1974
+ providerFirstByteTimeoutMs: limits?.providerFirstByteTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerFirstByteTimeoutMs,
1975
+ providerIdleTimeoutMs: limits?.providerIdleTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerIdleTimeoutMs,
1976
+ checkTimeoutMs: limits?.checkTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.checkTimeoutMs,
1977
+ jobTimeoutMs: limits?.jobTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.jobTimeoutMs
912
1978
  });
913
1979
  for (const [name, value] of Object.entries(resolved)) {
914
1980
  if (!Number.isSafeInteger(value) || value <= 0) {
@@ -1098,10 +2164,43 @@ function assertPositiveBudget(budget) {
1098
2164
  }
1099
2165
  }
1100
2166
  }
2167
+ function resolveDataFlowOptions(options) {
2168
+ if (options === void 0 || options === false) {
2169
+ return DEFAULT_OPTIONS.dataFlow;
2170
+ }
2171
+ const candidate = options;
2172
+ if (typeof candidate !== "object" || candidate === null) {
2173
+ throw new RangeError("SpotPatch dataFlow configuration is invalid.");
2174
+ }
2175
+ const runtime = options.runtime ?? "dispatch";
2176
+ if (runtime !== "dispatch") {
2177
+ throw new RangeError("SpotPatch dataFlow runtime mode is invalid.");
2178
+ }
2179
+ return Object.freeze({
2180
+ enabled: true,
2181
+ runtime,
2182
+ limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
2183
+ });
2184
+ }
2185
+ function createRuntimeDataFlowConfig(options) {
2186
+ return Object.freeze({
2187
+ enabled: options.enabled,
2188
+ runtime: options.runtime,
2189
+ limits: Object.freeze({
2190
+ observationMaxEntries: options.limits.observationMaxEntries,
2191
+ observationMaxBytes: options.limits.observationMaxBytes,
2192
+ observationTtlMs: options.limits.observationTtlMs,
2193
+ reportMaxBytes: options.limits.reportMaxBytes
2194
+ })
2195
+ });
2196
+ }
1101
2197
  function resolveOptions(options = {}, environmentAi) {
1102
2198
  if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
1103
2199
  throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
1104
2200
  }
2201
+ if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
2202
+ throw new RangeError("SpotPatch externalAgent must be a boolean.");
2203
+ }
1105
2204
  const budget = Object.freeze({
1106
2205
  ...DEFAULT_OPTIONS.budget,
1107
2206
  ...options.budget
@@ -1110,15 +2209,15 @@ function resolveOptions(options = {}, environmentAi) {
1110
2209
  const maxTargets = options.maxTargets ?? DEFAULT_OPTIONS.maxTargets;
1111
2210
  const locale = options.locale ?? DEFAULT_OPTIONS.locale;
1112
2211
  const editor = options.editor ?? DEFAULT_OPTIONS.editor;
1113
- if (!import_shared2.SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
2212
+ if (!import_shared8.SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
1114
2213
  throw new RangeError("SpotPatch locale must be auto, en-US, or zh-CN.");
1115
2214
  }
1116
- if (!import_shared2.SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
2215
+ if (!import_shared8.SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
1117
2216
  throw new RangeError("SpotPatch editor must be auto, vscode, or cursor.");
1118
2217
  }
1119
- if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > import_shared2.MAX_ANNOTATION_TARGETS) {
2218
+ if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > import_shared8.MAX_ANNOTATION_TARGETS) {
1120
2219
  throw new RangeError(
1121
- `SpotPatch maxTargets must be an integer between 1 and ${String(import_shared2.MAX_ANNOTATION_TARGETS)}.`
2220
+ `SpotPatch maxTargets must be an integer between 1 and ${String(import_shared8.MAX_ANNOTATION_TARGETS)}.`
1122
2221
  );
1123
2222
  }
1124
2223
  const resolved = {
@@ -1133,7 +2232,11 @@ function resolveOptions(options = {}, environmentAi) {
1133
2232
  debug: options.debug ?? DEFAULT_OPTIONS.debug,
1134
2233
  locale,
1135
2234
  maxTargets,
1136
- ai: resolveAiOptions(options.ai ?? environmentAi)
2235
+ ai: resolveAiOptions(options.ai ?? environmentAi),
2236
+ dataFlow: resolveDataFlowOptions(options.dataFlow),
2237
+ externalAgent: Object.freeze({
2238
+ enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
2239
+ })
1137
2240
  };
1138
2241
  if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
1139
2242
  throw new RangeError("SpotPatch shortcut is invalid.");
@@ -1143,9 +2246,9 @@ function resolveOptions(options = {}, environmentAi) {
1143
2246
 
1144
2247
  // src/project-validation.ts
1145
2248
  var import_node_child_process = require("child_process");
1146
- var import_promises2 = require("fs/promises");
2249
+ var import_promises3 = require("fs/promises");
1147
2250
  var import_node_module = require("module");
1148
- var import_node_path2 = __toESM(require("path"), 1);
2251
+ var import_node_path3 = __toESM(require("path"), 1);
1149
2252
  var import_node_util = require("util");
1150
2253
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
1151
2254
  var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
@@ -1155,19 +2258,19 @@ function isRecord(value) {
1155
2258
  }
1156
2259
  async function isRegularFile(absolutePath) {
1157
2260
  try {
1158
- const metadata = await (0, import_promises2.lstat)(absolutePath);
2261
+ const metadata = await (0, import_promises3.lstat)(absolutePath);
1159
2262
  return metadata.isFile() && !metadata.isSymbolicLink();
1160
2263
  } catch {
1161
2264
  return false;
1162
2265
  }
1163
2266
  }
1164
2267
  async function readManifest(appRoot) {
1165
- const manifestPath = import_node_path2.default.join(appRoot, "package.json");
2268
+ const manifestPath = import_node_path3.default.join(appRoot, "package.json");
1166
2269
  if (!await isRegularFile(manifestPath)) {
1167
2270
  return void 0;
1168
2271
  }
1169
2272
  try {
1170
- const value = JSON.parse(await (0, import_promises2.readFile)(manifestPath, "utf8"));
2273
+ const value = JSON.parse(await (0, import_promises3.readFile)(manifestPath, "utf8"));
1171
2274
  return isRecord(value) ? value : void 0;
1172
2275
  } catch {
1173
2276
  return void 0;
@@ -1190,9 +2293,9 @@ async function findGitRoot(appRoot) {
1190
2293
  timeout: 5e3,
1191
2294
  windowsHide: true
1192
2295
  });
1193
- const root = await (0, import_promises2.realpath)(result.stdout.trim());
1194
- const relative = import_node_path2.default.relative(root, appRoot);
1195
- if (relative === "" || !relative.startsWith(`..${import_node_path2.default.sep}`) && relative !== ".." && !import_node_path2.default.isAbsolute(relative)) {
2296
+ const root = await (0, import_promises3.realpath)(result.stdout.trim());
2297
+ const relative = import_node_path3.default.relative(root, appRoot);
2298
+ if (relative === "" || !relative.startsWith(`..${import_node_path3.default.sep}`) && relative !== ".." && !import_node_path3.default.isAbsolute(relative)) {
1196
2299
  return root;
1197
2300
  }
1198
2301
  } catch {
@@ -1201,22 +2304,22 @@ async function findGitRoot(appRoot) {
1201
2304
  return void 0;
1202
2305
  }
1203
2306
  async function resolveTypeScriptCli(appRoot) {
1204
- const resolveFromApplication = (0, import_node_module.createRequire)(import_node_path2.default.join(appRoot, "package.json"));
2307
+ const resolveFromApplication = (0, import_node_module.createRequire)(import_node_path3.default.join(appRoot, "package.json"));
1205
2308
  try {
1206
2309
  const packagePath = resolveFromApplication.resolve("typescript/package.json");
1207
- const cliPath = import_node_path2.default.join(import_node_path2.default.dirname(packagePath), "bin", "tsc");
1208
- await (0, import_promises2.access)(cliPath);
1209
- return await (0, import_promises2.realpath)(cliPath);
2310
+ const cliPath = import_node_path3.default.join(import_node_path3.default.dirname(packagePath), "bin", "tsc");
2311
+ await (0, import_promises3.access)(cliPath);
2312
+ return await (0, import_promises3.realpath)(cliPath);
1210
2313
  } catch {
1211
2314
  return void 0;
1212
2315
  }
1213
2316
  }
1214
2317
  function portableRelativePath(from, to) {
1215
- return import_node_path2.default.relative(from, to).split(import_node_path2.default.sep).join("/");
2318
+ return import_node_path3.default.relative(from, to).split(import_node_path3.default.sep).join("/");
1216
2319
  }
1217
2320
  async function discoverProjectValidationCheck(options) {
1218
- const appRoot = await (0, import_promises2.realpath)(options.appRoot);
1219
- const tsconfigPath = import_node_path2.default.join(appRoot, "tsconfig.json");
2321
+ const appRoot = await (0, import_promises3.realpath)(options.appRoot);
2322
+ const tsconfigPath = import_node_path3.default.join(appRoot, "tsconfig.json");
1220
2323
  const [manifest, projectRoot, hasTsconfig] = await Promise.all([
1221
2324
  readManifest(appRoot),
1222
2325
  findGitRoot(appRoot),
@@ -1304,61 +2407,86 @@ async function resolveProjectOptions(input) {
1304
2407
  }
1305
2408
 
1306
2409
  // src/registry/source-registry.ts
1307
- var import_node_path3 = __toESM(require("path"), 1);
2410
+ var import_node_path4 = __toESM(require("path"), 1);
1308
2411
 
1309
2412
  // src/registry/source-id.ts
1310
- var import_node_crypto3 = require("crypto");
2413
+ var import_node_crypto8 = require("crypto");
1311
2414
  var SOURCE_ID_BYTES = 8;
1312
- var createRandomSourceId = () => (0, import_node_crypto3.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
2415
+ var createRandomSourceId = () => (0, import_node_crypto8.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
1313
2416
 
1314
2417
  // src/registry/source-registry.ts
1315
2418
  function normalizeAbsolutePath(absolutePath) {
1316
- return import_node_path3.default.normalize(import_node_path3.default.resolve(absolutePath));
2419
+ return import_node_path4.default.normalize(import_node_path4.default.resolve(absolutePath));
1317
2420
  }
1318
2421
  function createSourceRegistry(options = {}) {
1319
2422
  const createId = options.createId ?? createRandomSourceId;
1320
2423
  const pathToId = /* @__PURE__ */ new Map();
1321
2424
  const idToPath = /* @__PURE__ */ new Map();
2425
+ const componentAnchors = /* @__PURE__ */ new Map();
2426
+ const componentIdsByPath = /* @__PURE__ */ new Map();
2427
+ function registerSourcePath(absolutePath) {
2428
+ const normalizedPath = normalizeAbsolutePath(absolutePath);
2429
+ const existingId = pathToId.get(normalizedPath);
2430
+ if (existingId !== void 0) {
2431
+ return existingId;
2432
+ }
2433
+ let fileId = createId();
2434
+ while (idToPath.has(fileId)) fileId = createId();
2435
+ pathToId.set(normalizedPath, fileId);
2436
+ idToPath.set(fileId, normalizedPath);
2437
+ return fileId;
2438
+ }
1322
2439
  return Object.freeze({
1323
2440
  register(absolutePath) {
2441
+ return registerSourcePath(absolutePath);
2442
+ },
2443
+ registerDataFlowComponents(absolutePath, sourceVersion, components) {
1324
2444
  const normalizedPath = normalizeAbsolutePath(absolutePath);
1325
- const existingId = pathToId.get(normalizedPath);
1326
- if (existingId !== void 0) {
1327
- return existingId;
2445
+ const previousIds = componentIdsByPath.get(normalizedPath);
2446
+ for (const componentSourceId of previousIds ?? []) {
2447
+ componentAnchors.delete(componentSourceId);
1328
2448
  }
1329
- let fileId = createId();
1330
- while (idToPath.has(fileId)) {
1331
- fileId = createId();
2449
+ const fileId = registerSourcePath(normalizedPath);
2450
+ const currentIds = /* @__PURE__ */ new Set();
2451
+ for (const component of components) {
2452
+ currentIds.add(component.componentSourceId);
2453
+ componentAnchors.set(
2454
+ component.componentSourceId,
2455
+ Object.freeze({ ...component, fileId, sourceVersion })
2456
+ );
1332
2457
  }
1333
- pathToId.set(normalizedPath, fileId);
1334
- idToPath.set(fileId, normalizedPath);
1335
- return fileId;
2458
+ componentIdsByPath.set(normalizedPath, currentIds);
1336
2459
  },
1337
2460
  resolve(fileId) {
1338
2461
  return idToPath.get(fileId);
1339
2462
  },
2463
+ resolveDataFlowComponent(componentSourceId) {
2464
+ return componentAnchors.get(componentSourceId);
2465
+ },
1340
2466
  clear() {
1341
2467
  pathToId.clear();
1342
2468
  idToPath.clear();
2469
+ componentAnchors.clear();
2470
+ componentIdsByPath.clear();
1343
2471
  }
1344
2472
  });
1345
2473
  }
1346
2474
 
1347
2475
  // src/server/middleware.ts
1348
- var import_shared10 = require("@spotpatch/shared");
2476
+ var import_shared18 = require("@spotpatch/shared");
1349
2477
 
1350
- // src/server/agent-http.ts
1351
- var import_shared7 = require("@spotpatch/shared");
2478
+ // src/external-handoff/browser-http.ts
2479
+ var import_shared12 = require("@spotpatch/shared");
1352
2480
 
1353
- // src/server/agent-request.ts
1354
- var import_promises5 = require("fs/promises");
1355
- var import_node_path6 = __toESM(require("path"), 1);
1356
- var import_shared5 = require("@spotpatch/shared");
2481
+ // src/server/annotation-authorizer.ts
2482
+ var import_promises6 = require("fs/promises");
2483
+ var import_node_path7 = __toESM(require("path"), 1);
2484
+ var import_shared11 = require("@spotpatch/shared");
1357
2485
 
1358
2486
  // src/server/source-context.ts
1359
- var import_promises4 = require("fs/promises");
1360
- var import_node_path5 = __toESM(require("path"), 1);
1361
- var import_shared4 = require("@spotpatch/shared");
2487
+ var import_promises5 = require("fs/promises");
2488
+ var import_node_path6 = __toESM(require("path"), 1);
2489
+ var import_shared10 = require("@spotpatch/shared");
1362
2490
 
1363
2491
  // src/server/extract-code-context.ts
1364
2492
  var import_oxc_parser = require("oxc-parser");
@@ -1587,16 +2715,9 @@ function extractCodeContext(options) {
1587
2715
  }
1588
2716
 
1589
2717
  // src/server/source-file.ts
1590
- var import_promises3 = require("fs/promises");
1591
- var import_node_path4 = __toESM(require("path"), 1);
1592
- var import_shared3 = require("@spotpatch/shared");
1593
-
1594
- // src/server/constants.ts
1595
- var MAX_REQUEST_BODY_BYTES = 32 * 1024;
1596
- var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
1597
- var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
1598
-
1599
- // src/server/source-file.ts
2718
+ var import_promises4 = require("fs/promises");
2719
+ var import_node_path5 = __toESM(require("path"), 1);
2720
+ var import_shared9 = require("@spotpatch/shared");
1600
2721
  var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
1601
2722
  function isMissingFileError(error) {
1602
2723
  return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
@@ -1606,56 +2727,56 @@ async function assertInsideRoot(root, candidate) {
1606
2727
  let realCandidate;
1607
2728
  try {
1608
2729
  [realRoot, realCandidate] = await Promise.all([
1609
- (0, import_promises3.realpath)(root),
1610
- (0, import_promises3.realpath)(candidate)
2730
+ (0, import_promises4.realpath)(root),
2731
+ (0, import_promises4.realpath)(candidate)
1611
2732
  ]);
1612
2733
  } catch (error) {
1613
2734
  if (isMissingFileError(error)) {
1614
- throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
2735
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
1615
2736
  cause: error
1616
2737
  });
1617
2738
  }
1618
2739
  throw error;
1619
2740
  }
1620
- const relative = import_node_path4.default.relative(realRoot, realCandidate);
1621
- const outside = relative.startsWith(`..${import_node_path4.default.sep}`) || relative === ".." || import_node_path4.default.isAbsolute(relative);
2741
+ const relative = import_node_path5.default.relative(realRoot, realCandidate);
2742
+ const outside = relative.startsWith(`..${import_node_path5.default.sep}`) || relative === ".." || import_node_path5.default.isAbsolute(relative);
1622
2743
  if (outside) {
1623
- throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
2744
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
1624
2745
  }
1625
2746
  return realCandidate;
1626
2747
  }
1627
2748
  async function resolveSourceFile(options) {
1628
2749
  const registeredPath = options.registry.resolve(options.fileId);
1629
2750
  if (registeredPath === void 0) {
1630
- throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
2751
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND);
1631
2752
  }
1632
2753
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
1633
- if (!ALLOWED_EXTENSIONS.has(import_node_path4.default.extname(sourcePath).toLowerCase())) {
1634
- throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
2754
+ if (!ALLOWED_EXTENSIONS.has(import_node_path5.default.extname(sourcePath).toLowerCase())) {
2755
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND);
1635
2756
  }
1636
2757
  let sourceStat;
1637
2758
  try {
1638
- sourceStat = await (0, import_promises3.stat)(sourcePath);
2759
+ sourceStat = await (0, import_promises4.stat)(sourcePath);
1639
2760
  } catch (error) {
1640
2761
  if (isMissingFileError(error)) {
1641
- throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
2762
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
1642
2763
  cause: error
1643
2764
  });
1644
2765
  }
1645
2766
  throw error;
1646
2767
  }
1647
2768
  if (!sourceStat.isFile()) {
1648
- throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_NOT_FOUND);
2769
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND);
1649
2770
  }
1650
2771
  if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
1651
- throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.SOURCE_TOO_LARGE);
2772
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_TOO_LARGE);
1652
2773
  }
1653
2774
  return sourcePath;
1654
2775
  }
1655
2776
 
1656
2777
  // src/server/source-context.ts
1657
2778
  function toDisplayPath(root, sourcePath) {
1658
- return import_node_path5.default.relative(root, sourcePath).split(import_node_path5.default.sep).join("/");
2779
+ return import_node_path6.default.relative(root, sourcePath).split(import_node_path6.default.sep).join("/");
1659
2780
  }
1660
2781
  async function readSourceContext(options) {
1661
2782
  const sourcePath = await resolveSourceFile({
@@ -1665,10 +2786,10 @@ async function readSourceContext(options) {
1665
2786
  });
1666
2787
  let source;
1667
2788
  try {
1668
- source = await (0, import_promises4.readFile)(sourcePath, "utf8");
2789
+ source = await (0, import_promises5.readFile)(sourcePath, "utf8");
1669
2790
  } catch (error) {
1670
2791
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1671
- throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
2792
+ throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
1672
2793
  cause: error
1673
2794
  });
1674
2795
  }
@@ -1676,13 +2797,13 @@ async function readSourceContext(options) {
1676
2797
  }
1677
2798
  const lines = source.split(/\r?\n/);
1678
2799
  if (options.request.line > lines.length) {
1679
- throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
2800
+ throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
1680
2801
  }
1681
- const extension = import_node_path5.default.extname(sourcePath).toLowerCase();
2802
+ const extension = import_node_path6.default.extname(sourcePath).toLowerCase();
1682
2803
  return extractCodeContext({
1683
2804
  source,
1684
2805
  sourcePath,
1685
- relativePath: toDisplayPath(await (0, import_promises4.realpath)(options.root), sourcePath),
2806
+ relativePath: toDisplayPath(await (0, import_promises5.realpath)(options.root), sourcePath),
1686
2807
  language: extension === ".tsx" ? "tsx" : "jsx",
1687
2808
  line: options.request.line,
1688
2809
  column: options.request.column,
@@ -1691,7 +2812,17 @@ async function readSourceContext(options) {
1691
2812
  });
1692
2813
  }
1693
2814
 
1694
- // src/server/agent-request.ts
2815
+ // src/server/annotation-authorizer.ts
2816
+ function sanitizePageContext(page) {
2817
+ return Object.freeze({
2818
+ url: (0, import_shared11.sanitizeUrl)(page.url, page.url),
2819
+ pathname: (0, import_shared11.redactSensitiveText)(page.pathname),
2820
+ title: (0, import_shared11.redactSensitiveText)(page.title),
2821
+ viewportWidth: page.viewportWidth,
2822
+ viewportHeight: page.viewportHeight,
2823
+ devicePixelRatio: page.devicePixelRatio
2824
+ });
2825
+ }
1695
2826
  function compactSourceRef(source) {
1696
2827
  return Object.freeze({
1697
2828
  origin: source.origin,
@@ -1705,27 +2836,20 @@ function compactSourceRef(source) {
1705
2836
  async function authorizeSourceRef(source, registry, root) {
1706
2837
  const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
1707
2838
  if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
1708
- throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
2839
+ throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
1709
2840
  }
1710
2841
  if (source.fileId === void 0) {
1711
2842
  if (source.origin === "none" && source.relativePath !== void 0) {
1712
- throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
2843
+ throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
1713
2844
  }
1714
2845
  return compactSourceRef(source);
1715
2846
  }
1716
- const sourcePath = await resolveSourceFile({
1717
- fileId: source.fileId,
1718
- registry,
1719
- root
1720
- });
1721
- const relativePath = import_node_path6.default.relative(await (0, import_promises5.realpath)(root), sourcePath).split(import_node_path6.default.sep).join("/");
2847
+ const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
2848
+ const relativePath = import_node_path7.default.relative(await (0, import_promises6.realpath)(root), sourcePath).split(import_node_path7.default.sep).join("/");
1722
2849
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
1723
- throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
2850
+ throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
1724
2851
  }
1725
- return Object.freeze({
1726
- ...compactSourceRef(source),
1727
- relativePath
1728
- });
2852
+ return Object.freeze({ ...compactSourceRef(source), relativePath });
1729
2853
  }
1730
2854
  function freezeMatchedRule(rule) {
1731
2855
  return Object.freeze({
@@ -1759,7 +2883,7 @@ async function authorizeTarget(target, input) {
1759
2883
  maxLines: input.options.budget.maxCodeLines
1760
2884
  });
1761
2885
  if (marker === void 0 && target.code !== void 0) {
1762
- throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
2886
+ throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
1763
2887
  }
1764
2888
  const code = marker === void 0 ? void 0 : await readSourceContext({
1765
2889
  request: marker,
@@ -1769,11 +2893,11 @@ async function authorizeTarget(target, input) {
1769
2893
  maxLines: input.options.budget.maxCodeLines
1770
2894
  });
1771
2895
  if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
1772
- throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
2896
+ throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
1773
2897
  }
1774
2898
  return Object.freeze({
1775
2899
  instruction: target.instruction,
1776
- ...target.page === void 0 ? {} : { page: Object.freeze({ ...target.page }) },
2900
+ ...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
1777
2901
  source,
1778
2902
  react: Object.freeze({
1779
2903
  supported: target.react.supported,
@@ -1801,76 +2925,130 @@ async function authorizeTarget(target, input) {
1801
2925
  warnings: Object.freeze([...target.warnings])
1802
2926
  });
1803
2927
  }
1804
- async function authorizeAgentJobRequest(input) {
1805
- const requestedTargets = input.request.annotation.targets;
2928
+ async function authorizeAnnotation(input) {
2929
+ const requestedTargets = input.annotation.targets;
1806
2930
  if (requestedTargets.length > input.options.maxTargets) {
1807
- throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
2931
+ throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
1808
2932
  }
1809
2933
  const identities = requestedTargets.map(targetIdentity);
1810
2934
  if (new Set(identities).size !== identities.length) {
1811
- throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.INVALID_REQUEST);
2935
+ throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
1812
2936
  }
1813
2937
  const targets = Object.freeze(
1814
2938
  await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
1815
2939
  );
1816
- const annotation = Object.freeze({
2940
+ return Object.freeze({
1817
2941
  schemaVersion: 3,
1818
- id: input.request.annotation.id,
1819
- locale: input.request.annotation.locale,
1820
- page: Object.freeze({ ...input.request.annotation.page }),
2942
+ id: input.annotation.id,
2943
+ locale: input.annotation.locale,
2944
+ page: sanitizePageContext(input.annotation.page),
1821
2945
  targets,
1822
- createdAt: input.request.annotation.createdAt
1823
- });
1824
- return Object.freeze({
1825
- annotation,
1826
- ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
1827
- providerProfileId: input.request.providerProfileId,
1828
- modelProfileId: input.request.modelProfileId,
1829
- providerDataConsent: true,
1830
- ...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
1831
- workingTreeMode: input.request.workingTreeMode
2946
+ createdAt: input.annotation.createdAt
1832
2947
  });
1833
2948
  }
1834
2949
 
1835
- // src/server/request-body.ts
1836
- var import_shared6 = require("@spotpatch/shared");
1837
- function isJsonContentType(value) {
1838
- return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
1839
- }
1840
- async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
1841
- if (!isJsonContentType(request.headers["content-type"])) {
1842
- throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
2950
+ // src/external-handoff/browser-http.ts
2951
+ function matchExternalHandoffBrowserPath(path9) {
2952
+ if (path9 === import_shared12.SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
2953
+ if (path9 === import_shared12.SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
2954
+ if (path9 === import_shared12.SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
2955
+ if (path9 === import_shared12.SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
2956
+ return "resolve-delivery";
1843
2957
  }
1844
- const declaredLength = Number(request.headers["content-length"]);
1845
- if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
1846
- throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
2958
+ return void 0;
2959
+ }
2960
+ function requireService(options) {
2961
+ if (!options.options.externalAgent.enabled || options.service === void 0) {
2962
+ throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED);
1847
2963
  }
1848
- const chunks = [];
1849
- let byteLength = 0;
1850
- let exceededLimit = false;
1851
- for await (const rawChunk of request) {
1852
- const chunk = rawChunk;
1853
- if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
1854
- throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
2964
+ return options.service;
2965
+ }
2966
+ function remapAuthorizationError(error) {
2967
+ if (error instanceof import_shared12.SpotPatchError) {
2968
+ if (error.code === import_shared12.ERROR_CODES.SOURCE_NOT_FOUND || error.code === import_shared12.ERROR_CODES.SOURCE_OUTSIDE_ROOT || error.code === import_shared12.ERROR_CODES.SOURCE_TOO_LARGE) {
2969
+ throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.HANDOFF_SOURCE_STALE, void 0, {
2970
+ cause: error
2971
+ });
1855
2972
  }
1856
- const buffer = Buffer.from(chunk);
1857
- byteLength += buffer.byteLength;
1858
- if (byteLength > maximumBytes) {
1859
- exceededLimit = true;
1860
- continue;
2973
+ if (error.code === import_shared12.ERROR_CODES.INVALID_REQUEST) {
2974
+ throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.HANDOFF_VALIDATION_FAILED, void 0, {
2975
+ cause: error
2976
+ });
1861
2977
  }
1862
- chunks.push(buffer);
1863
2978
  }
1864
- if (exceededLimit || byteLength === 0) {
1865
- throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
2979
+ throw error;
2980
+ }
2981
+ async function handleExternalHandoffBrowserRequest(request, response, route, options, writeSuccess) {
2982
+ if (request.method !== "POST") {
2983
+ throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
1866
2984
  }
1867
- try {
1868
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1869
- } catch (error) {
1870
- throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST, void 0, {
1871
- cause: error
1872
- });
2985
+ const service = requireService(options);
2986
+ if (route === "capability") {
2987
+ const parsed2 = import_shared12.externalHandoffCapabilityRequestSchema.safeParse(
2988
+ await readJsonRequestBody(request)
2989
+ );
2990
+ if (!parsed2.success) throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
2991
+ writeSuccess(response, 200, service.capability());
2992
+ return;
1873
2993
  }
2994
+ if (route === "status") {
2995
+ const parsed2 = import_shared12.externalHandoffStatusRequestSchema.safeParse(
2996
+ await readJsonRequestBody(request)
2997
+ );
2998
+ if (!parsed2.success) throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
2999
+ writeSuccess(response, 200, service.status(parsed2.data.cursor));
3000
+ return;
3001
+ }
3002
+ if (route === "resolve-delivery") {
3003
+ const parsed2 = import_shared12.externalHandoffResolveDeliveryRequestSchema.safeParse(
3004
+ await readJsonRequestBody(request)
3005
+ );
3006
+ if (!parsed2.success) throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
3007
+ writeSuccess(response, 200, service.resolveDelivery(parsed2.data.cursor));
3008
+ return;
3009
+ }
3010
+ const parsed = import_shared12.externalHandoffPublishRequestSchema.safeParse(
3011
+ await readJsonRequestBody(request, import_shared12.EXTERNAL_HANDOFF_LIMITS.maximumPublishBodyBytes)
3012
+ );
3013
+ if (!parsed.success) {
3014
+ throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
3015
+ }
3016
+ const result = await service.publish(parsed.data, async (annotation) => {
3017
+ try {
3018
+ return await authorizeAnnotation({
3019
+ annotation,
3020
+ options: options.options,
3021
+ registry: options.registry,
3022
+ root: options.root
3023
+ });
3024
+ } catch (error) {
3025
+ remapAuthorizationError(error);
3026
+ }
3027
+ });
3028
+ writeSuccess(response, result.replayed ? 200 : 201, result);
3029
+ }
3030
+
3031
+ // src/server/agent-http.ts
3032
+ var import_shared14 = require("@spotpatch/shared");
3033
+
3034
+ // src/server/agent-request.ts
3035
+ var import_shared13 = require("@spotpatch/shared");
3036
+ async function authorizeAgentJobRequest(input) {
3037
+ const annotation = await authorizeAnnotation({
3038
+ annotation: input.request.annotation,
3039
+ options: input.options,
3040
+ registry: input.registry,
3041
+ root: input.root
3042
+ });
3043
+ return Object.freeze({
3044
+ annotation,
3045
+ ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
3046
+ providerProfileId: input.request.providerProfileId,
3047
+ modelProfileId: input.request.modelProfileId,
3048
+ providerDataConsent: true,
3049
+ ...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
3050
+ workingTreeMode: input.request.workingTreeMode
3051
+ });
1874
3052
  }
1875
3053
 
1876
3054
  // src/server/agent-http.ts
@@ -1890,21 +3068,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1890
3068
  "reverted",
1891
3069
  "failed"
1892
3070
  ]);
1893
- function matchAgentRequestPath(path8) {
1894
- if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentCapability) {
3071
+ function matchAgentRequestPath(path9) {
3072
+ if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentCapability) {
1895
3073
  return Object.freeze({ kind: "capability" });
1896
3074
  }
1897
- if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
3075
+ if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
1898
3076
  return Object.freeze({ kind: "workspace-health" });
1899
3077
  }
1900
- if (path8 === import_shared7.SPOTPATCH_ENDPOINTS.agentJobs) {
3078
+ if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentJobs) {
1901
3079
  return Object.freeze({ kind: "create-job" });
1902
3080
  }
1903
- const prefix = `${import_shared7.SPOTPATCH_ENDPOINTS.agentJobs}/`;
1904
- if (!path8.startsWith(prefix)) {
3081
+ const prefix = `${import_shared14.SPOTPATCH_ENDPOINTS.agentJobs}/`;
3082
+ if (!path9.startsWith(prefix)) {
1905
3083
  return void 0;
1906
3084
  }
1907
- const segments = path8.slice(prefix.length).split("/");
3085
+ const segments = path9.slice(prefix.length).split("/");
1908
3086
  const jobId = segments[0];
1909
3087
  const action = segments[1];
1910
3088
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1918,7 +3096,7 @@ function matchAgentRequestPath(path8) {
1918
3096
  }
1919
3097
  function requireAgentManager(options) {
1920
3098
  if (options.agentManager === void 0 || options.options.ai === false) {
1921
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.AI_DISABLED);
3099
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.AI_DISABLED);
1922
3100
  }
1923
3101
  return options.agentManager;
1924
3102
  }
@@ -1971,13 +3149,13 @@ function streamAgentJobEvents(response, manager, jobId) {
1971
3149
  }
1972
3150
  async function handleCapability(request, response, options, writeSuccess) {
1973
3151
  if (request.method !== "POST") {
1974
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
3152
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
1975
3153
  }
1976
- const parsed = import_shared7.agentCapabilityRequestSchema.safeParse(
3154
+ const parsed = import_shared14.agentCapabilityRequestSchema.safeParse(
1977
3155
  await readJsonRequestBody(request)
1978
3156
  );
1979
3157
  if (!parsed.success) {
1980
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
3158
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
1981
3159
  }
1982
3160
  const controller = new AbortController();
1983
3161
  const abort = () => {
@@ -1996,13 +3174,13 @@ async function handleCapability(request, response, options, writeSuccess) {
1996
3174
  }
1997
3175
  async function handleCreateJob(request, response, options, writeSuccess) {
1998
3176
  if (request.method !== "POST") {
1999
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
3177
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
2000
3178
  }
2001
- const parsed = import_shared7.agentJobCreateRequestSchema.safeParse(
3179
+ const parsed = import_shared14.agentJobCreateRequestSchema.safeParse(
2002
3180
  await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
2003
3181
  );
2004
3182
  if (!parsed.success) {
2005
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
3183
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
2006
3184
  }
2007
3185
  const authorizedRequest = await authorizeAgentJobRequest({
2008
3186
  request: parsed.data,
@@ -2015,13 +3193,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
2015
3193
  }
2016
3194
  async function handleWorkspaceHealth(request, response, options, writeSuccess) {
2017
3195
  if (request.method !== "POST") {
2018
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
3196
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
2019
3197
  }
2020
- const parsed = import_shared7.agentWorkspaceHealthRequestSchema.safeParse(
3198
+ const parsed = import_shared14.agentWorkspaceHealthRequestSchema.safeParse(
2021
3199
  await readJsonRequestBody(request)
2022
3200
  );
2023
3201
  if (!parsed.success) {
2024
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
3202
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
2025
3203
  }
2026
3204
  const controller = new AbortController();
2027
3205
  const abort = () => {
@@ -2038,13 +3216,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
2038
3216
  async function handleJobAction(request, response, options, route, writeSuccess) {
2039
3217
  const manager = requireAgentManager(options);
2040
3218
  if (request.method !== "POST") {
2041
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
3219
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
2042
3220
  }
2043
- const parsed = import_shared7.agentJobActionRequestSchema.safeParse(
3221
+ const parsed = import_shared14.agentJobActionRequestSchema.safeParse(
2044
3222
  await readJsonRequestBody(request)
2045
3223
  );
2046
3224
  if (!parsed.success) {
2047
- throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.INVALID_REQUEST);
3225
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
2048
3226
  }
2049
3227
  if (route.action === "events") {
2050
3228
  streamAgentJobEvents(response, manager, route.jobId);
@@ -2158,21 +3336,188 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
2158
3336
  }
2159
3337
  var launchConfiguredEditor = createEditorLauncher();
2160
3338
 
3339
+ // src/server/data-flow-http.ts
3340
+ var import_node_crypto9 = require("crypto");
3341
+ var import_analyzer = require("@spotpatch/analyzer");
3342
+ var import_shared15 = require("@spotpatch/shared");
3343
+ function envelopeBytes(report) {
3344
+ return Buffer.byteLength(JSON.stringify({ ok: true, data: report }), "utf8");
3345
+ }
3346
+ function limitDataFlowReportToBytes(report, maximumBytes) {
3347
+ const structurallyLimited = (0, import_shared15.limitDataFlowReportCollections)(report);
3348
+ if (envelopeBytes(structurallyLimited) <= maximumBytes) {
3349
+ return structurallyLimited;
3350
+ }
3351
+ let limited = (0, import_shared15.limitDataFlowReportCollections)(structurallyLimited, {
3352
+ forceTruncation: true,
3353
+ maximumDependencies: 0,
3354
+ truncatedBy: "bytes"
3355
+ });
3356
+ if (envelopeBytes(limited) > maximumBytes) {
3357
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INTERNAL_ERROR);
3358
+ }
3359
+ for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
3360
+ const candidate = (0, import_shared15.limitDataFlowReportCollections)(structurallyLimited, {
3361
+ forceTruncation: true,
3362
+ maximumDependencies,
3363
+ truncatedBy: "bytes"
3364
+ });
3365
+ if (envelopeBytes(candidate) > maximumBytes) break;
3366
+ limited = candidate;
3367
+ }
3368
+ return limited;
3369
+ }
3370
+ function createDataFlowAnalyzer(options) {
3371
+ if (!options.options.dataFlow.enabled) return void 0;
3372
+ return (0, import_analyzer.createStaticDataFlowAnalyzer)({
3373
+ root: options.root,
3374
+ registryEpoch: options.session.id,
3375
+ registerSource: (absolutePath) => options.registry.register(absolutePath),
3376
+ limits: options.options.dataFlow.limits
3377
+ });
3378
+ }
3379
+ async function analyzeTarget(request, analyzer, options) {
3380
+ const resolvedRequest = (() => {
3381
+ if ("componentSourceId" in request) {
3382
+ const anchor = options.registry.resolveDataFlowComponent(
3383
+ request.componentSourceId
3384
+ );
3385
+ if (anchor?.sourceVersion !== request.sourceVersion) {
3386
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
3387
+ }
3388
+ return anchor;
3389
+ }
3390
+ return request;
3391
+ })();
3392
+ const absolutePath = await resolveSourceFile({
3393
+ fileId: resolvedRequest.fileId,
3394
+ registry: options.registry,
3395
+ root: options.root
3396
+ });
3397
+ const report = analyzer.analyzeComponent({
3398
+ absolutePath,
3399
+ line: resolvedRequest.line,
3400
+ column: resolvedRequest.column
3401
+ });
3402
+ if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
3403
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
3404
+ }
3405
+ return limitDataFlowReportToBytes(
3406
+ report,
3407
+ options.options.dataFlow.limits.reportMaxBytes
3408
+ );
3409
+ }
3410
+ function requireAnalyzer(analyzer) {
3411
+ if (analyzer === void 0) {
3412
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_DISABLED);
3413
+ }
3414
+ return analyzer;
3415
+ }
3416
+ async function handleComponentDataFlowReport(request, analyzer, options) {
3417
+ const parsed = import_shared15.dataFlowComponentReportRequestSchema.safeParse(
3418
+ await readJsonRequestBody(
3419
+ request,
3420
+ options.options.dataFlow.limits.protocolRequestMaxBytes
3421
+ )
3422
+ );
3423
+ if (!parsed.success) {
3424
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
3425
+ }
3426
+ return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
3427
+ }
3428
+ async function handlePageDataFlowReport(request, analyzer, options) {
3429
+ const parsed = import_shared15.dataFlowPageReportRequestSchema.safeParse(
3430
+ await readJsonRequestBody(
3431
+ request,
3432
+ options.options.dataFlow.limits.protocolRequestMaxBytes
3433
+ )
3434
+ );
3435
+ if (!parsed.success) {
3436
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
3437
+ }
3438
+ const activeAnalyzer = requireAnalyzer(analyzer);
3439
+ const componentReports = await Promise.all(
3440
+ parsed.data.targets.map((target) => analyzeTarget(target, activeAnalyzer, options))
3441
+ );
3442
+ const dependencies = new Map(
3443
+ componentReports.flatMap(
3444
+ (report2) => report2.dependencies.map((dependency) => [dependency.id, dependency])
3445
+ )
3446
+ );
3447
+ const evidence = new Map(
3448
+ componentReports.flatMap(
3449
+ (report2) => report2.evidence.map((entry) => [entry.id, entry])
3450
+ )
3451
+ );
3452
+ const diagnostics = new Map(
3453
+ componentReports.flatMap(
3454
+ (report2) => report2.diagnostics.map((entry) => [entry.id, entry])
3455
+ )
3456
+ );
3457
+ const analyzedVersions = new Set(
3458
+ componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
3459
+ );
3460
+ const reportId = `page_${(0, import_node_crypto9.createHash)("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
3461
+ const complete = componentReports.every((report2) => report2.completeness.complete);
3462
+ const report = Object.freeze({
3463
+ schemaVersion: import_shared15.DATA_FLOW_SCHEMA_VERSION,
3464
+ reportId,
3465
+ baseline: Object.freeze({
3466
+ registryEpoch: options.session.id,
3467
+ analyzerVersion: componentReports[0]?.baseline.analyzerVersion ?? "unavailable",
3468
+ adapterSetHash: componentReports[0]?.baseline.adapterSetHash ?? "unavailable",
3469
+ analyzedSourceVersions: Object.freeze([...analyzedVersions].sort())
3470
+ }),
3471
+ capability: Object.freeze({
3472
+ enabled: true,
3473
+ staticAnalysis: complete ? "available" : "partial",
3474
+ runtimeObservation: "dispatch-only",
3475
+ responseShape: "consumed-fields-only",
3476
+ aiAssistance: "disabled",
3477
+ reasons: Object.freeze(
3478
+ componentReports.flatMap((report2) => report2.capability.reasons)
3479
+ )
3480
+ }),
3481
+ dependencies: Object.freeze([...dependencies.values()]),
3482
+ evidence: Object.freeze([...evidence.values()]),
3483
+ diagnostics: Object.freeze([...diagnostics.values()]),
3484
+ completeness: Object.freeze({
3485
+ complete,
3486
+ visitedModules: componentReports.reduce(
3487
+ (total, report2) => total + report2.completeness.visitedModules,
3488
+ 0
3489
+ ),
3490
+ visitedCallsites: componentReports.reduce(
3491
+ (total, report2) => total + report2.completeness.visitedCallsites,
3492
+ 0
3493
+ ),
3494
+ frontierCount: componentReports.reduce(
3495
+ (total, report2) => total + report2.completeness.frontierCount,
3496
+ 0
3497
+ )
3498
+ })
3499
+ });
3500
+ return limitDataFlowReportToBytes(
3501
+ report,
3502
+ options.options.dataFlow.limits.reportMaxBytes
3503
+ );
3504
+ }
3505
+
2161
3506
  // src/server/request-security.ts
2162
- var import_node_crypto4 = require("crypto");
3507
+ var import_node_crypto10 = require("crypto");
2163
3508
  var import_node_net = require("net");
2164
- var import_shared8 = require("@spotpatch/shared");
3509
+ var import_shared16 = require("@spotpatch/shared");
2165
3510
  function getSingleHeader(request, name) {
2166
3511
  const value = request.headers[name.toLowerCase()];
2167
3512
  return Array.isArray(value) ? value[0] : value;
2168
3513
  }
2169
- function tokensMatch(actual, expected) {
3514
+ function tokensMatch2(actual, expected) {
2170
3515
  if (actual === void 0) {
2171
3516
  return false;
2172
3517
  }
2173
3518
  const actualBytes = Buffer.from(actual);
2174
3519
  const expectedBytes = Buffer.from(expected);
2175
- return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto4.timingSafeEqual)(actualBytes, expectedBytes);
3520
+ return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto10.timingSafeEqual)(actualBytes, expectedBytes);
2176
3521
  }
2177
3522
  function isLoopbackHostname(hostname) {
2178
3523
  const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
@@ -2206,32 +3551,32 @@ function parseOrigin(value) {
2206
3551
  }
2207
3552
  }
2208
3553
  function assertRequestAuthorized(request, options) {
2209
- const actualToken = getSingleHeader(request, import_shared8.SPOTPATCH_TOKEN_HEADER);
2210
- if (!tokensMatch(actualToken, options.sessionToken)) {
2211
- throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.INVALID_TOKEN);
3554
+ const actualToken = getSingleHeader(request, import_shared16.SPOTPATCH_TOKEN_HEADER);
3555
+ if (!tokensMatch2(actualToken, options.sessionToken)) {
3556
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_TOKEN);
2212
3557
  }
2213
3558
  const hostHeader = getSingleHeader(request, "host");
2214
3559
  const originHeader = getSingleHeader(request, "origin");
2215
3560
  const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
2216
3561
  const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
2217
3562
  if (host === void 0 || origin === void 0) {
2218
- throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.ORIGIN_NOT_ALLOWED);
3563
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
2219
3564
  }
2220
3565
  const hostIsLoopback = isLoopbackHostname(host.hostname);
2221
3566
  const originIsLoopback = isLoopbackHostname(origin.hostname);
2222
3567
  if (!options.allowLan) {
2223
3568
  if (!hostIsLoopback || !originIsLoopback) {
2224
- throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.ORIGIN_NOT_ALLOWED);
3569
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
2225
3570
  }
2226
3571
  return;
2227
3572
  }
2228
3573
  if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
2229
- throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.ORIGIN_NOT_ALLOWED);
3574
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
2230
3575
  }
2231
3576
  }
2232
3577
 
2233
3578
  // src/server/runtime-bootstrap.ts
2234
- var import_shared9 = require("@spotpatch/shared");
3579
+ var import_shared17 = require("@spotpatch/shared");
2235
3580
  function getSingleHeader2(request, name) {
2236
3581
  const value = request.headers[name.toLowerCase()];
2237
3582
  return Array.isArray(value) ? value[0] : value;
@@ -2246,7 +3591,7 @@ function resolveRuntimeBootstrapOptions(options) {
2246
3591
  if (expectedOrigin.origin !== options.expectedOrigin || expectedOrigin.protocol !== "http:" || !isLoopbackHostname(expectedOrigin.hostname)) {
2247
3592
  throw new TypeError("The SpotPatch bootstrap origin must be a loopback origin.");
2248
3593
  }
2249
- const parsedConfig = import_shared9.runtimeConfigSchema.safeParse(options.runtimeConfig);
3594
+ const parsedConfig = import_shared17.runtimeConfigSchema.safeParse(options.runtimeConfig);
2250
3595
  if (!parsedConfig.success) {
2251
3596
  throw new TypeError("The SpotPatch Runtime configuration is invalid.");
2252
3597
  }
@@ -2258,7 +3603,7 @@ function resolveRuntimeBootstrapOptions(options) {
2258
3603
  function assertRuntimeBootstrapRequest(request, expectedOrigin) {
2259
3604
  const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
2260
3605
  if (request.method !== "POST" || contentType !== "application/json") {
2261
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.INVALID_REQUEST);
3606
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
2262
3607
  }
2263
3608
  const host = getSingleHeader2(request, "host");
2264
3609
  let hostIsLoopback = false;
@@ -2270,106 +3615,148 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
2270
3615
  }
2271
3616
  }
2272
3617
  if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
2273
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.ORIGIN_NOT_ALLOWED);
3618
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.ORIGIN_NOT_ALLOWED);
2274
3619
  }
2275
3620
  }
2276
3621
  async function readRuntimeBootstrap(request, options) {
2277
3622
  assertRuntimeBootstrapRequest(request, options.expectedOrigin);
2278
- const parsedBody = import_shared9.runtimeBootstrapRequestSchema.safeParse(
3623
+ const parsedBody = import_shared17.runtimeBootstrapRequestSchema.safeParse(
2279
3624
  await readJsonRequestBody(request)
2280
3625
  );
2281
3626
  if (!parsedBody.success) {
2282
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.INVALID_REQUEST);
3627
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
2283
3628
  }
2284
3629
  return options.runtimeConfig;
2285
3630
  }
2286
3631
 
2287
3632
  // src/server/middleware.ts
2288
3633
  var STATUS_BY_ERROR = Object.freeze({
2289
- [import_shared10.ERROR_CODES.INVALID_REQUEST]: 400,
2290
- [import_shared10.ERROR_CODES.INVALID_TOKEN]: 401,
2291
- [import_shared10.ERROR_CODES.ORIGIN_NOT_ALLOWED]: 403,
2292
- [import_shared10.ERROR_CODES.SOURCE_NOT_FOUND]: 404,
2293
- [import_shared10.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: 403,
2294
- [import_shared10.ERROR_CODES.SOURCE_TOO_LARGE]: 413,
2295
- [import_shared10.ERROR_CODES.EDITOR_OPEN_FAILED]: 500,
2296
- [import_shared10.ERROR_CODES.AI_DISABLED]: 404,
2297
- [import_shared10.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: 503,
2298
- [import_shared10.ERROR_CODES.PROVIDER_AUTH_FAILED]: 502,
2299
- [import_shared10.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
2300
- [import_shared10.ERROR_CODES.MODEL_NOT_ALLOWED]: 400,
2301
- [import_shared10.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
2302
- [import_shared10.ERROR_CODES.PROVIDER_RATE_LIMITED]: 429,
2303
- [import_shared10.ERROR_CODES.AGENT_BUSY]: 409,
2304
- [import_shared10.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: 413,
2305
- [import_shared10.ERROR_CODES.AGENT_CANCELLED]: 409,
2306
- [import_shared10.ERROR_CODES.WORKTREE_DIRTY]: 409,
2307
- [import_shared10.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: 409,
2308
- [import_shared10.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: 409,
2309
- [import_shared10.ERROR_CODES.WORKTREE_CONFLICTED]: 409,
2310
- [import_shared10.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
2311
- [import_shared10.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
2312
- [import_shared10.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
2313
- [import_shared10.ERROR_CODES.TOOL_DENIED]: 403,
2314
- [import_shared10.ERROR_CODES.TOOL_INPUT_INVALID]: 422,
2315
- [import_shared10.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: 422,
2316
- [import_shared10.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: 422,
2317
- [import_shared10.ERROR_CODES.TOOL_PATH_DENIED]: 403,
2318
- [import_shared10.ERROR_CODES.PATCH_REJECTED]: 422,
2319
- [import_shared10.ERROR_CODES.VALIDATION_FAILED]: 422,
2320
- [import_shared10.ERROR_CODES.APPLY_CONFLICT]: 409,
2321
- [import_shared10.ERROR_CODES.INTERNAL_ERROR]: 500
3634
+ [import_shared18.ERROR_CODES.INVALID_REQUEST]: 400,
3635
+ [import_shared18.ERROR_CODES.INVALID_TOKEN]: 401,
3636
+ [import_shared18.ERROR_CODES.ORIGIN_NOT_ALLOWED]: 403,
3637
+ [import_shared18.ERROR_CODES.SOURCE_NOT_FOUND]: 404,
3638
+ [import_shared18.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: 403,
3639
+ [import_shared18.ERROR_CODES.SOURCE_TOO_LARGE]: 413,
3640
+ [import_shared18.ERROR_CODES.EDITOR_OPEN_FAILED]: 500,
3641
+ [import_shared18.ERROR_CODES.DATA_FLOW_DISABLED]: 404,
3642
+ [import_shared18.ERROR_CODES.DATA_FLOW_SOURCE_STALE]: 409,
3643
+ [import_shared18.ERROR_CODES.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
3644
+ [import_shared18.ERROR_CODES.AI_DISABLED]: 404,
3645
+ [import_shared18.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: 503,
3646
+ [import_shared18.ERROR_CODES.PROVIDER_AUTH_FAILED]: 502,
3647
+ [import_shared18.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
3648
+ [import_shared18.ERROR_CODES.MODEL_NOT_ALLOWED]: 400,
3649
+ [import_shared18.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
3650
+ [import_shared18.ERROR_CODES.PROVIDER_RATE_LIMITED]: 429,
3651
+ [import_shared18.ERROR_CODES.AGENT_BUSY]: 409,
3652
+ [import_shared18.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: 413,
3653
+ [import_shared18.ERROR_CODES.AGENT_CANCELLED]: 409,
3654
+ [import_shared18.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED]: 404,
3655
+ [import_shared18.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE]: 503,
3656
+ [import_shared18.ERROR_CODES.HANDOFF_VALIDATION_FAILED]: 422,
3657
+ [import_shared18.ERROR_CODES.HANDOFF_SOURCE_STALE]: 409,
3658
+ [import_shared18.ERROR_CODES.HANDOFF_NOT_FOUND]: 404,
3659
+ [import_shared18.ERROR_CODES.HANDOFF_EXPIRED]: 410,
3660
+ [import_shared18.ERROR_CODES.HANDOFF_CURSOR_INVALID]: 409,
3661
+ [import_shared18.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE]: 413,
3662
+ [import_shared18.ERROR_CODES.BRIDGE_UNAUTHORIZED]: 401,
3663
+ [import_shared18.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH]: 409,
3664
+ [import_shared18.ERROR_CODES.BRIDGE_BUSY]: 429,
3665
+ [import_shared18.ERROR_CODES.EXTERNAL_AGENT_BUSY]: 409,
3666
+ [import_shared18.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT]: 409,
3667
+ [import_shared18.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID]: 409,
3668
+ [import_shared18.ERROR_CODES.ACTIVE_DISPATCH_INVALID]: 409,
3669
+ [import_shared18.ERROR_CODES.SESSION_NOT_FOUND]: 404,
3670
+ [import_shared18.ERROR_CODES.SESSION_AMBIGUOUS]: 409,
3671
+ [import_shared18.ERROR_CODES.SESSION_CLOSED]: 410,
3672
+ [import_shared18.ERROR_CODES.WORKTREE_DIRTY]: 409,
3673
+ [import_shared18.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: 409,
3674
+ [import_shared18.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: 409,
3675
+ [import_shared18.ERROR_CODES.WORKTREE_CONFLICTED]: 409,
3676
+ [import_shared18.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
3677
+ [import_shared18.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
3678
+ [import_shared18.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
3679
+ [import_shared18.ERROR_CODES.TOOL_DENIED]: 403,
3680
+ [import_shared18.ERROR_CODES.TOOL_INPUT_INVALID]: 422,
3681
+ [import_shared18.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: 422,
3682
+ [import_shared18.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: 422,
3683
+ [import_shared18.ERROR_CODES.TOOL_PATH_DENIED]: 403,
3684
+ [import_shared18.ERROR_CODES.PATCH_REJECTED]: 422,
3685
+ [import_shared18.ERROR_CODES.VALIDATION_FAILED]: 422,
3686
+ [import_shared18.ERROR_CODES.APPLY_CONFLICT]: 409,
3687
+ [import_shared18.ERROR_CODES.INTERNAL_ERROR]: 500
2322
3688
  });
2323
3689
  var PUBLIC_MESSAGES = Object.freeze({
2324
- [import_shared10.ERROR_CODES.INVALID_REQUEST]: "The request is invalid.",
2325
- [import_shared10.ERROR_CODES.INVALID_TOKEN]: "The session token is invalid.",
2326
- [import_shared10.ERROR_CODES.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
2327
- [import_shared10.ERROR_CODES.SOURCE_NOT_FOUND]: "The source file is unavailable.",
2328
- [import_shared10.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
2329
- [import_shared10.ERROR_CODES.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
2330
- [import_shared10.ERROR_CODES.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
2331
- [import_shared10.ERROR_CODES.AI_DISABLED]: "AI execution is not enabled.",
2332
- [import_shared10.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
2333
- [import_shared10.ERROR_CODES.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
2334
- [import_shared10.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
2335
- [import_shared10.ERROR_CODES.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
2336
- [import_shared10.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
2337
- [import_shared10.ERROR_CODES.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
2338
- [import_shared10.ERROR_CODES.AGENT_BUSY]: "Another Agent job is already running.",
2339
- [import_shared10.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
2340
- [import_shared10.ERROR_CODES.AGENT_CANCELLED]: "The Agent job was cancelled.",
2341
- [import_shared10.ERROR_CODES.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
2342
- [import_shared10.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
2343
- [import_shared10.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
2344
- [import_shared10.ERROR_CODES.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
2345
- [import_shared10.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
2346
- [import_shared10.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
2347
- [import_shared10.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
2348
- [import_shared10.ERROR_CODES.TOOL_DENIED]: "The Agent tool request was denied.",
2349
- [import_shared10.ERROR_CODES.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
2350
- [import_shared10.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
2351
- [import_shared10.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
2352
- [import_shared10.ERROR_CODES.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
2353
- [import_shared10.ERROR_CODES.PATCH_REJECTED]: "The proposed patch was rejected.",
2354
- [import_shared10.ERROR_CODES.VALIDATION_FAILED]: "The proposed change failed validation.",
2355
- [import_shared10.ERROR_CODES.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
2356
- [import_shared10.ERROR_CODES.INTERNAL_ERROR]: "The request could not be completed."
3690
+ [import_shared18.ERROR_CODES.INVALID_REQUEST]: "The request is invalid.",
3691
+ [import_shared18.ERROR_CODES.INVALID_TOKEN]: "The session token is invalid.",
3692
+ [import_shared18.ERROR_CODES.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
3693
+ [import_shared18.ERROR_CODES.SOURCE_NOT_FOUND]: "The source file is unavailable.",
3694
+ [import_shared18.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
3695
+ [import_shared18.ERROR_CODES.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
3696
+ [import_shared18.ERROR_CODES.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
3697
+ [import_shared18.ERROR_CODES.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
3698
+ [import_shared18.ERROR_CODES.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
3699
+ [import_shared18.ERROR_CODES.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
3700
+ [import_shared18.ERROR_CODES.AI_DISABLED]: "AI execution is not enabled.",
3701
+ [import_shared18.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
3702
+ [import_shared18.ERROR_CODES.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
3703
+ [import_shared18.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
3704
+ [import_shared18.ERROR_CODES.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
3705
+ [import_shared18.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
3706
+ [import_shared18.ERROR_CODES.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
3707
+ [import_shared18.ERROR_CODES.AGENT_BUSY]: "Another Agent job is already running.",
3708
+ [import_shared18.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
3709
+ [import_shared18.ERROR_CODES.AGENT_CANCELLED]: "The Agent job was cancelled.",
3710
+ [import_shared18.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED]: "External Agent handoff is not enabled.",
3711
+ [import_shared18.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE]: "External Agent handoff is temporarily unavailable.",
3712
+ [import_shared18.ERROR_CODES.HANDOFF_VALIDATION_FAILED]: "The handoff content is invalid.",
3713
+ [import_shared18.ERROR_CODES.HANDOFF_SOURCE_STALE]: "The selected source is stale.",
3714
+ [import_shared18.ERROR_CODES.HANDOFF_NOT_FOUND]: "No current handoff is available.",
3715
+ [import_shared18.ERROR_CODES.HANDOFF_EXPIRED]: "The handoff has expired.",
3716
+ [import_shared18.ERROR_CODES.HANDOFF_CURSOR_INVALID]: "The handoff cursor is invalid.",
3717
+ [import_shared18.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE]: "The handoff exceeds the size limit.",
3718
+ [import_shared18.ERROR_CODES.BRIDGE_UNAUTHORIZED]: "The local bridge request is unauthorized.",
3719
+ [import_shared18.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH]: "The local bridge protocol is incompatible.",
3720
+ [import_shared18.ERROR_CODES.BRIDGE_BUSY]: "The local bridge is busy.",
3721
+ [import_shared18.ERROR_CODES.EXTERNAL_AGENT_BUSY]: "The connected external Agent is busy.",
3722
+ [import_shared18.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT]: "Another active Agent adapter is connected.",
3723
+ [import_shared18.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID]: "The active Agent adapter lease is invalid.",
3724
+ [import_shared18.ERROR_CODES.ACTIVE_DISPATCH_INVALID]: "The active Agent dispatch transition is invalid.",
3725
+ [import_shared18.ERROR_CODES.SESSION_NOT_FOUND]: "No active SpotPatch session was found.",
3726
+ [import_shared18.ERROR_CODES.SESSION_AMBIGUOUS]: "More than one SpotPatch session matches.",
3727
+ [import_shared18.ERROR_CODES.SESSION_CLOSED]: "The SpotPatch session has closed.",
3728
+ [import_shared18.ERROR_CODES.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
3729
+ [import_shared18.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
3730
+ [import_shared18.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
3731
+ [import_shared18.ERROR_CODES.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
3732
+ [import_shared18.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
3733
+ [import_shared18.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
3734
+ [import_shared18.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
3735
+ [import_shared18.ERROR_CODES.TOOL_DENIED]: "The Agent tool request was denied.",
3736
+ [import_shared18.ERROR_CODES.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
3737
+ [import_shared18.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
3738
+ [import_shared18.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
3739
+ [import_shared18.ERROR_CODES.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
3740
+ [import_shared18.ERROR_CODES.PATCH_REJECTED]: "The proposed patch was rejected.",
3741
+ [import_shared18.ERROR_CODES.VALIDATION_FAILED]: "The proposed change failed validation.",
3742
+ [import_shared18.ERROR_CODES.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
3743
+ [import_shared18.ERROR_CODES.INTERNAL_ERROR]: "The request could not be completed."
2357
3744
  });
2358
- function writeJson(response, status, payload) {
3745
+ function writeJson2(response, status, payload) {
2359
3746
  response.statusCode = status;
2360
3747
  response.setHeader("Cache-Control", "no-store");
2361
3748
  response.setHeader("Content-Type", "application/json; charset=utf-8");
2362
3749
  response.end(JSON.stringify(payload));
2363
3750
  }
2364
3751
  function asSpotPatchError(error) {
2365
- return error instanceof import_shared10.SpotPatchError ? error : new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
3752
+ return error instanceof import_shared18.SpotPatchError ? error : new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
2366
3753
  }
2367
3754
  function writeError(response, error, logger) {
2368
3755
  const normalized = asSpotPatchError(error);
2369
- if (normalized.code === import_shared10.ERROR_CODES.INTERNAL_ERROR) {
3756
+ if (normalized.code === import_shared18.ERROR_CODES.INTERNAL_ERROR) {
2370
3757
  logger?.warn("[spotpatch:server] Internal request failure.");
2371
3758
  }
2372
- writeJson(response, STATUS_BY_ERROR[normalized.code], {
3759
+ writeJson2(response, STATUS_BY_ERROR[normalized.code], {
2373
3760
  ok: false,
2374
3761
  error: {
2375
3762
  code: normalized.code,
@@ -2385,11 +3772,11 @@ function requestPath(request) {
2385
3772
  }
2386
3773
  }
2387
3774
  async function handleSourceContext(request, options) {
2388
- const parsed = import_shared10.sourceContextRequestSchema.safeParse(
3775
+ const parsed = import_shared18.sourceContextRequestSchema.safeParse(
2389
3776
  await readJsonRequestBody(request)
2390
3777
  );
2391
3778
  if (!parsed.success) {
2392
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
3779
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
2393
3780
  }
2394
3781
  return readSourceContext({
2395
3782
  request: parsed.data,
@@ -2400,9 +3787,9 @@ async function handleSourceContext(request, options) {
2400
3787
  });
2401
3788
  }
2402
3789
  async function handleOpenEditor(request, options) {
2403
- const parsed = import_shared10.openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
3790
+ const parsed = import_shared18.openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
2404
3791
  if (!parsed.success) {
2405
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
3792
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
2406
3793
  }
2407
3794
  const body = parsed.data;
2408
3795
  const sourcePath = await resolveSourceFile({
@@ -2419,51 +3806,90 @@ async function handleOpenEditor(request, options) {
2419
3806
  options.logger?.warn(
2420
3807
  `[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
2421
3808
  );
2422
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.EDITOR_OPEN_FAILED, void 0, {
3809
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.EDITOR_OPEN_FAILED, void 0, {
2423
3810
  cause: error
2424
3811
  });
2425
3812
  }
2426
3813
  }
2427
3814
  function createSpotPatchMiddleware(options) {
2428
3815
  const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
3816
+ const dataFlowAnalyzer = createDataFlowAnalyzer(options);
2429
3817
  return (request, response, next) => {
2430
- const path8 = requestPath(request);
2431
- const agentRoute = matchAgentRequestPath(path8);
2432
- if (path8 !== import_shared10.SPOTPATCH_ENDPOINTS.sourceContext && path8 !== import_shared10.SPOTPATCH_ENDPOINTS.openEditor && agentRoute === void 0 && !path8.startsWith(`${import_shared10.SPOTPATCH_API_BASE}/`)) {
3818
+ const path9 = requestPath(request);
3819
+ const agentRoute = matchAgentRequestPath(path9);
3820
+ const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
3821
+ if (path9 !== import_shared18.SPOTPATCH_ENDPOINTS.sourceContext && path9 !== import_shared18.SPOTPATCH_ENDPOINTS.openEditor && path9 !== import_shared18.SPOTPATCH_ENDPOINTS.dataFlowComponentReport && path9 !== import_shared18.SPOTPATCH_ENDPOINTS.dataFlowPageReport && agentRoute === void 0 && externalHandoffRoute === void 0 && !path9.startsWith(`${import_shared18.SPOTPATCH_API_BASE}/`)) {
2433
3822
  next();
2434
3823
  return;
2435
3824
  }
2436
3825
  const handle = async () => {
2437
- if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
3826
+ if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
2438
3827
  const data = await readRuntimeBootstrap(
2439
3828
  request,
2440
3829
  bootstrap
2441
3830
  );
2442
- writeJson(response, 200, { ok: true, data });
3831
+ writeJson2(response, 200, { ok: true, data });
2443
3832
  return;
2444
3833
  }
2445
3834
  assertRequestAuthorized(request, {
2446
3835
  allowLan: options.options.allowLan,
2447
3836
  sessionToken: options.session.token
2448
3837
  });
2449
- if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.sourceContext) {
3838
+ if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.sourceContext) {
2450
3839
  if (request.method !== "POST") {
2451
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
3840
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
2452
3841
  }
2453
3842
  const data = await handleSourceContext(request, options);
2454
- writeJson(response, 200, { ok: true, data });
3843
+ writeJson2(response, 200, { ok: true, data });
2455
3844
  return;
2456
3845
  }
2457
- if (path8 === import_shared10.SPOTPATCH_ENDPOINTS.openEditor) {
3846
+ if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.openEditor) {
2458
3847
  if (request.method !== "POST") {
2459
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
3848
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
2460
3849
  }
2461
3850
  const data = await handleOpenEditor(request, options);
2462
- writeJson(response, 200, { ok: true, data });
3851
+ writeJson2(response, 200, { ok: true, data });
3852
+ return;
3853
+ }
3854
+ if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.dataFlowComponentReport) {
3855
+ if (request.method !== "POST") {
3856
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
3857
+ }
3858
+ const data = await handleComponentDataFlowReport(
3859
+ request,
3860
+ dataFlowAnalyzer,
3861
+ options
3862
+ );
3863
+ writeJson2(response, 200, { ok: true, data });
3864
+ return;
3865
+ }
3866
+ if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.dataFlowPageReport) {
3867
+ if (request.method !== "POST") {
3868
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
3869
+ }
3870
+ const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
3871
+ writeJson2(response, 200, { ok: true, data });
3872
+ return;
3873
+ }
3874
+ if (externalHandoffRoute !== void 0) {
3875
+ await handleExternalHandoffBrowserRequest(
3876
+ request,
3877
+ response,
3878
+ externalHandoffRoute,
3879
+ {
3880
+ options: options.options,
3881
+ registry: options.registry,
3882
+ root: options.root,
3883
+ ...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
3884
+ },
3885
+ (target, status, data) => {
3886
+ writeJson2(target, status, { ok: true, data });
3887
+ }
3888
+ );
2463
3889
  return;
2464
3890
  }
2465
3891
  if (agentRoute === void 0) {
2466
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
3892
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
2467
3893
  }
2468
3894
  await handleAgentRequest(
2469
3895
  request,
@@ -2471,7 +3897,7 @@ function createSpotPatchMiddleware(options) {
2471
3897
  options,
2472
3898
  agentRoute,
2473
3899
  (target, status, data) => {
2474
- writeJson(target, status, { ok: true, data });
3900
+ writeJson2(target, status, { ok: true, data });
2475
3901
  }
2476
3902
  );
2477
3903
  };
@@ -2482,9 +3908,9 @@ function createSpotPatchMiddleware(options) {
2482
3908
  }
2483
3909
 
2484
3910
  // src/server/source-registration.ts
2485
- var import_node_crypto5 = require("crypto");
2486
- var import_promises6 = require("fs/promises");
2487
- var import_node_path7 = __toESM(require("path"), 1);
3911
+ var import_node_crypto11 = require("crypto");
3912
+ var import_promises7 = require("fs/promises");
3913
+ var import_node_path8 = __toESM(require("path"), 1);
2488
3914
  var import_compiler = require("@spotpatch/compiler");
2489
3915
  var import_zod2 = require("zod");
2490
3916
  var REGISTRATION_BODY_LIMIT_BYTES = 4096;
@@ -2505,16 +3931,16 @@ function identitiesMatch(actual, expected) {
2505
3931
  }
2506
3932
  const actualBytes = Buffer.from(actual);
2507
3933
  const expectedBytes = Buffer.from(expected);
2508
- return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto5.timingSafeEqual)(actualBytes, expectedBytes);
3934
+ return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto11.timingSafeEqual)(actualBytes, expectedBytes);
2509
3935
  }
2510
3936
  function isWithinRoot(root, candidate) {
2511
- const relative = import_node_path7.default.relative(root, candidate);
2512
- return relative === "" || !relative.startsWith(`..${import_node_path7.default.sep}`) && relative !== ".." && !import_node_path7.default.isAbsolute(relative);
3937
+ const relative = import_node_path8.default.relative(root, candidate);
3938
+ return relative === "" || !relative.startsWith(`..${import_node_path8.default.sep}`) && relative !== ".." && !import_node_path8.default.isAbsolute(relative);
2513
3939
  }
2514
3940
  function hasForbiddenSegment(root, candidate) {
2515
- return import_node_path7.default.relative(root, candidate).split(import_node_path7.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
3941
+ return import_node_path8.default.relative(root, candidate).split(import_node_path8.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2516
3942
  }
2517
- function writeJson2(response, statusCode, payload) {
3943
+ function writeJson3(response, statusCode, payload) {
2518
3944
  const body = JSON.stringify(payload);
2519
3945
  response.statusCode = statusCode;
2520
3946
  response.setHeader("Cache-Control", "no-store");
@@ -2534,15 +3960,15 @@ function requestComesFromLoopbackWorker(request) {
2534
3960
  }
2535
3961
  }
2536
3962
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
2537
- if (!import_node_path7.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
3963
+ if (!import_node_path8.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2538
3964
  return void 0;
2539
3965
  }
2540
3966
  try {
2541
- const sourceStat = await (0, import_promises6.lstat)(requestedPath);
3967
+ const sourceStat = await (0, import_promises7.lstat)(requestedPath);
2542
3968
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
2543
3969
  return void 0;
2544
3970
  }
2545
- const resolvedPath = await (0, import_promises6.realpath)(requestedPath);
3971
+ const resolvedPath = await (0, import_promises7.realpath)(requestedPath);
2546
3972
  if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
2547
3973
  return void 0;
2548
3974
  }
@@ -2555,7 +3981,7 @@ async function createSourceRegistrationService(input) {
2555
3981
  if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
2556
3982
  throw new TypeError("The source registration identity is invalid.");
2557
3983
  }
2558
- const root = await (0, import_promises6.realpath)(input.root);
3984
+ const root = await (0, import_promises7.realpath)(input.root);
2559
3985
  const sourceFilter = (0, import_compiler.createSourceFilter)(root, input.options);
2560
3986
  const handler = (request, response) => {
2561
3987
  const handle = async () => {
@@ -2564,14 +3990,14 @@ async function createSourceRegistrationService(input) {
2564
3990
  getSingleHeader3(request, INTERNAL_SECRET_HEADER),
2565
3991
  input.internalSecret
2566
3992
  )) {
2567
- writeJson2(response, 403, { ok: false });
3993
+ writeJson3(response, 403, { ok: false });
2568
3994
  return;
2569
3995
  }
2570
3996
  const parsed = registrationRequestSchema.safeParse(
2571
3997
  await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
2572
3998
  );
2573
3999
  if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
2574
- writeJson2(response, 400, { ok: false });
4000
+ writeJson3(response, 400, { ok: false });
2575
4001
  return;
2576
4002
  }
2577
4003
  const sourcePath = await resolveAuthorizedSource(
@@ -2580,17 +4006,17 @@ async function createSourceRegistrationService(input) {
2580
4006
  (absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
2581
4007
  );
2582
4008
  if (sourcePath === void 0) {
2583
- writeJson2(response, 403, { ok: false });
4009
+ writeJson3(response, 403, { ok: false });
2584
4010
  return;
2585
4011
  }
2586
- writeJson2(response, 200, {
4012
+ writeJson3(response, 200, {
2587
4013
  epoch: input.registryEpoch,
2588
4014
  fileId: input.registry.register(sourcePath)
2589
4015
  });
2590
4016
  };
2591
4017
  void handle().catch(() => {
2592
4018
  if (!response.headersSent) {
2593
- writeJson2(response, 400, { ok: false });
4019
+ writeJson3(response, 400, { ok: false });
2594
4020
  } else {
2595
4021
  response.destroy();
2596
4022
  }
@@ -2600,11 +4026,11 @@ async function createSourceRegistrationService(input) {
2600
4026
  }
2601
4027
 
2602
4028
  // src/session/session.ts
2603
- var import_node_crypto6 = require("crypto");
4029
+ var import_node_crypto12 = require("crypto");
2604
4030
  function createSession() {
2605
4031
  return Object.freeze({
2606
- id: (0, import_node_crypto6.randomBytes)(16).toString("base64url"),
2607
- token: (0, import_node_crypto6.randomBytes)(16).toString("base64url")
4032
+ id: (0, import_node_crypto12.randomBytes)(16).toString("base64url"),
4033
+ token: (0, import_node_crypto12.randomBytes)(16).toString("base64url")
2608
4034
  });
2609
4035
  }
2610
4036
 
@@ -2614,8 +4040,10 @@ var OPTION_KEYS = Object.freeze([
2614
4040
  "allowLan",
2615
4041
  "budget",
2616
4042
  "debug",
4043
+ "dataFlow",
2617
4044
  "editor",
2618
4045
  "enabled",
4046
+ "externalAgent",
2619
4047
  "exclude",
2620
4048
  "include",
2621
4049
  "locale",
@@ -2701,8 +4129,12 @@ function serializeResolvedSpotPatchOptions(options) {
2701
4129
  allowLan: options.allowLan,
2702
4130
  budget: options.budget,
2703
4131
  debug: options.debug,
4132
+ dataFlow: options.dataFlow.enabled ? Object.freeze({
4133
+ runtime: options.dataFlow.runtime
4134
+ }) : false,
2704
4135
  editor: options.editor,
2705
4136
  enabled: options.enabled,
4137
+ externalAgent: options.externalAgent.enabled,
2706
4138
  exclude: Object.freeze(options.exclude.map(serializeFilter)),
2707
4139
  include: Object.freeze(options.include.map(serializeFilter)),
2708
4140
  locale: options.locale,
@@ -2743,11 +4175,20 @@ function parseBudget(value) {
2743
4175
  );
2744
4176
  return Object.freeze(budget);
2745
4177
  }
4178
+ function parseDataFlow(value) {
4179
+ if (value === false) return false;
4180
+ if (!isRecord2(value) || !hasExactKeys(value, ["runtime"]) || value.runtime !== "dispatch") {
4181
+ throw new TypeError("The SpotPatch data-flow transport is invalid.");
4182
+ }
4183
+ return Object.freeze({
4184
+ runtime: value.runtime
4185
+ });
4186
+ }
2746
4187
  function parseSerializedSpotPatchOptions(value) {
2747
4188
  if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
2748
4189
  throw new TypeError("The SpotPatch options transport is invalid.");
2749
4190
  }
2750
- 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)) {
4191
+ 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)) {
2751
4192
  throw new TypeError("The SpotPatch options transport is invalid.");
2752
4193
  }
2753
4194
  try {
@@ -2756,8 +4197,10 @@ function parseSerializedSpotPatchOptions(value) {
2756
4197
  allowLan: value.allowLan,
2757
4198
  budget: parseBudget(value.budget),
2758
4199
  debug: value.debug,
4200
+ dataFlow: parseDataFlow(value.dataFlow),
2759
4201
  editor: value.editor,
2760
4202
  enabled: value.enabled,
4203
+ externalAgent: value.externalAgent,
2761
4204
  exclude: parseFilterList(value.exclude),
2762
4205
  include: parseFilterList(value.include),
2763
4206
  locale: value.locale,
@@ -2777,8 +4220,10 @@ function parseSerializedSpotPatchOptions(value) {
2777
4220
  DEFAULT_OPTIONS,
2778
4221
  applyIntegrationPlan,
2779
4222
  createAgentJobManager,
4223
+ createExternalHandoffService,
2780
4224
  createIntegrationFileChange,
2781
4225
  createRuntimeAiConfig,
4226
+ createRuntimeDataFlowConfig,
2782
4227
  createSession,
2783
4228
  createSourceRegistrationService,
2784
4229
  createSourceRegistry,