@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.js CHANGED
@@ -472,6 +472,1103 @@ function createAgentJobManager(options) {
472
472
  });
473
473
  }
474
474
 
475
+ // src/external-handoff/service.ts
476
+ import {
477
+ ERROR_CODES as ERROR_CODES6,
478
+ EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION as EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION3,
479
+ EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION as EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION2,
480
+ SpotPatchError as SpotPatchError6
481
+ } from "@spotpatch/shared";
482
+ import { computeExternalHandoffProjectKey as computeExternalHandoffProjectKey2 } from "@spotpatch/shared/external-agent-node";
483
+
484
+ // src/external-handoff/active-registry.ts
485
+ import { randomBytes as randomBytes2 } from "crypto";
486
+ import {
487
+ ERROR_CODES as ERROR_CODES2,
488
+ EXTERNAL_HANDOFF_LIMITS,
489
+ SpotPatchError as SpotPatchError2
490
+ } from "@spotpatch/shared";
491
+
492
+ // src/external-handoff/clock.ts
493
+ import { performance } from "perf_hooks";
494
+ var SYSTEM_EXTERNAL_HANDOFF_CLOCK = Object.freeze({
495
+ monotonicNow: () => performance.now(),
496
+ wallNow: () => Date.now()
497
+ });
498
+
499
+ // src/external-handoff/active-registry.ts
500
+ var ALLOWED_TRANSITIONS = Object.freeze({
501
+ queued: ["dispatching", "failed"],
502
+ dispatching: ["dispatched", "working", "failed", "delivery-unknown"],
503
+ dispatched: ["working", "failed", "delivery-unknown"],
504
+ working: ["completed", "failed", "delivery-unknown"],
505
+ completed: [],
506
+ failed: [],
507
+ "delivery-unknown": []
508
+ });
509
+ var TERMINAL_PHASES = /* @__PURE__ */ new Set([
510
+ "completed",
511
+ "failed",
512
+ "delivery-unknown"
513
+ ]);
514
+ function defaultRandomId() {
515
+ return randomBytes2(32).toString("base64url");
516
+ }
517
+ function requirePresent(value) {
518
+ if (value === null) throw new SpotPatchError2(ERROR_CODES2.INTERNAL_ERROR);
519
+ return value;
520
+ }
521
+ function createActiveAdapterRegistry(options = {}) {
522
+ const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
523
+ const randomId = options.randomId ?? defaultRandomId;
524
+ let blocked;
525
+ let closed = false;
526
+ let dispatch;
527
+ let lastReleasedToken;
528
+ let lease;
529
+ const nowIso = () => new Date(clock.wallNow()).toISOString();
530
+ const requireOpen = () => {
531
+ if (closed) throw new SpotPatchError2(ERROR_CODES2.SESSION_CLOSED);
532
+ };
533
+ const dispatchSummary = () => dispatch === void 0 ? null : Object.freeze({
534
+ adapterKind: dispatch.adapterKind,
535
+ revision: dispatch.revision,
536
+ phase: dispatch.phase,
537
+ updatedAt: dispatch.updatedAt
538
+ });
539
+ const activeSummary = () => {
540
+ if (blocked !== void 0) {
541
+ return Object.freeze({
542
+ kind: blocked.adapterKind,
543
+ state: "blocked",
544
+ canDispatch: false,
545
+ connectedAt: blocked.connectedAt,
546
+ updatedAt: blocked.updatedAt
547
+ });
548
+ }
549
+ if (lease === void 0) return null;
550
+ const busy = dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase);
551
+ return Object.freeze({
552
+ kind: lease.adapterKind,
553
+ state: busy ? "busy" : "ready",
554
+ canDispatch: !busy,
555
+ connectedAt: lease.connectedAt,
556
+ updatedAt: lease.updatedAt
557
+ });
558
+ };
559
+ const state = (cursor) => Object.freeze({
560
+ activeAdapter: activeSummary(),
561
+ dispatch: cursor === void 0 || dispatch?.cursor === cursor ? dispatchSummary() : null
562
+ });
563
+ const enterUnknown = (activeLease) => {
564
+ if (dispatch === void 0) return;
565
+ const updatedAt = nowIso();
566
+ dispatch.phase = "delivery-unknown";
567
+ dispatch.updatedAt = updatedAt;
568
+ blocked = Object.freeze({
569
+ adapterKind: activeLease.adapterKind,
570
+ connectedAt: activeLease.connectedAt,
571
+ updatedAt
572
+ });
573
+ };
574
+ const endLease = (activeLease) => {
575
+ if (dispatch?.phase === "queued") {
576
+ dispatch.phase = "failed";
577
+ dispatch.updatedAt = nowIso();
578
+ } else if (dispatch?.phase === "dispatching" || dispatch?.phase === "dispatched" || dispatch?.phase === "working") {
579
+ enterUnknown(activeLease);
580
+ }
581
+ lastReleasedToken = activeLease.token;
582
+ lease = void 0;
583
+ };
584
+ const sweep = () => {
585
+ if (lease === void 0) return;
586
+ const monotonicNow = clock.monotonicNow();
587
+ if (monotonicNow >= lease.expiresAtMonotonic || dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase) && monotonicNow >= dispatch.deadlineMonotonic) {
588
+ endLease(lease);
589
+ }
590
+ };
591
+ const assertPublishable = () => {
592
+ requireOpen();
593
+ sweep();
594
+ if (blocked !== void 0 || lease !== void 0 && dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase)) {
595
+ throw new SpotPatchError2(ERROR_CODES2.EXTERNAL_AGENT_BUSY);
596
+ }
597
+ };
598
+ const requireLease = (leaseToken) => {
599
+ requireOpen();
600
+ sweep();
601
+ if (lease?.token !== leaseToken) {
602
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_LEASE_INVALID);
603
+ }
604
+ return lease;
605
+ };
606
+ return Object.freeze({
607
+ assertPublishable,
608
+ claim(adapterKind, connectorInstanceId, baselineCursor) {
609
+ requireOpen();
610
+ sweep();
611
+ if (blocked !== void 0) {
612
+ throw new SpotPatchError2(ERROR_CODES2.EXTERNAL_AGENT_BUSY);
613
+ }
614
+ if (lease !== void 0) {
615
+ if (lease.adapterKind === adapterKind && lease.connectorInstanceId === connectorInstanceId) {
616
+ lease.expiresAtMonotonic = clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
617
+ lease.updatedAt = nowIso();
618
+ return Object.freeze({
619
+ leaseToken: lease.token,
620
+ heartbeatIntervalMs: EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
621
+ baselineCursor: lease.baselineCursor,
622
+ activeAdapter: requirePresent(activeSummary())
623
+ });
624
+ }
625
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_CONFLICT);
626
+ }
627
+ const timestamp = nowIso();
628
+ lease = {
629
+ adapterKind,
630
+ baselineCursor,
631
+ connectedAt: timestamp,
632
+ connectorInstanceId,
633
+ token: randomId(),
634
+ expiresAtMonotonic: clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs,
635
+ updatedAt: timestamp
636
+ };
637
+ lastReleasedToken = void 0;
638
+ return Object.freeze({
639
+ leaseToken: lease.token,
640
+ heartbeatIntervalMs: EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
641
+ baselineCursor,
642
+ activeAdapter: requirePresent(activeSummary())
643
+ });
644
+ },
645
+ heartbeat(leaseToken) {
646
+ const activeLease = requireLease(leaseToken);
647
+ activeLease.expiresAtMonotonic = clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
648
+ activeLease.updatedAt = nowIso();
649
+ return state();
650
+ },
651
+ report(leaseToken, cursor, phase) {
652
+ const activeLease = requireLease(leaseToken);
653
+ if (dispatch?.cursor !== cursor || dispatch.adapterKind !== activeLease.adapterKind) {
654
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
655
+ }
656
+ if (dispatch.phase === phase) return state(cursor);
657
+ const allowed = ALLOWED_TRANSITIONS[dispatch.phase];
658
+ if (!allowed.includes(phase)) {
659
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
660
+ }
661
+ const updatedAt = nowIso();
662
+ dispatch.phase = phase;
663
+ dispatch.updatedAt = updatedAt;
664
+ activeLease.updatedAt = updatedAt;
665
+ if (phase === "delivery-unknown") {
666
+ blocked = Object.freeze({
667
+ adapterKind: activeLease.adapterKind,
668
+ connectedAt: activeLease.connectedAt,
669
+ updatedAt
670
+ });
671
+ lastReleasedToken = activeLease.token;
672
+ lease = void 0;
673
+ }
674
+ return state(cursor);
675
+ },
676
+ release(leaseToken) {
677
+ requireOpen();
678
+ sweep();
679
+ if (lease === void 0 && lastReleasedToken === leaseToken) return state();
680
+ const activeLease = lease;
681
+ if (activeLease?.token !== leaseToken) {
682
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_LEASE_INVALID);
683
+ }
684
+ endLease(activeLease);
685
+ return state();
686
+ },
687
+ reserve(cursor, revision) {
688
+ assertPublishable();
689
+ if (lease === void 0) return Object.freeze({ mode: "inbox" });
690
+ const updatedAt = nowIso();
691
+ dispatch = {
692
+ adapterKind: lease.adapterKind,
693
+ cursor,
694
+ deadlineMonotonic: clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeDispatchTimeoutMs,
695
+ revision,
696
+ phase: "queued",
697
+ updatedAt
698
+ };
699
+ lease.updatedAt = updatedAt;
700
+ return Object.freeze({
701
+ mode: "active",
702
+ adapter: requirePresent(activeSummary()),
703
+ dispatch: requirePresent(dispatchSummary())
704
+ });
705
+ },
706
+ resolveDelivery(cursor) {
707
+ requireOpen();
708
+ sweep();
709
+ const activeDispatch = dispatch;
710
+ if (blocked === void 0 || activeDispatch?.cursor !== cursor || activeDispatch.phase !== "delivery-unknown") {
711
+ throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
712
+ }
713
+ blocked = void 0;
714
+ return state(cursor);
715
+ },
716
+ snapshot(cursor) {
717
+ requireOpen();
718
+ sweep();
719
+ return state(cursor);
720
+ },
721
+ close() {
722
+ if (closed) return;
723
+ closed = true;
724
+ blocked = void 0;
725
+ dispatch = void 0;
726
+ lease = void 0;
727
+ lastReleasedToken = void 0;
728
+ }
729
+ });
730
+ }
731
+
732
+ // src/external-handoff/broker.ts
733
+ import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
734
+ import {
735
+ createServer
736
+ } from "http";
737
+ import {
738
+ ERROR_CODES as ERROR_CODES4,
739
+ EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
740
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS2,
741
+ SpotPatchError as SpotPatchError4
742
+ } from "@spotpatch/shared";
743
+ import {
744
+ SPOTPATCH_BRIDGE_PATHS,
745
+ SPOTPATCH_BRIDGE_TOKEN_HEADER,
746
+ bridgeAckRequestSchema,
747
+ bridgeActiveClaimRequestSchema,
748
+ bridgeActiveHeartbeatRequestSchema,
749
+ bridgeActiveReleaseRequestSchema,
750
+ bridgeActiveReportRequestSchema,
751
+ bridgeCurrentRequestSchema,
752
+ bridgeStatusRequestSchema,
753
+ bridgeWaitRequestSchema
754
+ } from "@spotpatch/shared/external-agent-node";
755
+
756
+ // src/server/request-body.ts
757
+ import { ERROR_CODES as ERROR_CODES3, SpotPatchError as SpotPatchError3 } from "@spotpatch/shared";
758
+
759
+ // src/server/constants.ts
760
+ var MAX_REQUEST_BODY_BYTES = 32 * 1024;
761
+ var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
762
+ var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
763
+
764
+ // src/server/request-body.ts
765
+ function isJsonContentType(value) {
766
+ return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
767
+ }
768
+ async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
769
+ if (!isJsonContentType(request.headers["content-type"])) {
770
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
771
+ }
772
+ const declaredLength = Number(request.headers["content-length"]);
773
+ if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
774
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
775
+ }
776
+ const chunks = [];
777
+ let byteLength = 0;
778
+ let exceededLimit = false;
779
+ for await (const rawChunk of request) {
780
+ const chunk = rawChunk;
781
+ if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
782
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
783
+ }
784
+ const buffer = Buffer.from(chunk);
785
+ byteLength += buffer.byteLength;
786
+ if (byteLength > maximumBytes) {
787
+ exceededLimit = true;
788
+ continue;
789
+ }
790
+ chunks.push(buffer);
791
+ }
792
+ if (exceededLimit || byteLength === 0) {
793
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
794
+ }
795
+ try {
796
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
797
+ } catch (error) {
798
+ throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST, void 0, {
799
+ cause: error
800
+ });
801
+ }
802
+ }
803
+
804
+ // src/external-handoff/broker.ts
805
+ function singleHeader(request, name) {
806
+ const value = request.headers[name.toLowerCase()];
807
+ return Array.isArray(value) ? void 0 : value;
808
+ }
809
+ function tokensMatch(actual, expected) {
810
+ if (actual === void 0) return false;
811
+ const actualBytes = Buffer.from(actual);
812
+ const expectedBytes = Buffer.from(expected);
813
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
814
+ }
815
+ function writeJson(response, status, payload) {
816
+ response.statusCode = status;
817
+ response.setHeader("Cache-Control", "no-store");
818
+ response.setHeader("Connection", "close");
819
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
820
+ response.setHeader("X-Content-Type-Options", "nosniff");
821
+ response.end(JSON.stringify(payload));
822
+ }
823
+ function statusForError(code) {
824
+ if (code === ERROR_CODES4.BRIDGE_UNAUTHORIZED) return 401;
825
+ if (code === ERROR_CODES4.HANDOFF_NOT_FOUND) return 404;
826
+ if (code === ERROR_CODES4.HANDOFF_EXPIRED || code === ERROR_CODES4.SESSION_CLOSED) {
827
+ return 410;
828
+ }
829
+ if (code === ERROR_CODES4.BRIDGE_BUSY) return 429;
830
+ if (code === ERROR_CODES4.ACTIVE_ADAPTER_LEASE_INVALID) return 401;
831
+ if (code === ERROR_CODES4.HANDOFF_RESPONSE_TOO_LARGE) return 413;
832
+ if (code === ERROR_CODES4.HANDOFF_CURSOR_INVALID || code === ERROR_CODES4.BRIDGE_PROTOCOL_MISMATCH || code === ERROR_CODES4.EXTERNAL_AGENT_BUSY || code === ERROR_CODES4.ACTIVE_ADAPTER_CONFLICT || code === ERROR_CODES4.ACTIVE_DISPATCH_INVALID) {
833
+ return 409;
834
+ }
835
+ if (code === ERROR_CODES4.INVALID_REQUEST) return 400;
836
+ return 500;
837
+ }
838
+ function normalizeError2(error) {
839
+ return error instanceof SpotPatchError4 ? error : new SpotPatchError4(ERROR_CODES4.INTERNAL_ERROR, void 0, { cause: error });
840
+ }
841
+ function assertAuthorized(request, expectedHost, bridgeToken) {
842
+ if (request.socket.remoteAddress !== "127.0.0.1" || singleHeader(request, "host") !== expectedHost || !tokensMatch(singleHeader(request, SPOTPATCH_BRIDGE_TOKEN_HEADER), bridgeToken)) {
843
+ throw new SpotPatchError4(ERROR_CODES4.BRIDGE_UNAUTHORIZED);
844
+ }
845
+ }
846
+ async function closeServer(server, sockets) {
847
+ await new Promise((resolve) => {
848
+ server.close(() => {
849
+ resolve();
850
+ });
851
+ for (const socket of sockets) {
852
+ socket.destroy();
853
+ }
854
+ });
855
+ }
856
+ async function createExternalHandoffBroker(options) {
857
+ const bridgeToken = randomBytes3(32).toString("base64url");
858
+ const sockets = /* @__PURE__ */ new Set();
859
+ let expectedHost = "";
860
+ const server = createServer(
861
+ { maxHeaderSize: EXTERNAL_HANDOFF_LIMITS2.maximumBrokerHeaderBytes },
862
+ (request, response) => {
863
+ const handle = async () => {
864
+ assertAuthorized(request, expectedHost, bridgeToken);
865
+ if (request.method !== "POST") {
866
+ throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
867
+ }
868
+ const body = await readJsonRequestBody(
869
+ request,
870
+ EXTERNAL_HANDOFF_LIMITS2.maximumBrokerRequestBytes
871
+ );
872
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.status) {
873
+ const parsed = bridgeStatusRequestSchema.safeParse(body);
874
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
875
+ let current = null;
876
+ try {
877
+ current = options.store.status();
878
+ } catch (error) {
879
+ if (!(error instanceof SpotPatchError4) || error.code !== ERROR_CODES4.HANDOFF_NOT_FOUND) {
880
+ throw error;
881
+ }
882
+ }
883
+ writeJson(response, 200, {
884
+ ok: true,
885
+ data: Object.freeze({
886
+ brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
887
+ projectKey: options.projectKey,
888
+ sessionId: options.sessionId,
889
+ framework: options.framework,
890
+ current
891
+ })
892
+ });
893
+ return;
894
+ }
895
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.current) {
896
+ const parsed = bridgeCurrentRequestSchema.safeParse(body);
897
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
898
+ const snapshot2 = options.store.current(parsed.data.cursor);
899
+ writeJson(response, 200, {
900
+ ok: true,
901
+ data: Object.freeze({ outcome: "handoff", snapshot: snapshot2 })
902
+ });
903
+ return;
904
+ }
905
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.ack) {
906
+ const parsed = bridgeAckRequestSchema.safeParse(body);
907
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
908
+ const summary = options.store.ack(
909
+ parsed.data.cursor,
910
+ parsed.data.connectorInstanceId
911
+ );
912
+ writeJson(response, 200, {
913
+ ok: true,
914
+ data: Object.freeze({ summary })
915
+ });
916
+ return;
917
+ }
918
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.wait) {
919
+ const parsed = bridgeWaitRequestSchema.safeParse(body);
920
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
921
+ const controller = new AbortController();
922
+ const abort = () => {
923
+ if (!response.writableEnded) controller.abort("bridge-client-closed");
924
+ };
925
+ response.once("close", abort);
926
+ try {
927
+ const data = await options.store.wait(
928
+ parsed.data.afterCursor,
929
+ parsed.data.timeoutMs,
930
+ controller.signal
931
+ );
932
+ writeJson(response, 200, { ok: true, data });
933
+ } finally {
934
+ response.removeListener("close", abort);
935
+ }
936
+ return;
937
+ }
938
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.activeClaim) {
939
+ const parsed = bridgeActiveClaimRequestSchema.safeParse(body);
940
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
941
+ const data = options.activeRegistry.claim(
942
+ parsed.data.adapterKind,
943
+ parsed.data.connectorInstanceId,
944
+ options.store.currentCursor()
945
+ );
946
+ writeJson(response, 200, { ok: true, data });
947
+ return;
948
+ }
949
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.activeHeartbeat) {
950
+ const parsed = bridgeActiveHeartbeatRequestSchema.safeParse(body);
951
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
952
+ const data = options.activeRegistry.heartbeat(parsed.data.leaseToken);
953
+ writeJson(response, 200, { ok: true, data });
954
+ return;
955
+ }
956
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.activeReport) {
957
+ const parsed = bridgeActiveReportRequestSchema.safeParse(body);
958
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
959
+ const data = options.activeRegistry.report(
960
+ parsed.data.leaseToken,
961
+ parsed.data.cursor,
962
+ parsed.data.phase
963
+ );
964
+ writeJson(response, 200, { ok: true, data });
965
+ return;
966
+ }
967
+ if (request.url === SPOTPATCH_BRIDGE_PATHS.activeRelease) {
968
+ const parsed = bridgeActiveReleaseRequestSchema.safeParse(body);
969
+ if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
970
+ const data = options.activeRegistry.release(parsed.data.leaseToken);
971
+ writeJson(response, 200, { ok: true, data });
972
+ return;
973
+ }
974
+ throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
975
+ };
976
+ void handle().catch((error) => {
977
+ if (response.writableEnded || response.destroyed) return;
978
+ const normalized = normalizeError2(error);
979
+ if (normalized.code === ERROR_CODES4.BRIDGE_BUSY) {
980
+ response.setHeader("Retry-After", "1");
981
+ }
982
+ writeJson(response, statusForError(normalized.code), {
983
+ ok: false,
984
+ error: {
985
+ code: normalized.code,
986
+ message: "The local SpotPatch bridge request failed."
987
+ }
988
+ });
989
+ });
990
+ }
991
+ );
992
+ server.maxConnections = EXTERNAL_HANDOFF_LIMITS2.maximumBrokerSockets;
993
+ server.headersTimeout = 5e3;
994
+ server.requestTimeout = EXTERNAL_HANDOFF_LIMITS2.maximumWaitMs + 5e3;
995
+ server.keepAliveTimeout = 1;
996
+ server.on("connection", (socket) => {
997
+ if (sockets.size >= EXTERNAL_HANDOFF_LIMITS2.maximumBrokerSockets) {
998
+ socket.destroy();
999
+ return;
1000
+ }
1001
+ sockets.add(socket);
1002
+ socket.once("close", () => sockets.delete(socket));
1003
+ });
1004
+ await new Promise((resolve, reject) => {
1005
+ server.once("error", reject);
1006
+ server.listen(0, "127.0.0.1", resolve);
1007
+ });
1008
+ const address = server.address();
1009
+ if (address === null || typeof address === "string" || address.address !== "127.0.0.1") {
1010
+ await closeServer(server, sockets);
1011
+ throw new Error("SpotPatch external Agent broker did not bind IPv4 loopback.");
1012
+ }
1013
+ expectedHost = `127.0.0.1:${String(address.port)}`;
1014
+ let closed = false;
1015
+ let ready = true;
1016
+ server.removeAllListeners("error");
1017
+ server.on("error", () => {
1018
+ ready = false;
1019
+ for (const socket of sockets) socket.destroy();
1020
+ });
1021
+ return Object.freeze({
1022
+ bridgeToken,
1023
+ endpoint: `http://${expectedHost}`,
1024
+ isReady: () => ready && !closed,
1025
+ async close() {
1026
+ if (closed) return;
1027
+ closed = true;
1028
+ ready = false;
1029
+ await closeServer(server, sockets);
1030
+ }
1031
+ });
1032
+ }
1033
+
1034
+ // src/external-handoff/discovery.ts
1035
+ import { randomBytes as randomBytes4 } from "crypto";
1036
+ import { lstat, open, rename, unlink } from "fs/promises";
1037
+ import path from "path";
1038
+ import {
1039
+ EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION as EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION2,
1040
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS3
1041
+ } from "@spotpatch/shared";
1042
+ import {
1043
+ computeExternalHandoffProjectKey,
1044
+ externalHandoffDescriptorSchema,
1045
+ resolveExternalHandoffRuntimeDirectory
1046
+ } from "@spotpatch/shared/external-agent-node";
1047
+ async function syncDirectory(directory) {
1048
+ const handle = await open(directory, "r");
1049
+ try {
1050
+ await handle.sync();
1051
+ } catch (error) {
1052
+ const code = error.code;
1053
+ if (code !== "EINVAL" && code !== "ENOTSUP") {
1054
+ throw error;
1055
+ }
1056
+ } finally {
1057
+ await handle.close();
1058
+ }
1059
+ }
1060
+ async function publishExternalHandoffDescriptor(options) {
1061
+ const directory = await resolveExternalHandoffRuntimeDirectory(true);
1062
+ const descriptor = externalHandoffDescriptorSchema.parse({
1063
+ schemaVersion: 1,
1064
+ brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION2,
1065
+ projectKey: await computeExternalHandoffProjectKey(options.root),
1066
+ sessionId: options.sessionId,
1067
+ framework: options.framework,
1068
+ endpoint: options.endpoint,
1069
+ bridgeToken: options.bridgeToken,
1070
+ pid: process.pid,
1071
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1072
+ });
1073
+ const serialized = JSON.stringify(descriptor);
1074
+ if (Buffer.byteLength(serialized, "utf8") > EXTERNAL_HANDOFF_LIMITS3.maximumDescriptorBytes) {
1075
+ throw new RangeError("SpotPatch external Agent descriptor exceeds its limit.");
1076
+ }
1077
+ const destination = path.join(directory, `${descriptor.sessionId}.json`);
1078
+ const temporary = path.join(
1079
+ directory,
1080
+ `.${descriptor.sessionId}.${randomBytes4(8).toString("hex")}.tmp`
1081
+ );
1082
+ let temporaryExists = false;
1083
+ let published = false;
1084
+ let descriptorIdentity = Object.freeze({
1085
+ device: -1,
1086
+ inode: -1
1087
+ });
1088
+ try {
1089
+ const handle = await open(temporary, "wx", 384);
1090
+ temporaryExists = true;
1091
+ try {
1092
+ await handle.writeFile(serialized, "utf8");
1093
+ await handle.sync();
1094
+ } finally {
1095
+ await handle.close();
1096
+ }
1097
+ await rename(temporary, destination);
1098
+ temporaryExists = false;
1099
+ published = true;
1100
+ const status = await lstat(destination);
1101
+ const uid = process.getuid?.();
1102
+ if (!status.isFile() || status.isSymbolicLink() || uid === void 0 || status.uid !== uid || (status.mode & 63) !== 0) {
1103
+ throw new Error("SpotPatch external Agent descriptor is not private.");
1104
+ }
1105
+ descriptorIdentity = Object.freeze({ device: status.dev, inode: status.ino });
1106
+ await syncDirectory(directory);
1107
+ } catch (error) {
1108
+ if (temporaryExists) {
1109
+ await unlink(temporary).catch(() => void 0);
1110
+ }
1111
+ if (published) {
1112
+ await unlink(destination).catch(() => void 0);
1113
+ }
1114
+ throw error;
1115
+ }
1116
+ let closed = false;
1117
+ return Object.freeze({
1118
+ descriptor,
1119
+ async close() {
1120
+ if (closed) return;
1121
+ closed = true;
1122
+ await lstat(destination).then(async (status) => {
1123
+ if (status.dev === descriptorIdentity.device && status.ino === descriptorIdentity.inode) {
1124
+ await unlink(destination);
1125
+ }
1126
+ }).catch((error) => {
1127
+ if (error.code !== "ENOENT") {
1128
+ throw error;
1129
+ }
1130
+ });
1131
+ await syncDirectory(directory);
1132
+ }
1133
+ });
1134
+ }
1135
+
1136
+ // src/external-handoff/fingerprint.ts
1137
+ import { createHash as createHash2 } from "crypto";
1138
+ function canonicalJson(value) {
1139
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
1140
+ return JSON.stringify(value);
1141
+ }
1142
+ if (typeof value === "number") {
1143
+ if (!Number.isFinite(value)) throw new TypeError("Non-finite JSON number.");
1144
+ return JSON.stringify(value);
1145
+ }
1146
+ if (Array.isArray(value)) {
1147
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
1148
+ }
1149
+ if (typeof value === "object") {
1150
+ const record = value;
1151
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
1152
+ }
1153
+ throw new TypeError("Unsupported JSON value.");
1154
+ }
1155
+ function fingerprintExternalHandoffAnnotation(annotation) {
1156
+ return createHash2("sha256").update(canonicalJson(annotation)).digest("hex");
1157
+ }
1158
+
1159
+ // src/external-handoff/store.ts
1160
+ import { randomBytes as randomBytes5 } from "crypto";
1161
+ import {
1162
+ ERROR_CODES as ERROR_CODES5,
1163
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS4,
1164
+ EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
1165
+ SpotPatchError as SpotPatchError5
1166
+ } from "@spotpatch/shared";
1167
+ function defaultRandomId2() {
1168
+ return randomBytes5(24).toString("base64url");
1169
+ }
1170
+ function pageSummary(annotation) {
1171
+ let origin = "[unavailable]";
1172
+ try {
1173
+ const url = new URL(annotation.page.url);
1174
+ origin = url.origin === "null" ? "[unavailable]" : url.origin;
1175
+ } catch {
1176
+ }
1177
+ return Object.freeze({ origin, pathname: annotation.page.pathname });
1178
+ }
1179
+ function summaryOf(current, state) {
1180
+ const snapshot2 = current.snapshot;
1181
+ return Object.freeze({
1182
+ sessionId: snapshot2.session.id,
1183
+ framework: snapshot2.session.framework,
1184
+ revision: snapshot2.revision,
1185
+ cursor: snapshot2.cursor,
1186
+ targetCount: snapshot2.annotation.targets.length,
1187
+ page: pageSummary(snapshot2.annotation),
1188
+ publishedAt: snapshot2.publishedAt,
1189
+ expiresAt: snapshot2.expiresAt,
1190
+ state,
1191
+ pickupCount: current.receipts.size,
1192
+ ...current.pickedUpAt === void 0 ? {} : { pickedUpAt: current.pickedUpAt }
1193
+ });
1194
+ }
1195
+ function replayResult(record) {
1196
+ return Object.freeze({ ...record.result, replayed: true });
1197
+ }
1198
+ function createExternalHandoffStore(options) {
1199
+ const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
1200
+ const randomId = options.randomId ?? defaultRandomId2;
1201
+ const history = [];
1202
+ const idempotency = /* @__PURE__ */ new Map();
1203
+ const waiters = /* @__PURE__ */ new Set();
1204
+ let closed = false;
1205
+ let current;
1206
+ let revision = 0;
1207
+ const requireOpen = () => {
1208
+ if (closed) throw new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED);
1209
+ };
1210
+ const archive = (state) => {
1211
+ if (current === void 0) return;
1212
+ history.unshift(summaryOf(current, state));
1213
+ history.length = Math.min(
1214
+ history.length,
1215
+ EXTERNAL_HANDOFF_LIMITS4.maximumHistorySummaries
1216
+ );
1217
+ current = void 0;
1218
+ };
1219
+ const sweep = () => {
1220
+ const monotonicNow = clock.monotonicNow();
1221
+ if (current !== void 0 && monotonicNow >= current.expiresAtMonotonic) {
1222
+ archive("expired");
1223
+ }
1224
+ for (const [requestId, record] of idempotency) {
1225
+ if (monotonicNow >= record.expiresAtMonotonic) idempotency.delete(requestId);
1226
+ }
1227
+ };
1228
+ const knownSummary = (cursor) => {
1229
+ sweep();
1230
+ if (current?.snapshot.cursor === cursor) {
1231
+ return summaryOf(current, "available");
1232
+ }
1233
+ return history.find((summary) => summary.cursor === cursor);
1234
+ };
1235
+ const readCurrent = (cursor) => {
1236
+ requireOpen();
1237
+ sweep();
1238
+ if (current === void 0) {
1239
+ const prior = cursor === void 0 ? void 0 : knownSummary(cursor);
1240
+ throw new SpotPatchError5(
1241
+ prior?.state === "expired" ? ERROR_CODES5.HANDOFF_EXPIRED : cursor === void 0 ? ERROR_CODES5.HANDOFF_NOT_FOUND : ERROR_CODES5.HANDOFF_CURSOR_INVALID
1242
+ );
1243
+ }
1244
+ if (cursor !== void 0 && cursor !== current.snapshot.cursor) {
1245
+ const prior = knownSummary(cursor);
1246
+ throw new SpotPatchError5(
1247
+ prior?.state === "expired" ? ERROR_CODES5.HANDOFF_EXPIRED : ERROR_CODES5.HANDOFF_CURSOR_INVALID
1248
+ );
1249
+ }
1250
+ return current.snapshot;
1251
+ };
1252
+ const findReplay = (requestId, fingerprint) => {
1253
+ requireOpen();
1254
+ sweep();
1255
+ const record = idempotency.get(requestId);
1256
+ if (record === void 0) return void 0;
1257
+ if (record.fingerprint !== fingerprint) {
1258
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_VALIDATION_FAILED);
1259
+ }
1260
+ return replayResult(record);
1261
+ };
1262
+ const settleWaiters = (result) => {
1263
+ const pending = [...waiters];
1264
+ waiters.clear();
1265
+ for (const waiter of pending) waiter.resolve(result);
1266
+ };
1267
+ return Object.freeze({
1268
+ activeWaitCount: () => waiters.size,
1269
+ replay: findReplay,
1270
+ publish(input) {
1271
+ requireOpen();
1272
+ const replayed = findReplay(input.requestId, input.fingerprint);
1273
+ if (replayed !== void 0) return replayed;
1274
+ if (idempotency.size >= EXTERNAL_HANDOFF_LIMITS4.maximumRequestIdRecords) {
1275
+ throw new SpotPatchError5(ERROR_CODES5.EXTERNAL_HANDOFF_UNAVAILABLE);
1276
+ }
1277
+ const nextRevision = revision + 1;
1278
+ const publishedAtMs = clock.wallNow();
1279
+ const publishedAtMonotonic = clock.monotonicNow();
1280
+ const snapshot2 = Object.freeze({
1281
+ schemaVersion: EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
1282
+ cursor: randomId(),
1283
+ session: Object.freeze({ id: options.sessionId, framework: options.framework }),
1284
+ revision: nextRevision,
1285
+ publishedAt: new Date(publishedAtMs).toISOString(),
1286
+ expiresAt: new Date(
1287
+ publishedAtMs + EXTERNAL_HANDOFF_LIMITS4.handoffTtlMs
1288
+ ).toISOString(),
1289
+ annotation: input.annotation
1290
+ });
1291
+ if (Buffer.byteLength(JSON.stringify(snapshot2), "utf8") > EXTERNAL_HANDOFF_LIMITS4.maximumSnapshotBytes) {
1292
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_RESPONSE_TOO_LARGE);
1293
+ }
1294
+ const delivery = input.reserve(snapshot2.cursor, nextRevision);
1295
+ if (current !== void 0) archive("superseded");
1296
+ revision = nextRevision;
1297
+ current = {
1298
+ expiresAtMonotonic: publishedAtMonotonic + EXTERNAL_HANDOFF_LIMITS4.handoffTtlMs,
1299
+ receipts: /* @__PURE__ */ new Set(),
1300
+ snapshot: snapshot2
1301
+ };
1302
+ const result = Object.freeze({
1303
+ handoff: summaryOf(current, "available"),
1304
+ delivery,
1305
+ replayed: false
1306
+ });
1307
+ idempotency.set(
1308
+ input.requestId,
1309
+ Object.freeze({
1310
+ expiresAtMonotonic: publishedAtMonotonic + EXTERNAL_HANDOFF_LIMITS4.requestIdTtlMs,
1311
+ fingerprint: input.fingerprint,
1312
+ result
1313
+ })
1314
+ );
1315
+ settleWaiters(Object.freeze({ outcome: "handoff", snapshot: snapshot2 }));
1316
+ return result;
1317
+ },
1318
+ current: readCurrent,
1319
+ currentCursor() {
1320
+ requireOpen();
1321
+ sweep();
1322
+ return current?.snapshot.cursor ?? null;
1323
+ },
1324
+ status(cursor) {
1325
+ requireOpen();
1326
+ sweep();
1327
+ if (cursor === void 0) {
1328
+ if (current === void 0) {
1329
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_NOT_FOUND);
1330
+ }
1331
+ return summaryOf(current, "available");
1332
+ }
1333
+ const summary = knownSummary(cursor);
1334
+ if (summary === void 0) {
1335
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
1336
+ }
1337
+ return summary;
1338
+ },
1339
+ ack(cursor, connectorInstanceId) {
1340
+ const snapshot2 = readCurrent(cursor);
1341
+ if (snapshot2 !== current?.snapshot) {
1342
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
1343
+ }
1344
+ if (!current.receipts.has(connectorInstanceId)) {
1345
+ if (current.receipts.size >= EXTERNAL_HANDOFF_LIMITS4.maximumConnectorReceipts) {
1346
+ throw new SpotPatchError5(ERROR_CODES5.BRIDGE_BUSY);
1347
+ }
1348
+ current.receipts.add(connectorInstanceId);
1349
+ current.pickedUpAt = new Date(clock.wallNow()).toISOString();
1350
+ }
1351
+ return summaryOf(current, "available");
1352
+ },
1353
+ async wait(afterCursor, timeoutMs, signal) {
1354
+ requireOpen();
1355
+ sweep();
1356
+ if (afterCursor === void 0 && current !== void 0) {
1357
+ return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
1358
+ }
1359
+ if (afterCursor !== void 0) {
1360
+ const known = knownSummary(afterCursor);
1361
+ if (known === void 0) {
1362
+ throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
1363
+ }
1364
+ if (current !== void 0 && current.snapshot.cursor !== afterCursor) {
1365
+ return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
1366
+ }
1367
+ }
1368
+ if (waiters.size >= EXTERNAL_HANDOFF_LIMITS4.maximumWaiters) {
1369
+ throw new SpotPatchError5(ERROR_CODES5.BRIDGE_BUSY);
1370
+ }
1371
+ if (signal.aborted) throw new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED);
1372
+ return new Promise((resolve, reject) => {
1373
+ let settled = false;
1374
+ const finish = () => {
1375
+ if (settled) return false;
1376
+ settled = true;
1377
+ waiters.delete(waiter);
1378
+ clearTimeout(timer);
1379
+ signal.removeEventListener("abort", abort);
1380
+ return true;
1381
+ };
1382
+ const waiter = {
1383
+ reject(error) {
1384
+ if (finish()) reject(error);
1385
+ },
1386
+ resolve(result) {
1387
+ if (finish()) resolve(result);
1388
+ }
1389
+ };
1390
+ const abort = () => {
1391
+ waiter.reject(new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED));
1392
+ };
1393
+ const timer = setTimeout(() => {
1394
+ waiter.resolve(Object.freeze({ outcome: "timeout" }));
1395
+ }, timeoutMs);
1396
+ timer.unref();
1397
+ signal.addEventListener("abort", abort, { once: true });
1398
+ waiters.add(waiter);
1399
+ });
1400
+ },
1401
+ close() {
1402
+ if (closed) return;
1403
+ closed = true;
1404
+ current = void 0;
1405
+ history.length = 0;
1406
+ idempotency.clear();
1407
+ const pending = [...waiters];
1408
+ waiters.clear();
1409
+ for (const waiter of pending) {
1410
+ waiter.reject(new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED));
1411
+ }
1412
+ }
1413
+ });
1414
+ }
1415
+
1416
+ // src/external-handoff/service.ts
1417
+ function asReplay(result) {
1418
+ return Object.freeze({ ...result, replayed: true });
1419
+ }
1420
+ function createExternalHandoffService(options) {
1421
+ const activeRegistry = createActiveAdapterRegistry();
1422
+ const store = createExternalHandoffStore({
1423
+ framework: options.framework,
1424
+ sessionId: options.sessionId
1425
+ });
1426
+ const inFlight = /* @__PURE__ */ new Map();
1427
+ let broker;
1428
+ let descriptor;
1429
+ let startPromise;
1430
+ let closePromise;
1431
+ let state = "idle";
1432
+ const isClosed = () => state === "closed";
1433
+ const requireReady = () => {
1434
+ if (state !== "ready" || broker?.isReady() !== true) {
1435
+ throw new SpotPatchError6(
1436
+ state === "closed" ? ERROR_CODES6.SESSION_CLOSED : ERROR_CODES6.EXTERNAL_HANDOFF_UNAVAILABLE
1437
+ );
1438
+ }
1439
+ };
1440
+ const start = async () => {
1441
+ if (state === "ready") return;
1442
+ if (state === "closed") throw new SpotPatchError6(ERROR_CODES6.SESSION_CLOSED);
1443
+ if (startPromise !== void 0) return startPromise;
1444
+ state = "starting";
1445
+ startPromise = (async () => {
1446
+ let createdBroker;
1447
+ let createdDescriptor;
1448
+ try {
1449
+ const projectKey = await computeExternalHandoffProjectKey2(options.root);
1450
+ createdBroker = await createExternalHandoffBroker({
1451
+ activeRegistry,
1452
+ framework: options.framework,
1453
+ projectKey,
1454
+ sessionId: options.sessionId,
1455
+ store
1456
+ });
1457
+ createdDescriptor = await publishExternalHandoffDescriptor({
1458
+ bridgeToken: createdBroker.bridgeToken,
1459
+ endpoint: createdBroker.endpoint,
1460
+ framework: options.framework,
1461
+ root: options.root,
1462
+ sessionId: options.sessionId
1463
+ });
1464
+ if (isClosed()) {
1465
+ await createdDescriptor.close();
1466
+ await createdBroker.close();
1467
+ throw new SpotPatchError6(ERROR_CODES6.SESSION_CLOSED);
1468
+ }
1469
+ broker = createdBroker;
1470
+ descriptor = createdDescriptor;
1471
+ state = "ready";
1472
+ } catch (error) {
1473
+ if (createdDescriptor !== void 0 && descriptor !== createdDescriptor) {
1474
+ await createdDescriptor.close().catch(() => void 0);
1475
+ }
1476
+ if (createdBroker !== void 0 && broker !== createdBroker) {
1477
+ await createdBroker.close().catch(() => void 0);
1478
+ }
1479
+ if (!isClosed()) state = "failed";
1480
+ throw error;
1481
+ }
1482
+ })();
1483
+ return startPromise;
1484
+ };
1485
+ return Object.freeze({
1486
+ start,
1487
+ capability() {
1488
+ const currentCursor = store.currentCursor();
1489
+ const active = activeRegistry.snapshot(currentCursor ?? void 0);
1490
+ return Object.freeze({
1491
+ enabled: true,
1492
+ brokerReady: state === "ready" && broker?.isReady() === true,
1493
+ activeWaitCount: store.activeWaitCount(),
1494
+ snapshotSchemaVersion: EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION2,
1495
+ brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION3,
1496
+ activeAdapter: active.activeAdapter,
1497
+ dispatch: currentCursor === null ? null : active.dispatch
1498
+ });
1499
+ },
1500
+ async publish(request, authorize) {
1501
+ requireReady();
1502
+ const fingerprint = fingerprintExternalHandoffAnnotation(request.annotation);
1503
+ const replayed = store.replay(request.requestId, fingerprint);
1504
+ if (replayed !== void 0) return replayed;
1505
+ const pending = inFlight.get(request.requestId);
1506
+ if (pending !== void 0) {
1507
+ if (pending.fingerprint !== fingerprint) {
1508
+ throw new SpotPatchError6(ERROR_CODES6.HANDOFF_VALIDATION_FAILED);
1509
+ }
1510
+ return asReplay(await pending.promise);
1511
+ }
1512
+ activeRegistry.assertPublishable();
1513
+ const promise = (async () => {
1514
+ const annotation = await authorize(request.annotation);
1515
+ return store.publish({
1516
+ annotation,
1517
+ fingerprint,
1518
+ requestId: request.requestId,
1519
+ reserve: activeRegistry.reserve
1520
+ });
1521
+ })();
1522
+ const activePublish = Object.freeze({ fingerprint, promise });
1523
+ inFlight.set(request.requestId, activePublish);
1524
+ try {
1525
+ return await promise;
1526
+ } finally {
1527
+ if (inFlight.get(request.requestId) === activePublish) {
1528
+ inFlight.delete(request.requestId);
1529
+ }
1530
+ }
1531
+ },
1532
+ status(cursor) {
1533
+ requireReady();
1534
+ const handoff = store.status(cursor);
1535
+ const active = activeRegistry.snapshot(cursor ?? handoff.cursor);
1536
+ return Object.freeze({
1537
+ handoff,
1538
+ activeAdapter: active.activeAdapter,
1539
+ dispatch: active.dispatch
1540
+ });
1541
+ },
1542
+ resolveDelivery(cursor) {
1543
+ requireReady();
1544
+ const handoff = store.status(cursor);
1545
+ const active = activeRegistry.resolveDelivery(cursor);
1546
+ return Object.freeze({
1547
+ handoff,
1548
+ activeAdapter: active.activeAdapter,
1549
+ dispatch: active.dispatch
1550
+ });
1551
+ },
1552
+ close() {
1553
+ closePromise ??= (async () => {
1554
+ if (state === "closed") return;
1555
+ state = "closed";
1556
+ activeRegistry.close();
1557
+ store.close();
1558
+ inFlight.clear();
1559
+ await startPromise?.catch(() => void 0);
1560
+ const publishedDescriptor = descriptor;
1561
+ descriptor = void 0;
1562
+ const activeBroker = broker;
1563
+ broker = void 0;
1564
+ await publishedDescriptor?.close().catch(() => void 0);
1565
+ await activeBroker?.close().catch(() => void 0);
1566
+ })();
1567
+ return closePromise;
1568
+ }
1569
+ });
1570
+ }
1571
+
475
1572
  // src/environment-ai.ts
476
1573
  var AI_ENVIRONMENT_NAMES = Object.freeze({
477
1574
  authentication: "SPOTPATCH_AI_AUTHENTICATION",
@@ -562,32 +1659,32 @@ function resolveEnvironmentAiConfiguration(environment) {
562
1659
  }
563
1660
 
564
1661
  // src/integration/file-plan.ts
565
- import { randomBytes as randomBytes2 } from "crypto";
1662
+ import { randomBytes as randomBytes6 } from "crypto";
566
1663
  import {
567
1664
  access,
568
- lstat,
1665
+ lstat as lstat2,
569
1666
  mkdir,
570
1667
  readFile,
571
1668
  realpath,
572
- rename,
1669
+ rename as rename2,
573
1670
  stat,
574
- unlink,
1671
+ unlink as unlink2,
575
1672
  writeFile
576
1673
  } from "fs/promises";
577
- import path from "path";
1674
+ import path2 from "path";
578
1675
  function isMissingPathError(error) {
579
1676
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
580
1677
  }
581
1678
  function isPathWithin(root, target) {
582
- const relative = path.relative(root, target);
583
- return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
1679
+ const relative = path2.relative(root, target);
1680
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
584
1681
  }
585
1682
  function relativePathWithin(root, target) {
586
- const relative = path.relative(root, target);
1683
+ const relative = path2.relative(root, target);
587
1684
  if (relative.length === 0 || !isPathWithin(root, target)) {
588
1685
  throw new Error("SpotPatch init refuses to modify a path outside the app root.");
589
1686
  }
590
- return relative.split(path.sep).join("/");
1687
+ return relative.split(path2.sep).join("/");
591
1688
  }
592
1689
  async function integrationPathExists(absolutePath) {
593
1690
  try {
@@ -598,10 +1695,10 @@ async function integrationPathExists(absolutePath) {
598
1695
  }
599
1696
  }
600
1697
  async function readIntegrationFile(absolutePath) {
601
- const metadata = await lstat(absolutePath);
1698
+ const metadata = await lstat2(absolutePath);
602
1699
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
603
1700
  throw new Error(
604
- `SpotPatch refuses to modify the non-regular file ${path.basename(absolutePath)}.`
1701
+ `SpotPatch refuses to modify the non-regular file ${path2.basename(absolutePath)}.`
605
1702
  );
606
1703
  }
607
1704
  return readFile(absolutePath, "utf8");
@@ -610,8 +1707,8 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
610
1707
  if (previousContent === nextContent) {
611
1708
  return void 0;
612
1709
  }
613
- const root = path.resolve(appRoot);
614
- const target = path.resolve(absolutePath);
1710
+ const root = path2.resolve(appRoot);
1711
+ const target = path2.resolve(absolutePath);
615
1712
  return Object.freeze({
616
1713
  absolutePath: target,
617
1714
  nextContent,
@@ -620,19 +1717,19 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
620
1717
  });
621
1718
  }
622
1719
  function temporaryPath(absolutePath, label) {
623
- return path.join(
624
- path.dirname(absolutePath),
625
- `.${path.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes2(8).toString("hex")}`
1720
+ return path2.join(
1721
+ path2.dirname(absolutePath),
1722
+ `.${path2.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes6(8).toString("hex")}`
626
1723
  );
627
1724
  }
628
1725
  async function writeAtomic(absolutePath, content, mode) {
629
- await mkdir(path.dirname(absolutePath), { recursive: true });
1726
+ await mkdir(path2.dirname(absolutePath), { recursive: true });
630
1727
  const stagedPath = temporaryPath(absolutePath, "stage");
631
1728
  try {
632
1729
  await writeFile(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
633
- await rename(stagedPath, absolutePath);
1730
+ await rename2(stagedPath, absolutePath);
634
1731
  } catch (error) {
635
- await unlink(stagedPath).catch(() => void 0);
1732
+ await unlink2(stagedPath).catch(() => void 0);
636
1733
  throw error;
637
1734
  }
638
1735
  }
@@ -644,21 +1741,21 @@ async function rollbackChange(change) {
644
1741
  );
645
1742
  }
646
1743
  if (change.previousContent === void 0) {
647
- await unlink(change.absolutePath);
1744
+ await unlink2(change.absolutePath);
648
1745
  return;
649
1746
  }
650
1747
  const mode = (await stat(change.absolutePath)).mode & 511;
651
1748
  await writeAtomic(change.absolutePath, change.previousContent, mode);
652
1749
  }
653
1750
  async function assertSafeTarget(appRoot, realAppRoot, change) {
654
- const target = path.resolve(change.absolutePath);
1751
+ const target = path2.resolve(change.absolutePath);
655
1752
  const relativePath = relativePathWithin(appRoot, target);
656
- if (target !== change.absolutePath || relativePath !== change.relativePath || path.dirname(target) === target) {
1753
+ if (target !== change.absolutePath || relativePath !== change.relativePath || path2.dirname(target) === target) {
657
1754
  throw new Error("SpotPatch init received an invalid integration file plan.");
658
1755
  }
659
1756
  let targetMetadata;
660
1757
  try {
661
- targetMetadata = await lstat(target);
1758
+ targetMetadata = await lstat2(target);
662
1759
  } catch (error) {
663
1760
  if (!isMissingPathError(error)) {
664
1761
  throw error;
@@ -670,7 +1767,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
670
1767
  );
671
1768
  }
672
1769
  const containmentAnchor = await realpath(
673
- targetMetadata === void 0 ? path.dirname(target) : target
1770
+ targetMetadata === void 0 ? path2.dirname(target) : target
674
1771
  );
675
1772
  if (!isPathWithin(realAppRoot, containmentAnchor)) {
676
1773
  throw new Error("SpotPatch init refuses to modify a path outside the app root.");
@@ -679,7 +1776,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
679
1776
  async function assertCurrentBaseline(change) {
680
1777
  if (change.previousContent === void 0) {
681
1778
  try {
682
- await lstat(change.absolutePath);
1779
+ await lstat2(change.absolutePath);
683
1780
  } catch (error) {
684
1781
  if (isMissingPathError(error)) {
685
1782
  return;
@@ -701,7 +1798,7 @@ async function applyIntegrationPlan(plan) {
701
1798
  if (plan.changes.length === 0) {
702
1799
  return;
703
1800
  }
704
- const appRoot = path.resolve(plan.appRoot);
1801
+ const appRoot = path2.resolve(plan.appRoot);
705
1802
  const realAppRoot = await realpath(appRoot);
706
1803
  const targets = /* @__PURE__ */ new Set();
707
1804
  for (const change of plan.changes) {
@@ -742,7 +1839,8 @@ import {
742
1839
  DEFAULT_AGENT_LIMITS,
743
1840
  MAX_ANNOTATION_TARGETS,
744
1841
  SPOTPATCH_EDITOR_PREFERENCES,
745
- SPOTPATCH_LOCALE_PREFERENCES
1842
+ SPOTPATCH_LOCALE_PREFERENCES,
1843
+ DEFAULT_DATA_FLOW_LIMITS
746
1844
  } from "@spotpatch/shared";
747
1845
  import { z } from "zod";
748
1846
  var DEFAULT_EXCLUDE = Object.freeze([
@@ -753,7 +1851,7 @@ var DEFAULT_EXCLUDE = Object.freeze([
753
1851
  /(?:^|\/)dist(?:\/|$)/,
754
1852
  /(?:^|\/)coverage(?:\/|$)/
755
1853
  ]);
756
- var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:jsx|tsx)$/]);
1854
+ var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:js|jsx|ts|tsx)$/]);
757
1855
  var DEFAULT_BUDGET = Object.freeze({
758
1856
  totalCharacters: 16e3,
759
1857
  domCharacters: 3e3,
@@ -774,7 +1872,13 @@ var DEFAULT_OPTIONS = Object.freeze({
774
1872
  debug: false,
775
1873
  locale: "auto",
776
1874
  maxTargets: 8,
777
- ai: false
1875
+ ai: false,
1876
+ dataFlow: Object.freeze({
1877
+ enabled: false,
1878
+ runtime: "dispatch",
1879
+ limits: DEFAULT_DATA_FLOW_LIMITS
1880
+ }),
1881
+ externalAgent: Object.freeze({ enabled: false })
778
1882
  });
779
1883
  var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
780
1884
  var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
@@ -1066,10 +2170,43 @@ function assertPositiveBudget(budget) {
1066
2170
  }
1067
2171
  }
1068
2172
  }
2173
+ function resolveDataFlowOptions(options) {
2174
+ if (options === void 0 || options === false) {
2175
+ return DEFAULT_OPTIONS.dataFlow;
2176
+ }
2177
+ const candidate = options;
2178
+ if (typeof candidate !== "object" || candidate === null) {
2179
+ throw new RangeError("SpotPatch dataFlow configuration is invalid.");
2180
+ }
2181
+ const runtime = options.runtime ?? "dispatch";
2182
+ if (runtime !== "dispatch") {
2183
+ throw new RangeError("SpotPatch dataFlow runtime mode is invalid.");
2184
+ }
2185
+ return Object.freeze({
2186
+ enabled: true,
2187
+ runtime,
2188
+ limits: DEFAULT_DATA_FLOW_LIMITS
2189
+ });
2190
+ }
2191
+ function createRuntimeDataFlowConfig(options) {
2192
+ return Object.freeze({
2193
+ enabled: options.enabled,
2194
+ runtime: options.runtime,
2195
+ limits: Object.freeze({
2196
+ observationMaxEntries: options.limits.observationMaxEntries,
2197
+ observationMaxBytes: options.limits.observationMaxBytes,
2198
+ observationTtlMs: options.limits.observationTtlMs,
2199
+ reportMaxBytes: options.limits.reportMaxBytes
2200
+ })
2201
+ });
2202
+ }
1069
2203
  function resolveOptions(options = {}, environmentAi) {
1070
2204
  if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
1071
2205
  throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
1072
2206
  }
2207
+ if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
2208
+ throw new RangeError("SpotPatch externalAgent must be a boolean.");
2209
+ }
1073
2210
  const budget = Object.freeze({
1074
2211
  ...DEFAULT_OPTIONS.budget,
1075
2212
  ...options.budget
@@ -1101,7 +2238,11 @@ function resolveOptions(options = {}, environmentAi) {
1101
2238
  debug: options.debug ?? DEFAULT_OPTIONS.debug,
1102
2239
  locale,
1103
2240
  maxTargets,
1104
- ai: resolveAiOptions(options.ai ?? environmentAi)
2241
+ ai: resolveAiOptions(options.ai ?? environmentAi),
2242
+ dataFlow: resolveDataFlowOptions(options.dataFlow),
2243
+ externalAgent: Object.freeze({
2244
+ enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
2245
+ })
1105
2246
  };
1106
2247
  if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
1107
2248
  throw new RangeError("SpotPatch shortcut is invalid.");
@@ -1111,9 +2252,9 @@ function resolveOptions(options = {}, environmentAi) {
1111
2252
 
1112
2253
  // src/project-validation.ts
1113
2254
  import { execFile } from "child_process";
1114
- import { access as access2, lstat as lstat2, readFile as readFile2, realpath as realpath2 } from "fs/promises";
2255
+ import { access as access2, lstat as lstat3, readFile as readFile2, realpath as realpath2 } from "fs/promises";
1115
2256
  import { createRequire } from "module";
1116
- import path2 from "path";
2257
+ import path3 from "path";
1117
2258
  import { promisify } from "util";
1118
2259
  var execFileAsync = promisify(execFile);
1119
2260
  var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
@@ -1123,14 +2264,14 @@ function isRecord(value) {
1123
2264
  }
1124
2265
  async function isRegularFile(absolutePath) {
1125
2266
  try {
1126
- const metadata = await lstat2(absolutePath);
2267
+ const metadata = await lstat3(absolutePath);
1127
2268
  return metadata.isFile() && !metadata.isSymbolicLink();
1128
2269
  } catch {
1129
2270
  return false;
1130
2271
  }
1131
2272
  }
1132
2273
  async function readManifest(appRoot) {
1133
- const manifestPath = path2.join(appRoot, "package.json");
2274
+ const manifestPath = path3.join(appRoot, "package.json");
1134
2275
  if (!await isRegularFile(manifestPath)) {
1135
2276
  return void 0;
1136
2277
  }
@@ -1159,8 +2300,8 @@ async function findGitRoot(appRoot) {
1159
2300
  windowsHide: true
1160
2301
  });
1161
2302
  const root = await realpath2(result.stdout.trim());
1162
- const relative = path2.relative(root, appRoot);
1163
- if (relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative)) {
2303
+ const relative = path3.relative(root, appRoot);
2304
+ if (relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative)) {
1164
2305
  return root;
1165
2306
  }
1166
2307
  } catch {
@@ -1169,10 +2310,10 @@ async function findGitRoot(appRoot) {
1169
2310
  return void 0;
1170
2311
  }
1171
2312
  async function resolveTypeScriptCli(appRoot) {
1172
- const resolveFromApplication = createRequire(path2.join(appRoot, "package.json"));
2313
+ const resolveFromApplication = createRequire(path3.join(appRoot, "package.json"));
1173
2314
  try {
1174
2315
  const packagePath = resolveFromApplication.resolve("typescript/package.json");
1175
- const cliPath = path2.join(path2.dirname(packagePath), "bin", "tsc");
2316
+ const cliPath = path3.join(path3.dirname(packagePath), "bin", "tsc");
1176
2317
  await access2(cliPath);
1177
2318
  return await realpath2(cliPath);
1178
2319
  } catch {
@@ -1180,11 +2321,11 @@ async function resolveTypeScriptCli(appRoot) {
1180
2321
  }
1181
2322
  }
1182
2323
  function portableRelativePath(from, to) {
1183
- return path2.relative(from, to).split(path2.sep).join("/");
2324
+ return path3.relative(from, to).split(path3.sep).join("/");
1184
2325
  }
1185
2326
  async function discoverProjectValidationCheck(options) {
1186
2327
  const appRoot = await realpath2(options.appRoot);
1187
- const tsconfigPath = path2.join(appRoot, "tsconfig.json");
2328
+ const tsconfigPath = path3.join(appRoot, "tsconfig.json");
1188
2329
  const [manifest, projectRoot, hasTsconfig] = await Promise.all([
1189
2330
  readManifest(appRoot),
1190
2331
  findGitRoot(appRoot),
@@ -1272,81 +2413,109 @@ async function resolveProjectOptions(input) {
1272
2413
  }
1273
2414
 
1274
2415
  // src/registry/source-registry.ts
1275
- import path3 from "path";
2416
+ import path4 from "path";
1276
2417
 
1277
2418
  // src/registry/source-id.ts
1278
- import { randomBytes as randomBytes3 } from "crypto";
2419
+ import { randomBytes as randomBytes7 } from "crypto";
1279
2420
  var SOURCE_ID_BYTES = 8;
1280
- var createRandomSourceId = () => randomBytes3(SOURCE_ID_BYTES).toString("base64url");
2421
+ var createRandomSourceId = () => randomBytes7(SOURCE_ID_BYTES).toString("base64url");
1281
2422
 
1282
2423
  // src/registry/source-registry.ts
1283
2424
  function normalizeAbsolutePath(absolutePath) {
1284
- return path3.normalize(path3.resolve(absolutePath));
2425
+ return path4.normalize(path4.resolve(absolutePath));
1285
2426
  }
1286
2427
  function createSourceRegistry(options = {}) {
1287
2428
  const createId = options.createId ?? createRandomSourceId;
1288
2429
  const pathToId = /* @__PURE__ */ new Map();
1289
2430
  const idToPath = /* @__PURE__ */ new Map();
2431
+ const componentAnchors = /* @__PURE__ */ new Map();
2432
+ const componentIdsByPath = /* @__PURE__ */ new Map();
2433
+ function registerSourcePath(absolutePath) {
2434
+ const normalizedPath = normalizeAbsolutePath(absolutePath);
2435
+ const existingId = pathToId.get(normalizedPath);
2436
+ if (existingId !== void 0) {
2437
+ return existingId;
2438
+ }
2439
+ let fileId = createId();
2440
+ while (idToPath.has(fileId)) fileId = createId();
2441
+ pathToId.set(normalizedPath, fileId);
2442
+ idToPath.set(fileId, normalizedPath);
2443
+ return fileId;
2444
+ }
1290
2445
  return Object.freeze({
1291
2446
  register(absolutePath) {
2447
+ return registerSourcePath(absolutePath);
2448
+ },
2449
+ registerDataFlowComponents(absolutePath, sourceVersion, components) {
1292
2450
  const normalizedPath = normalizeAbsolutePath(absolutePath);
1293
- const existingId = pathToId.get(normalizedPath);
1294
- if (existingId !== void 0) {
1295
- return existingId;
2451
+ const previousIds = componentIdsByPath.get(normalizedPath);
2452
+ for (const componentSourceId of previousIds ?? []) {
2453
+ componentAnchors.delete(componentSourceId);
1296
2454
  }
1297
- let fileId = createId();
1298
- while (idToPath.has(fileId)) {
1299
- fileId = createId();
2455
+ const fileId = registerSourcePath(normalizedPath);
2456
+ const currentIds = /* @__PURE__ */ new Set();
2457
+ for (const component of components) {
2458
+ currentIds.add(component.componentSourceId);
2459
+ componentAnchors.set(
2460
+ component.componentSourceId,
2461
+ Object.freeze({ ...component, fileId, sourceVersion })
2462
+ );
1300
2463
  }
1301
- pathToId.set(normalizedPath, fileId);
1302
- idToPath.set(fileId, normalizedPath);
1303
- return fileId;
2464
+ componentIdsByPath.set(normalizedPath, currentIds);
1304
2465
  },
1305
2466
  resolve(fileId) {
1306
2467
  return idToPath.get(fileId);
1307
2468
  },
2469
+ resolveDataFlowComponent(componentSourceId) {
2470
+ return componentAnchors.get(componentSourceId);
2471
+ },
1308
2472
  clear() {
1309
2473
  pathToId.clear();
1310
2474
  idToPath.clear();
2475
+ componentAnchors.clear();
2476
+ componentIdsByPath.clear();
1311
2477
  }
1312
2478
  });
1313
2479
  }
1314
2480
 
1315
2481
  // src/server/middleware.ts
1316
2482
  import {
1317
- ERROR_CODES as ERROR_CODES9,
2483
+ ERROR_CODES as ERROR_CODES15,
1318
2484
  SPOTPATCH_API_BASE,
1319
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
1320
- SpotPatchError as SpotPatchError9,
2485
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
2486
+ SpotPatchError as SpotPatchError15,
1321
2487
  openEditorRequestSchema,
1322
2488
  sourceContextRequestSchema
1323
2489
  } from "@spotpatch/shared";
1324
2490
 
1325
- // src/server/agent-http.ts
2491
+ // src/external-handoff/browser-http.ts
1326
2492
  import {
1327
- ERROR_CODES as ERROR_CODES6,
2493
+ ERROR_CODES as ERROR_CODES10,
2494
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
1328
2495
  SPOTPATCH_ENDPOINTS,
1329
- SpotPatchError as SpotPatchError6,
1330
- agentCapabilityRequestSchema,
1331
- agentJobActionRequestSchema,
1332
- agentJobCreateRequestSchema,
1333
- agentWorkspaceHealthRequestSchema
2496
+ SpotPatchError as SpotPatchError10,
2497
+ externalHandoffCapabilityRequestSchema,
2498
+ externalHandoffPublishRequestSchema,
2499
+ externalHandoffResolveDeliveryRequestSchema,
2500
+ externalHandoffStatusRequestSchema
1334
2501
  } from "@spotpatch/shared";
1335
2502
 
1336
- // src/server/agent-request.ts
2503
+ // src/server/annotation-authorizer.ts
1337
2504
  import { realpath as realpath5 } from "fs/promises";
1338
- import path6 from "path";
2505
+ import path7 from "path";
1339
2506
  import {
1340
- ERROR_CODES as ERROR_CODES4,
1341
- SpotPatchError as SpotPatchError4
2507
+ ERROR_CODES as ERROR_CODES9,
2508
+ SpotPatchError as SpotPatchError9,
2509
+ redactSensitiveText,
2510
+ sanitizeUrl
1342
2511
  } from "@spotpatch/shared";
1343
2512
 
1344
2513
  // src/server/source-context.ts
1345
2514
  import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
1346
- import path5 from "path";
2515
+ import path6 from "path";
1347
2516
  import {
1348
- ERROR_CODES as ERROR_CODES3,
1349
- SpotPatchError as SpotPatchError3
2517
+ ERROR_CODES as ERROR_CODES8,
2518
+ SpotPatchError as SpotPatchError8
1350
2519
  } from "@spotpatch/shared";
1351
2520
 
1352
2521
  // src/server/extract-code-context.ts
@@ -1580,15 +2749,8 @@ function extractCodeContext(options) {
1580
2749
 
1581
2750
  // src/server/source-file.ts
1582
2751
  import { realpath as realpath3, stat as stat2 } from "fs/promises";
1583
- import path4 from "path";
1584
- import { ERROR_CODES as ERROR_CODES2, SpotPatchError as SpotPatchError2 } from "@spotpatch/shared";
1585
-
1586
- // src/server/constants.ts
1587
- var MAX_REQUEST_BODY_BYTES = 32 * 1024;
1588
- var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
1589
- var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
1590
-
1591
- // src/server/source-file.ts
2752
+ import path5 from "path";
2753
+ import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
1592
2754
  var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
1593
2755
  function isMissingFileError(error) {
1594
2756
  return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
@@ -1603,51 +2765,51 @@ async function assertInsideRoot(root, candidate) {
1603
2765
  ]);
1604
2766
  } catch (error) {
1605
2767
  if (isMissingFileError(error)) {
1606
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
2768
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
1607
2769
  cause: error
1608
2770
  });
1609
2771
  }
1610
2772
  throw error;
1611
2773
  }
1612
- const relative = path4.relative(realRoot, realCandidate);
1613
- const outside = relative.startsWith(`..${path4.sep}`) || relative === ".." || path4.isAbsolute(relative);
2774
+ const relative = path5.relative(realRoot, realCandidate);
2775
+ const outside = relative.startsWith(`..${path5.sep}`) || relative === ".." || path5.isAbsolute(relative);
1614
2776
  if (outside) {
1615
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_OUTSIDE_ROOT);
2777
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
1616
2778
  }
1617
2779
  return realCandidate;
1618
2780
  }
1619
2781
  async function resolveSourceFile(options) {
1620
2782
  const registeredPath = options.registry.resolve(options.fileId);
1621
2783
  if (registeredPath === void 0) {
1622
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2784
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1623
2785
  }
1624
2786
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
1625
- if (!ALLOWED_EXTENSIONS.has(path4.extname(sourcePath).toLowerCase())) {
1626
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2787
+ if (!ALLOWED_EXTENSIONS.has(path5.extname(sourcePath).toLowerCase())) {
2788
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1627
2789
  }
1628
2790
  let sourceStat;
1629
2791
  try {
1630
2792
  sourceStat = await stat2(sourcePath);
1631
2793
  } catch (error) {
1632
2794
  if (isMissingFileError(error)) {
1633
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
2795
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
1634
2796
  cause: error
1635
2797
  });
1636
2798
  }
1637
2799
  throw error;
1638
2800
  }
1639
2801
  if (!sourceStat.isFile()) {
1640
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2802
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1641
2803
  }
1642
2804
  if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
1643
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_TOO_LARGE);
2805
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_TOO_LARGE);
1644
2806
  }
1645
2807
  return sourcePath;
1646
2808
  }
1647
2809
 
1648
2810
  // src/server/source-context.ts
1649
2811
  function toDisplayPath(root, sourcePath) {
1650
- return path5.relative(root, sourcePath).split(path5.sep).join("/");
2812
+ return path6.relative(root, sourcePath).split(path6.sep).join("/");
1651
2813
  }
1652
2814
  async function readSourceContext(options) {
1653
2815
  const sourcePath = await resolveSourceFile({
@@ -1660,7 +2822,7 @@ async function readSourceContext(options) {
1660
2822
  source = await readFile3(sourcePath, "utf8");
1661
2823
  } catch (error) {
1662
2824
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1663
- throw new SpotPatchError3(ERROR_CODES3.SOURCE_NOT_FOUND, void 0, {
2825
+ throw new SpotPatchError8(ERROR_CODES8.SOURCE_NOT_FOUND, void 0, {
1664
2826
  cause: error
1665
2827
  });
1666
2828
  }
@@ -1668,9 +2830,9 @@ async function readSourceContext(options) {
1668
2830
  }
1669
2831
  const lines = source.split(/\r?\n/);
1670
2832
  if (options.request.line > lines.length) {
1671
- throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
2833
+ throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
1672
2834
  }
1673
- const extension = path5.extname(sourcePath).toLowerCase();
2835
+ const extension = path6.extname(sourcePath).toLowerCase();
1674
2836
  return extractCodeContext({
1675
2837
  source,
1676
2838
  sourcePath,
@@ -1683,7 +2845,17 @@ async function readSourceContext(options) {
1683
2845
  });
1684
2846
  }
1685
2847
 
1686
- // src/server/agent-request.ts
2848
+ // src/server/annotation-authorizer.ts
2849
+ function sanitizePageContext(page) {
2850
+ return Object.freeze({
2851
+ url: sanitizeUrl(page.url, page.url),
2852
+ pathname: redactSensitiveText(page.pathname),
2853
+ title: redactSensitiveText(page.title),
2854
+ viewportWidth: page.viewportWidth,
2855
+ viewportHeight: page.viewportHeight,
2856
+ devicePixelRatio: page.devicePixelRatio
2857
+ });
2858
+ }
1687
2859
  function compactSourceRef(source) {
1688
2860
  return Object.freeze({
1689
2861
  origin: source.origin,
@@ -1697,27 +2869,20 @@ function compactSourceRef(source) {
1697
2869
  async function authorizeSourceRef(source, registry, root) {
1698
2870
  const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
1699
2871
  if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
1700
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2872
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1701
2873
  }
1702
2874
  if (source.fileId === void 0) {
1703
2875
  if (source.origin === "none" && source.relativePath !== void 0) {
1704
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2876
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1705
2877
  }
1706
2878
  return compactSourceRef(source);
1707
2879
  }
1708
- const sourcePath = await resolveSourceFile({
1709
- fileId: source.fileId,
1710
- registry,
1711
- root
1712
- });
1713
- const relativePath = path6.relative(await realpath5(root), sourcePath).split(path6.sep).join("/");
2880
+ const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
2881
+ const relativePath = path7.relative(await realpath5(root), sourcePath).split(path7.sep).join("/");
1714
2882
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
1715
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2883
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1716
2884
  }
1717
- return Object.freeze({
1718
- ...compactSourceRef(source),
1719
- relativePath
1720
- });
2885
+ return Object.freeze({ ...compactSourceRef(source), relativePath });
1721
2886
  }
1722
2887
  function freezeMatchedRule(rule) {
1723
2888
  return Object.freeze({
@@ -1751,7 +2916,7 @@ async function authorizeTarget(target, input) {
1751
2916
  maxLines: input.options.budget.maxCodeLines
1752
2917
  });
1753
2918
  if (marker === void 0 && target.code !== void 0) {
1754
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2919
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1755
2920
  }
1756
2921
  const code = marker === void 0 ? void 0 : await readSourceContext({
1757
2922
  request: marker,
@@ -1761,11 +2926,11 @@ async function authorizeTarget(target, input) {
1761
2926
  maxLines: input.options.budget.maxCodeLines
1762
2927
  });
1763
2928
  if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
1764
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2929
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1765
2930
  }
1766
2931
  return Object.freeze({
1767
2932
  instruction: target.instruction,
1768
- ...target.page === void 0 ? {} : { page: Object.freeze({ ...target.page }) },
2933
+ ...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
1769
2934
  source,
1770
2935
  react: Object.freeze({
1771
2936
  supported: target.react.supported,
@@ -1793,76 +2958,138 @@ async function authorizeTarget(target, input) {
1793
2958
  warnings: Object.freeze([...target.warnings])
1794
2959
  });
1795
2960
  }
1796
- async function authorizeAgentJobRequest(input) {
1797
- const requestedTargets = input.request.annotation.targets;
2961
+ async function authorizeAnnotation(input) {
2962
+ const requestedTargets = input.annotation.targets;
1798
2963
  if (requestedTargets.length > input.options.maxTargets) {
1799
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2964
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1800
2965
  }
1801
2966
  const identities = requestedTargets.map(targetIdentity);
1802
2967
  if (new Set(identities).size !== identities.length) {
1803
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2968
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1804
2969
  }
1805
2970
  const targets = Object.freeze(
1806
2971
  await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
1807
2972
  );
1808
- const annotation = Object.freeze({
2973
+ return Object.freeze({
1809
2974
  schemaVersion: 3,
1810
- id: input.request.annotation.id,
1811
- locale: input.request.annotation.locale,
1812
- page: Object.freeze({ ...input.request.annotation.page }),
2975
+ id: input.annotation.id,
2976
+ locale: input.annotation.locale,
2977
+ page: sanitizePageContext(input.annotation.page),
1813
2978
  targets,
1814
- createdAt: input.request.annotation.createdAt
1815
- });
1816
- return Object.freeze({
1817
- annotation,
1818
- ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
1819
- providerProfileId: input.request.providerProfileId,
1820
- modelProfileId: input.request.modelProfileId,
1821
- providerDataConsent: true,
1822
- ...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
1823
- workingTreeMode: input.request.workingTreeMode
2979
+ createdAt: input.annotation.createdAt
1824
2980
  });
1825
2981
  }
1826
2982
 
1827
- // src/server/request-body.ts
1828
- import { ERROR_CODES as ERROR_CODES5, SpotPatchError as SpotPatchError5 } from "@spotpatch/shared";
1829
- function isJsonContentType(value) {
1830
- return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
1831
- }
1832
- async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
1833
- if (!isJsonContentType(request.headers["content-type"])) {
1834
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
2983
+ // src/external-handoff/browser-http.ts
2984
+ function matchExternalHandoffBrowserPath(path9) {
2985
+ if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
2986
+ if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
2987
+ if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
2988
+ if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
2989
+ return "resolve-delivery";
1835
2990
  }
1836
- const declaredLength = Number(request.headers["content-length"]);
1837
- if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
1838
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
2991
+ return void 0;
2992
+ }
2993
+ function requireService(options) {
2994
+ if (!options.options.externalAgent.enabled || options.service === void 0) {
2995
+ throw new SpotPatchError10(ERROR_CODES10.EXTERNAL_HANDOFF_DISABLED);
1839
2996
  }
1840
- const chunks = [];
1841
- let byteLength = 0;
1842
- let exceededLimit = false;
1843
- for await (const rawChunk of request) {
1844
- const chunk = rawChunk;
1845
- if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
1846
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
2997
+ return options.service;
2998
+ }
2999
+ function remapAuthorizationError(error) {
3000
+ if (error instanceof SpotPatchError10) {
3001
+ if (error.code === ERROR_CODES10.SOURCE_NOT_FOUND || error.code === ERROR_CODES10.SOURCE_OUTSIDE_ROOT || error.code === ERROR_CODES10.SOURCE_TOO_LARGE) {
3002
+ throw new SpotPatchError10(ERROR_CODES10.HANDOFF_SOURCE_STALE, void 0, {
3003
+ cause: error
3004
+ });
1847
3005
  }
1848
- const buffer = Buffer.from(chunk);
1849
- byteLength += buffer.byteLength;
1850
- if (byteLength > maximumBytes) {
1851
- exceededLimit = true;
1852
- continue;
3006
+ if (error.code === ERROR_CODES10.INVALID_REQUEST) {
3007
+ throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED, void 0, {
3008
+ cause: error
3009
+ });
1853
3010
  }
1854
- chunks.push(buffer);
1855
3011
  }
1856
- if (exceededLimit || byteLength === 0) {
1857
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
3012
+ throw error;
3013
+ }
3014
+ async function handleExternalHandoffBrowserRequest(request, response, route, options, writeSuccess) {
3015
+ if (request.method !== "POST") {
3016
+ throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
1858
3017
  }
1859
- try {
1860
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1861
- } catch (error) {
1862
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST, void 0, {
1863
- cause: error
1864
- });
3018
+ const service = requireService(options);
3019
+ if (route === "capability") {
3020
+ const parsed2 = externalHandoffCapabilityRequestSchema.safeParse(
3021
+ await readJsonRequestBody(request)
3022
+ );
3023
+ if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3024
+ writeSuccess(response, 200, service.capability());
3025
+ return;
3026
+ }
3027
+ if (route === "status") {
3028
+ const parsed2 = externalHandoffStatusRequestSchema.safeParse(
3029
+ await readJsonRequestBody(request)
3030
+ );
3031
+ if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3032
+ writeSuccess(response, 200, service.status(parsed2.data.cursor));
3033
+ return;
3034
+ }
3035
+ if (route === "resolve-delivery") {
3036
+ const parsed2 = externalHandoffResolveDeliveryRequestSchema.safeParse(
3037
+ await readJsonRequestBody(request)
3038
+ );
3039
+ if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3040
+ writeSuccess(response, 200, service.resolveDelivery(parsed2.data.cursor));
3041
+ return;
3042
+ }
3043
+ const parsed = externalHandoffPublishRequestSchema.safeParse(
3044
+ await readJsonRequestBody(request, EXTERNAL_HANDOFF_LIMITS5.maximumPublishBodyBytes)
3045
+ );
3046
+ if (!parsed.success) {
3047
+ throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED);
1865
3048
  }
3049
+ const result = await service.publish(parsed.data, async (annotation) => {
3050
+ try {
3051
+ return await authorizeAnnotation({
3052
+ annotation,
3053
+ options: options.options,
3054
+ registry: options.registry,
3055
+ root: options.root
3056
+ });
3057
+ } catch (error) {
3058
+ remapAuthorizationError(error);
3059
+ }
3060
+ });
3061
+ writeSuccess(response, result.replayed ? 200 : 201, result);
3062
+ }
3063
+
3064
+ // src/server/agent-http.ts
3065
+ import {
3066
+ ERROR_CODES as ERROR_CODES11,
3067
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
3068
+ SpotPatchError as SpotPatchError11,
3069
+ agentCapabilityRequestSchema,
3070
+ agentJobActionRequestSchema,
3071
+ agentJobCreateRequestSchema,
3072
+ agentWorkspaceHealthRequestSchema
3073
+ } from "@spotpatch/shared";
3074
+
3075
+ // src/server/agent-request.ts
3076
+ import "@spotpatch/shared";
3077
+ async function authorizeAgentJobRequest(input) {
3078
+ const annotation = await authorizeAnnotation({
3079
+ annotation: input.request.annotation,
3080
+ options: input.options,
3081
+ registry: input.registry,
3082
+ root: input.root
3083
+ });
3084
+ return Object.freeze({
3085
+ annotation,
3086
+ ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
3087
+ providerProfileId: input.request.providerProfileId,
3088
+ modelProfileId: input.request.modelProfileId,
3089
+ providerDataConsent: true,
3090
+ ...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
3091
+ workingTreeMode: input.request.workingTreeMode
3092
+ });
1866
3093
  }
1867
3094
 
1868
3095
  // src/server/agent-http.ts
@@ -1882,21 +3109,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1882
3109
  "reverted",
1883
3110
  "failed"
1884
3111
  ]);
1885
- function matchAgentRequestPath(path8) {
1886
- if (path8 === SPOTPATCH_ENDPOINTS.agentCapability) {
3112
+ function matchAgentRequestPath(path9) {
3113
+ if (path9 === SPOTPATCH_ENDPOINTS2.agentCapability) {
1887
3114
  return Object.freeze({ kind: "capability" });
1888
3115
  }
1889
- if (path8 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
3116
+ if (path9 === SPOTPATCH_ENDPOINTS2.agentWorkspaceHealth) {
1890
3117
  return Object.freeze({ kind: "workspace-health" });
1891
3118
  }
1892
- if (path8 === SPOTPATCH_ENDPOINTS.agentJobs) {
3119
+ if (path9 === SPOTPATCH_ENDPOINTS2.agentJobs) {
1893
3120
  return Object.freeze({ kind: "create-job" });
1894
3121
  }
1895
- const prefix = `${SPOTPATCH_ENDPOINTS.agentJobs}/`;
1896
- if (!path8.startsWith(prefix)) {
3122
+ const prefix = `${SPOTPATCH_ENDPOINTS2.agentJobs}/`;
3123
+ if (!path9.startsWith(prefix)) {
1897
3124
  return void 0;
1898
3125
  }
1899
- const segments = path8.slice(prefix.length).split("/");
3126
+ const segments = path9.slice(prefix.length).split("/");
1900
3127
  const jobId = segments[0];
1901
3128
  const action = segments[1];
1902
3129
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1910,7 +3137,7 @@ function matchAgentRequestPath(path8) {
1910
3137
  }
1911
3138
  function requireAgentManager(options) {
1912
3139
  if (options.agentManager === void 0 || options.options.ai === false) {
1913
- throw new SpotPatchError6(ERROR_CODES6.AI_DISABLED);
3140
+ throw new SpotPatchError11(ERROR_CODES11.AI_DISABLED);
1914
3141
  }
1915
3142
  return options.agentManager;
1916
3143
  }
@@ -1963,13 +3190,13 @@ function streamAgentJobEvents(response, manager, jobId) {
1963
3190
  }
1964
3191
  async function handleCapability(request, response, options, writeSuccess) {
1965
3192
  if (request.method !== "POST") {
1966
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3193
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
1967
3194
  }
1968
3195
  const parsed = agentCapabilityRequestSchema.safeParse(
1969
3196
  await readJsonRequestBody(request)
1970
3197
  );
1971
3198
  if (!parsed.success) {
1972
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3199
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
1973
3200
  }
1974
3201
  const controller = new AbortController();
1975
3202
  const abort = () => {
@@ -1988,13 +3215,13 @@ async function handleCapability(request, response, options, writeSuccess) {
1988
3215
  }
1989
3216
  async function handleCreateJob(request, response, options, writeSuccess) {
1990
3217
  if (request.method !== "POST") {
1991
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3218
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
1992
3219
  }
1993
3220
  const parsed = agentJobCreateRequestSchema.safeParse(
1994
3221
  await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
1995
3222
  );
1996
3223
  if (!parsed.success) {
1997
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3224
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
1998
3225
  }
1999
3226
  const authorizedRequest = await authorizeAgentJobRequest({
2000
3227
  request: parsed.data,
@@ -2007,13 +3234,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
2007
3234
  }
2008
3235
  async function handleWorkspaceHealth(request, response, options, writeSuccess) {
2009
3236
  if (request.method !== "POST") {
2010
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3237
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2011
3238
  }
2012
3239
  const parsed = agentWorkspaceHealthRequestSchema.safeParse(
2013
3240
  await readJsonRequestBody(request)
2014
3241
  );
2015
3242
  if (!parsed.success) {
2016
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3243
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2017
3244
  }
2018
3245
  const controller = new AbortController();
2019
3246
  const abort = () => {
@@ -2030,13 +3257,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
2030
3257
  async function handleJobAction(request, response, options, route, writeSuccess) {
2031
3258
  const manager = requireAgentManager(options);
2032
3259
  if (request.method !== "POST") {
2033
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3260
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2034
3261
  }
2035
3262
  const parsed = agentJobActionRequestSchema.safeParse(
2036
3263
  await readJsonRequestBody(request)
2037
3264
  );
2038
3265
  if (!parsed.success) {
2039
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3266
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2040
3267
  }
2041
3268
  if (route.action === "events") {
2042
3269
  streamAgentJobEvents(response, manager, route.jobId);
@@ -2150,21 +3377,197 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
2150
3377
  }
2151
3378
  var launchConfiguredEditor = createEditorLauncher();
2152
3379
 
3380
+ // src/server/data-flow-http.ts
3381
+ import { createHash as createHash3 } from "crypto";
3382
+ import {
3383
+ createStaticDataFlowAnalyzer
3384
+ } from "@spotpatch/analyzer";
3385
+ import {
3386
+ DATA_FLOW_SCHEMA_VERSION,
3387
+ ERROR_CODES as ERROR_CODES12,
3388
+ SpotPatchError as SpotPatchError12,
3389
+ dataFlowComponentReportRequestSchema,
3390
+ dataFlowPageReportRequestSchema,
3391
+ limitDataFlowReportCollections
3392
+ } from "@spotpatch/shared";
3393
+ function envelopeBytes(report) {
3394
+ return Buffer.byteLength(JSON.stringify({ ok: true, data: report }), "utf8");
3395
+ }
3396
+ function limitDataFlowReportToBytes(report, maximumBytes) {
3397
+ const structurallyLimited = limitDataFlowReportCollections(report);
3398
+ if (envelopeBytes(structurallyLimited) <= maximumBytes) {
3399
+ return structurallyLimited;
3400
+ }
3401
+ let limited = limitDataFlowReportCollections(structurallyLimited, {
3402
+ forceTruncation: true,
3403
+ maximumDependencies: 0,
3404
+ truncatedBy: "bytes"
3405
+ });
3406
+ if (envelopeBytes(limited) > maximumBytes) {
3407
+ throw new SpotPatchError12(ERROR_CODES12.INTERNAL_ERROR);
3408
+ }
3409
+ for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
3410
+ const candidate = limitDataFlowReportCollections(structurallyLimited, {
3411
+ forceTruncation: true,
3412
+ maximumDependencies,
3413
+ truncatedBy: "bytes"
3414
+ });
3415
+ if (envelopeBytes(candidate) > maximumBytes) break;
3416
+ limited = candidate;
3417
+ }
3418
+ return limited;
3419
+ }
3420
+ function createDataFlowAnalyzer(options) {
3421
+ if (!options.options.dataFlow.enabled) return void 0;
3422
+ return createStaticDataFlowAnalyzer({
3423
+ root: options.root,
3424
+ registryEpoch: options.session.id,
3425
+ registerSource: (absolutePath) => options.registry.register(absolutePath),
3426
+ limits: options.options.dataFlow.limits
3427
+ });
3428
+ }
3429
+ async function analyzeTarget(request, analyzer, options) {
3430
+ const resolvedRequest = (() => {
3431
+ if ("componentSourceId" in request) {
3432
+ const anchor = options.registry.resolveDataFlowComponent(
3433
+ request.componentSourceId
3434
+ );
3435
+ if (anchor?.sourceVersion !== request.sourceVersion) {
3436
+ throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_SOURCE_STALE);
3437
+ }
3438
+ return anchor;
3439
+ }
3440
+ return request;
3441
+ })();
3442
+ const absolutePath = await resolveSourceFile({
3443
+ fileId: resolvedRequest.fileId,
3444
+ registry: options.registry,
3445
+ root: options.root
3446
+ });
3447
+ const report = analyzer.analyzeComponent({
3448
+ absolutePath,
3449
+ line: resolvedRequest.line,
3450
+ column: resolvedRequest.column
3451
+ });
3452
+ if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
3453
+ throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_SOURCE_STALE);
3454
+ }
3455
+ return limitDataFlowReportToBytes(
3456
+ report,
3457
+ options.options.dataFlow.limits.reportMaxBytes
3458
+ );
3459
+ }
3460
+ function requireAnalyzer(analyzer) {
3461
+ if (analyzer === void 0) {
3462
+ throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_DISABLED);
3463
+ }
3464
+ return analyzer;
3465
+ }
3466
+ async function handleComponentDataFlowReport(request, analyzer, options) {
3467
+ const parsed = dataFlowComponentReportRequestSchema.safeParse(
3468
+ await readJsonRequestBody(
3469
+ request,
3470
+ options.options.dataFlow.limits.protocolRequestMaxBytes
3471
+ )
3472
+ );
3473
+ if (!parsed.success) {
3474
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
3475
+ }
3476
+ return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
3477
+ }
3478
+ async function handlePageDataFlowReport(request, analyzer, options) {
3479
+ const parsed = dataFlowPageReportRequestSchema.safeParse(
3480
+ await readJsonRequestBody(
3481
+ request,
3482
+ options.options.dataFlow.limits.protocolRequestMaxBytes
3483
+ )
3484
+ );
3485
+ if (!parsed.success) {
3486
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
3487
+ }
3488
+ const activeAnalyzer = requireAnalyzer(analyzer);
3489
+ const componentReports = await Promise.all(
3490
+ parsed.data.targets.map((target) => analyzeTarget(target, activeAnalyzer, options))
3491
+ );
3492
+ const dependencies = new Map(
3493
+ componentReports.flatMap(
3494
+ (report2) => report2.dependencies.map((dependency) => [dependency.id, dependency])
3495
+ )
3496
+ );
3497
+ const evidence = new Map(
3498
+ componentReports.flatMap(
3499
+ (report2) => report2.evidence.map((entry) => [entry.id, entry])
3500
+ )
3501
+ );
3502
+ const diagnostics = new Map(
3503
+ componentReports.flatMap(
3504
+ (report2) => report2.diagnostics.map((entry) => [entry.id, entry])
3505
+ )
3506
+ );
3507
+ const analyzedVersions = new Set(
3508
+ componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
3509
+ );
3510
+ const reportId = `page_${createHash3("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
3511
+ const complete = componentReports.every((report2) => report2.completeness.complete);
3512
+ const report = Object.freeze({
3513
+ schemaVersion: DATA_FLOW_SCHEMA_VERSION,
3514
+ reportId,
3515
+ baseline: Object.freeze({
3516
+ registryEpoch: options.session.id,
3517
+ analyzerVersion: componentReports[0]?.baseline.analyzerVersion ?? "unavailable",
3518
+ adapterSetHash: componentReports[0]?.baseline.adapterSetHash ?? "unavailable",
3519
+ analyzedSourceVersions: Object.freeze([...analyzedVersions].sort())
3520
+ }),
3521
+ capability: Object.freeze({
3522
+ enabled: true,
3523
+ staticAnalysis: complete ? "available" : "partial",
3524
+ runtimeObservation: "dispatch-only",
3525
+ responseShape: "consumed-fields-only",
3526
+ aiAssistance: "disabled",
3527
+ reasons: Object.freeze(
3528
+ componentReports.flatMap((report2) => report2.capability.reasons)
3529
+ )
3530
+ }),
3531
+ dependencies: Object.freeze([...dependencies.values()]),
3532
+ evidence: Object.freeze([...evidence.values()]),
3533
+ diagnostics: Object.freeze([...diagnostics.values()]),
3534
+ completeness: Object.freeze({
3535
+ complete,
3536
+ visitedModules: componentReports.reduce(
3537
+ (total, report2) => total + report2.completeness.visitedModules,
3538
+ 0
3539
+ ),
3540
+ visitedCallsites: componentReports.reduce(
3541
+ (total, report2) => total + report2.completeness.visitedCallsites,
3542
+ 0
3543
+ ),
3544
+ frontierCount: componentReports.reduce(
3545
+ (total, report2) => total + report2.completeness.frontierCount,
3546
+ 0
3547
+ )
3548
+ })
3549
+ });
3550
+ return limitDataFlowReportToBytes(
3551
+ report,
3552
+ options.options.dataFlow.limits.reportMaxBytes
3553
+ );
3554
+ }
3555
+
2153
3556
  // src/server/request-security.ts
2154
- import { timingSafeEqual } from "crypto";
3557
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2155
3558
  import { isIP } from "net";
2156
- import { ERROR_CODES as ERROR_CODES7, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
3559
+ import { ERROR_CODES as ERROR_CODES13, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError13 } from "@spotpatch/shared";
2157
3560
  function getSingleHeader(request, name) {
2158
3561
  const value = request.headers[name.toLowerCase()];
2159
3562
  return Array.isArray(value) ? value[0] : value;
2160
3563
  }
2161
- function tokensMatch(actual, expected) {
3564
+ function tokensMatch2(actual, expected) {
2162
3565
  if (actual === void 0) {
2163
3566
  return false;
2164
3567
  }
2165
3568
  const actualBytes = Buffer.from(actual);
2166
3569
  const expectedBytes = Buffer.from(expected);
2167
- return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
3570
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
2168
3571
  }
2169
3572
  function isLoopbackHostname(hostname) {
2170
3573
  const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
@@ -2199,33 +3602,33 @@ function parseOrigin(value) {
2199
3602
  }
2200
3603
  function assertRequestAuthorized(request, options) {
2201
3604
  const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
2202
- if (!tokensMatch(actualToken, options.sessionToken)) {
2203
- throw new SpotPatchError7(ERROR_CODES7.INVALID_TOKEN);
3605
+ if (!tokensMatch2(actualToken, options.sessionToken)) {
3606
+ throw new SpotPatchError13(ERROR_CODES13.INVALID_TOKEN);
2204
3607
  }
2205
3608
  const hostHeader = getSingleHeader(request, "host");
2206
3609
  const originHeader = getSingleHeader(request, "origin");
2207
3610
  const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
2208
3611
  const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
2209
3612
  if (host === void 0 || origin === void 0) {
2210
- throw new SpotPatchError7(ERROR_CODES7.ORIGIN_NOT_ALLOWED);
3613
+ throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
2211
3614
  }
2212
3615
  const hostIsLoopback = isLoopbackHostname(host.hostname);
2213
3616
  const originIsLoopback = isLoopbackHostname(origin.hostname);
2214
3617
  if (!options.allowLan) {
2215
3618
  if (!hostIsLoopback || !originIsLoopback) {
2216
- throw new SpotPatchError7(ERROR_CODES7.ORIGIN_NOT_ALLOWED);
3619
+ throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
2217
3620
  }
2218
3621
  return;
2219
3622
  }
2220
3623
  if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
2221
- throw new SpotPatchError7(ERROR_CODES7.ORIGIN_NOT_ALLOWED);
3624
+ throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
2222
3625
  }
2223
3626
  }
2224
3627
 
2225
3628
  // src/server/runtime-bootstrap.ts
2226
3629
  import {
2227
- ERROR_CODES as ERROR_CODES8,
2228
- SpotPatchError as SpotPatchError8,
3630
+ ERROR_CODES as ERROR_CODES14,
3631
+ SpotPatchError as SpotPatchError14,
2229
3632
  runtimeBootstrapRequestSchema,
2230
3633
  runtimeConfigSchema
2231
3634
  } from "@spotpatch/shared";
@@ -2255,7 +3658,7 @@ function resolveRuntimeBootstrapOptions(options) {
2255
3658
  function assertRuntimeBootstrapRequest(request, expectedOrigin) {
2256
3659
  const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
2257
3660
  if (request.method !== "POST" || contentType !== "application/json") {
2258
- throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
3661
+ throw new SpotPatchError14(ERROR_CODES14.INVALID_REQUEST);
2259
3662
  }
2260
3663
  const host = getSingleHeader2(request, "host");
2261
3664
  let hostIsLoopback = false;
@@ -2267,7 +3670,7 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
2267
3670
  }
2268
3671
  }
2269
3672
  if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
2270
- throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
3673
+ throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
2271
3674
  }
2272
3675
  }
2273
3676
  async function readRuntimeBootstrap(request, options) {
@@ -2276,97 +3679,139 @@ async function readRuntimeBootstrap(request, options) {
2276
3679
  await readJsonRequestBody(request)
2277
3680
  );
2278
3681
  if (!parsedBody.success) {
2279
- throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
3682
+ throw new SpotPatchError14(ERROR_CODES14.INVALID_REQUEST);
2280
3683
  }
2281
3684
  return options.runtimeConfig;
2282
3685
  }
2283
3686
 
2284
3687
  // src/server/middleware.ts
2285
3688
  var STATUS_BY_ERROR = Object.freeze({
2286
- [ERROR_CODES9.INVALID_REQUEST]: 400,
2287
- [ERROR_CODES9.INVALID_TOKEN]: 401,
2288
- [ERROR_CODES9.ORIGIN_NOT_ALLOWED]: 403,
2289
- [ERROR_CODES9.SOURCE_NOT_FOUND]: 404,
2290
- [ERROR_CODES9.SOURCE_OUTSIDE_ROOT]: 403,
2291
- [ERROR_CODES9.SOURCE_TOO_LARGE]: 413,
2292
- [ERROR_CODES9.EDITOR_OPEN_FAILED]: 500,
2293
- [ERROR_CODES9.AI_DISABLED]: 404,
2294
- [ERROR_CODES9.PROVIDER_NOT_CONFIGURED]: 503,
2295
- [ERROR_CODES9.PROVIDER_AUTH_FAILED]: 502,
2296
- [ERROR_CODES9.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
2297
- [ERROR_CODES9.MODEL_NOT_ALLOWED]: 400,
2298
- [ERROR_CODES9.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
2299
- [ERROR_CODES9.PROVIDER_RATE_LIMITED]: 429,
2300
- [ERROR_CODES9.AGENT_BUSY]: 409,
2301
- [ERROR_CODES9.AGENT_LIMIT_EXCEEDED]: 413,
2302
- [ERROR_CODES9.AGENT_CANCELLED]: 409,
2303
- [ERROR_CODES9.WORKTREE_DIRTY]: 409,
2304
- [ERROR_CODES9.WORKTREE_NOT_REPOSITORY]: 409,
2305
- [ERROR_CODES9.WORKTREE_OPERATION_IN_PROGRESS]: 409,
2306
- [ERROR_CODES9.WORKTREE_CONFLICTED]: 409,
2307
- [ERROR_CODES9.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
2308
- [ERROR_CODES9.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
2309
- [ERROR_CODES9.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
2310
- [ERROR_CODES9.TOOL_DENIED]: 403,
2311
- [ERROR_CODES9.TOOL_INPUT_INVALID]: 422,
2312
- [ERROR_CODES9.TOOL_ARGUMENTS_INVALID]: 422,
2313
- [ERROR_CODES9.TOOL_CALL_ID_CONFLICT]: 422,
2314
- [ERROR_CODES9.TOOL_PATH_DENIED]: 403,
2315
- [ERROR_CODES9.PATCH_REJECTED]: 422,
2316
- [ERROR_CODES9.VALIDATION_FAILED]: 422,
2317
- [ERROR_CODES9.APPLY_CONFLICT]: 409,
2318
- [ERROR_CODES9.INTERNAL_ERROR]: 500
3689
+ [ERROR_CODES15.INVALID_REQUEST]: 400,
3690
+ [ERROR_CODES15.INVALID_TOKEN]: 401,
3691
+ [ERROR_CODES15.ORIGIN_NOT_ALLOWED]: 403,
3692
+ [ERROR_CODES15.SOURCE_NOT_FOUND]: 404,
3693
+ [ERROR_CODES15.SOURCE_OUTSIDE_ROOT]: 403,
3694
+ [ERROR_CODES15.SOURCE_TOO_LARGE]: 413,
3695
+ [ERROR_CODES15.EDITOR_OPEN_FAILED]: 500,
3696
+ [ERROR_CODES15.DATA_FLOW_DISABLED]: 404,
3697
+ [ERROR_CODES15.DATA_FLOW_SOURCE_STALE]: 409,
3698
+ [ERROR_CODES15.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
3699
+ [ERROR_CODES15.AI_DISABLED]: 404,
3700
+ [ERROR_CODES15.PROVIDER_NOT_CONFIGURED]: 503,
3701
+ [ERROR_CODES15.PROVIDER_AUTH_FAILED]: 502,
3702
+ [ERROR_CODES15.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
3703
+ [ERROR_CODES15.MODEL_NOT_ALLOWED]: 400,
3704
+ [ERROR_CODES15.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
3705
+ [ERROR_CODES15.PROVIDER_RATE_LIMITED]: 429,
3706
+ [ERROR_CODES15.AGENT_BUSY]: 409,
3707
+ [ERROR_CODES15.AGENT_LIMIT_EXCEEDED]: 413,
3708
+ [ERROR_CODES15.AGENT_CANCELLED]: 409,
3709
+ [ERROR_CODES15.EXTERNAL_HANDOFF_DISABLED]: 404,
3710
+ [ERROR_CODES15.EXTERNAL_HANDOFF_UNAVAILABLE]: 503,
3711
+ [ERROR_CODES15.HANDOFF_VALIDATION_FAILED]: 422,
3712
+ [ERROR_CODES15.HANDOFF_SOURCE_STALE]: 409,
3713
+ [ERROR_CODES15.HANDOFF_NOT_FOUND]: 404,
3714
+ [ERROR_CODES15.HANDOFF_EXPIRED]: 410,
3715
+ [ERROR_CODES15.HANDOFF_CURSOR_INVALID]: 409,
3716
+ [ERROR_CODES15.HANDOFF_RESPONSE_TOO_LARGE]: 413,
3717
+ [ERROR_CODES15.BRIDGE_UNAUTHORIZED]: 401,
3718
+ [ERROR_CODES15.BRIDGE_PROTOCOL_MISMATCH]: 409,
3719
+ [ERROR_CODES15.BRIDGE_BUSY]: 429,
3720
+ [ERROR_CODES15.EXTERNAL_AGENT_BUSY]: 409,
3721
+ [ERROR_CODES15.ACTIVE_ADAPTER_CONFLICT]: 409,
3722
+ [ERROR_CODES15.ACTIVE_ADAPTER_LEASE_INVALID]: 409,
3723
+ [ERROR_CODES15.ACTIVE_DISPATCH_INVALID]: 409,
3724
+ [ERROR_CODES15.SESSION_NOT_FOUND]: 404,
3725
+ [ERROR_CODES15.SESSION_AMBIGUOUS]: 409,
3726
+ [ERROR_CODES15.SESSION_CLOSED]: 410,
3727
+ [ERROR_CODES15.WORKTREE_DIRTY]: 409,
3728
+ [ERROR_CODES15.WORKTREE_NOT_REPOSITORY]: 409,
3729
+ [ERROR_CODES15.WORKTREE_OPERATION_IN_PROGRESS]: 409,
3730
+ [ERROR_CODES15.WORKTREE_CONFLICTED]: 409,
3731
+ [ERROR_CODES15.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
3732
+ [ERROR_CODES15.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
3733
+ [ERROR_CODES15.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
3734
+ [ERROR_CODES15.TOOL_DENIED]: 403,
3735
+ [ERROR_CODES15.TOOL_INPUT_INVALID]: 422,
3736
+ [ERROR_CODES15.TOOL_ARGUMENTS_INVALID]: 422,
3737
+ [ERROR_CODES15.TOOL_CALL_ID_CONFLICT]: 422,
3738
+ [ERROR_CODES15.TOOL_PATH_DENIED]: 403,
3739
+ [ERROR_CODES15.PATCH_REJECTED]: 422,
3740
+ [ERROR_CODES15.VALIDATION_FAILED]: 422,
3741
+ [ERROR_CODES15.APPLY_CONFLICT]: 409,
3742
+ [ERROR_CODES15.INTERNAL_ERROR]: 500
2319
3743
  });
2320
3744
  var PUBLIC_MESSAGES = Object.freeze({
2321
- [ERROR_CODES9.INVALID_REQUEST]: "The request is invalid.",
2322
- [ERROR_CODES9.INVALID_TOKEN]: "The session token is invalid.",
2323
- [ERROR_CODES9.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
2324
- [ERROR_CODES9.SOURCE_NOT_FOUND]: "The source file is unavailable.",
2325
- [ERROR_CODES9.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
2326
- [ERROR_CODES9.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
2327
- [ERROR_CODES9.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
2328
- [ERROR_CODES9.AI_DISABLED]: "AI execution is not enabled.",
2329
- [ERROR_CODES9.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
2330
- [ERROR_CODES9.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
2331
- [ERROR_CODES9.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
2332
- [ERROR_CODES9.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
2333
- [ERROR_CODES9.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
2334
- [ERROR_CODES9.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
2335
- [ERROR_CODES9.AGENT_BUSY]: "Another Agent job is already running.",
2336
- [ERROR_CODES9.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
2337
- [ERROR_CODES9.AGENT_CANCELLED]: "The Agent job was cancelled.",
2338
- [ERROR_CODES9.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
2339
- [ERROR_CODES9.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
2340
- [ERROR_CODES9.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
2341
- [ERROR_CODES9.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
2342
- [ERROR_CODES9.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
2343
- [ERROR_CODES9.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
2344
- [ERROR_CODES9.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
2345
- [ERROR_CODES9.TOOL_DENIED]: "The Agent tool request was denied.",
2346
- [ERROR_CODES9.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
2347
- [ERROR_CODES9.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
2348
- [ERROR_CODES9.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
2349
- [ERROR_CODES9.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
2350
- [ERROR_CODES9.PATCH_REJECTED]: "The proposed patch was rejected.",
2351
- [ERROR_CODES9.VALIDATION_FAILED]: "The proposed change failed validation.",
2352
- [ERROR_CODES9.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
2353
- [ERROR_CODES9.INTERNAL_ERROR]: "The request could not be completed."
3745
+ [ERROR_CODES15.INVALID_REQUEST]: "The request is invalid.",
3746
+ [ERROR_CODES15.INVALID_TOKEN]: "The session token is invalid.",
3747
+ [ERROR_CODES15.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
3748
+ [ERROR_CODES15.SOURCE_NOT_FOUND]: "The source file is unavailable.",
3749
+ [ERROR_CODES15.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
3750
+ [ERROR_CODES15.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
3751
+ [ERROR_CODES15.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
3752
+ [ERROR_CODES15.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
3753
+ [ERROR_CODES15.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
3754
+ [ERROR_CODES15.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
3755
+ [ERROR_CODES15.AI_DISABLED]: "AI execution is not enabled.",
3756
+ [ERROR_CODES15.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
3757
+ [ERROR_CODES15.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
3758
+ [ERROR_CODES15.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
3759
+ [ERROR_CODES15.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
3760
+ [ERROR_CODES15.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
3761
+ [ERROR_CODES15.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
3762
+ [ERROR_CODES15.AGENT_BUSY]: "Another Agent job is already running.",
3763
+ [ERROR_CODES15.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
3764
+ [ERROR_CODES15.AGENT_CANCELLED]: "The Agent job was cancelled.",
3765
+ [ERROR_CODES15.EXTERNAL_HANDOFF_DISABLED]: "External Agent handoff is not enabled.",
3766
+ [ERROR_CODES15.EXTERNAL_HANDOFF_UNAVAILABLE]: "External Agent handoff is temporarily unavailable.",
3767
+ [ERROR_CODES15.HANDOFF_VALIDATION_FAILED]: "The handoff content is invalid.",
3768
+ [ERROR_CODES15.HANDOFF_SOURCE_STALE]: "The selected source is stale.",
3769
+ [ERROR_CODES15.HANDOFF_NOT_FOUND]: "No current handoff is available.",
3770
+ [ERROR_CODES15.HANDOFF_EXPIRED]: "The handoff has expired.",
3771
+ [ERROR_CODES15.HANDOFF_CURSOR_INVALID]: "The handoff cursor is invalid.",
3772
+ [ERROR_CODES15.HANDOFF_RESPONSE_TOO_LARGE]: "The handoff exceeds the size limit.",
3773
+ [ERROR_CODES15.BRIDGE_UNAUTHORIZED]: "The local bridge request is unauthorized.",
3774
+ [ERROR_CODES15.BRIDGE_PROTOCOL_MISMATCH]: "The local bridge protocol is incompatible.",
3775
+ [ERROR_CODES15.BRIDGE_BUSY]: "The local bridge is busy.",
3776
+ [ERROR_CODES15.EXTERNAL_AGENT_BUSY]: "The connected external Agent is busy.",
3777
+ [ERROR_CODES15.ACTIVE_ADAPTER_CONFLICT]: "Another active Agent adapter is connected.",
3778
+ [ERROR_CODES15.ACTIVE_ADAPTER_LEASE_INVALID]: "The active Agent adapter lease is invalid.",
3779
+ [ERROR_CODES15.ACTIVE_DISPATCH_INVALID]: "The active Agent dispatch transition is invalid.",
3780
+ [ERROR_CODES15.SESSION_NOT_FOUND]: "No active SpotPatch session was found.",
3781
+ [ERROR_CODES15.SESSION_AMBIGUOUS]: "More than one SpotPatch session matches.",
3782
+ [ERROR_CODES15.SESSION_CLOSED]: "The SpotPatch session has closed.",
3783
+ [ERROR_CODES15.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
3784
+ [ERROR_CODES15.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
3785
+ [ERROR_CODES15.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
3786
+ [ERROR_CODES15.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
3787
+ [ERROR_CODES15.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
3788
+ [ERROR_CODES15.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
3789
+ [ERROR_CODES15.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
3790
+ [ERROR_CODES15.TOOL_DENIED]: "The Agent tool request was denied.",
3791
+ [ERROR_CODES15.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
3792
+ [ERROR_CODES15.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
3793
+ [ERROR_CODES15.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
3794
+ [ERROR_CODES15.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
3795
+ [ERROR_CODES15.PATCH_REJECTED]: "The proposed patch was rejected.",
3796
+ [ERROR_CODES15.VALIDATION_FAILED]: "The proposed change failed validation.",
3797
+ [ERROR_CODES15.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
3798
+ [ERROR_CODES15.INTERNAL_ERROR]: "The request could not be completed."
2354
3799
  });
2355
- function writeJson(response, status, payload) {
3800
+ function writeJson2(response, status, payload) {
2356
3801
  response.statusCode = status;
2357
3802
  response.setHeader("Cache-Control", "no-store");
2358
3803
  response.setHeader("Content-Type", "application/json; charset=utf-8");
2359
3804
  response.end(JSON.stringify(payload));
2360
3805
  }
2361
3806
  function asSpotPatchError(error) {
2362
- return error instanceof SpotPatchError9 ? error : new SpotPatchError9(ERROR_CODES9.INTERNAL_ERROR, void 0, { cause: error });
3807
+ return error instanceof SpotPatchError15 ? error : new SpotPatchError15(ERROR_CODES15.INTERNAL_ERROR, void 0, { cause: error });
2363
3808
  }
2364
3809
  function writeError(response, error, logger) {
2365
3810
  const normalized = asSpotPatchError(error);
2366
- if (normalized.code === ERROR_CODES9.INTERNAL_ERROR) {
3811
+ if (normalized.code === ERROR_CODES15.INTERNAL_ERROR) {
2367
3812
  logger?.warn("[spotpatch:server] Internal request failure.");
2368
3813
  }
2369
- writeJson(response, STATUS_BY_ERROR[normalized.code], {
3814
+ writeJson2(response, STATUS_BY_ERROR[normalized.code], {
2370
3815
  ok: false,
2371
3816
  error: {
2372
3817
  code: normalized.code,
@@ -2386,7 +3831,7 @@ async function handleSourceContext(request, options) {
2386
3831
  await readJsonRequestBody(request)
2387
3832
  );
2388
3833
  if (!parsed.success) {
2389
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3834
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2390
3835
  }
2391
3836
  return readSourceContext({
2392
3837
  request: parsed.data,
@@ -2399,7 +3844,7 @@ async function handleSourceContext(request, options) {
2399
3844
  async function handleOpenEditor(request, options) {
2400
3845
  const parsed = openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
2401
3846
  if (!parsed.success) {
2402
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3847
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2403
3848
  }
2404
3849
  const body = parsed.data;
2405
3850
  const sourcePath = await resolveSourceFile({
@@ -2416,51 +3861,90 @@ async function handleOpenEditor(request, options) {
2416
3861
  options.logger?.warn(
2417
3862
  `[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
2418
3863
  );
2419
- throw new SpotPatchError9(ERROR_CODES9.EDITOR_OPEN_FAILED, void 0, {
3864
+ throw new SpotPatchError15(ERROR_CODES15.EDITOR_OPEN_FAILED, void 0, {
2420
3865
  cause: error
2421
3866
  });
2422
3867
  }
2423
3868
  }
2424
3869
  function createSpotPatchMiddleware(options) {
2425
3870
  const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
3871
+ const dataFlowAnalyzer = createDataFlowAnalyzer(options);
2426
3872
  return (request, response, next) => {
2427
- const path8 = requestPath(request);
2428
- const agentRoute = matchAgentRequestPath(path8);
2429
- if (path8 !== SPOTPATCH_ENDPOINTS2.sourceContext && path8 !== SPOTPATCH_ENDPOINTS2.openEditor && agentRoute === void 0 && !path8.startsWith(`${SPOTPATCH_API_BASE}/`)) {
3873
+ const path9 = requestPath(request);
3874
+ const agentRoute = matchAgentRequestPath(path9);
3875
+ const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
3876
+ if (path9 !== SPOTPATCH_ENDPOINTS3.sourceContext && path9 !== SPOTPATCH_ENDPOINTS3.openEditor && path9 !== SPOTPATCH_ENDPOINTS3.dataFlowComponentReport && path9 !== SPOTPATCH_ENDPOINTS3.dataFlowPageReport && agentRoute === void 0 && externalHandoffRoute === void 0 && !path9.startsWith(`${SPOTPATCH_API_BASE}/`)) {
2430
3877
  next();
2431
3878
  return;
2432
3879
  }
2433
3880
  const handle = async () => {
2434
- if (path8 === SPOTPATCH_ENDPOINTS2.bootstrap && bootstrap !== void 0) {
3881
+ if (path9 === SPOTPATCH_ENDPOINTS3.bootstrap && bootstrap !== void 0) {
2435
3882
  const data = await readRuntimeBootstrap(
2436
3883
  request,
2437
3884
  bootstrap
2438
3885
  );
2439
- writeJson(response, 200, { ok: true, data });
3886
+ writeJson2(response, 200, { ok: true, data });
2440
3887
  return;
2441
3888
  }
2442
3889
  assertRequestAuthorized(request, {
2443
3890
  allowLan: options.options.allowLan,
2444
3891
  sessionToken: options.session.token
2445
3892
  });
2446
- if (path8 === SPOTPATCH_ENDPOINTS2.sourceContext) {
3893
+ if (path9 === SPOTPATCH_ENDPOINTS3.sourceContext) {
2447
3894
  if (request.method !== "POST") {
2448
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3895
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2449
3896
  }
2450
3897
  const data = await handleSourceContext(request, options);
2451
- writeJson(response, 200, { ok: true, data });
3898
+ writeJson2(response, 200, { ok: true, data });
2452
3899
  return;
2453
3900
  }
2454
- if (path8 === SPOTPATCH_ENDPOINTS2.openEditor) {
3901
+ if (path9 === SPOTPATCH_ENDPOINTS3.openEditor) {
2455
3902
  if (request.method !== "POST") {
2456
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3903
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2457
3904
  }
2458
3905
  const data = await handleOpenEditor(request, options);
2459
- writeJson(response, 200, { ok: true, data });
3906
+ writeJson2(response, 200, { ok: true, data });
3907
+ return;
3908
+ }
3909
+ if (path9 === SPOTPATCH_ENDPOINTS3.dataFlowComponentReport) {
3910
+ if (request.method !== "POST") {
3911
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
3912
+ }
3913
+ const data = await handleComponentDataFlowReport(
3914
+ request,
3915
+ dataFlowAnalyzer,
3916
+ options
3917
+ );
3918
+ writeJson2(response, 200, { ok: true, data });
3919
+ return;
3920
+ }
3921
+ if (path9 === SPOTPATCH_ENDPOINTS3.dataFlowPageReport) {
3922
+ if (request.method !== "POST") {
3923
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
3924
+ }
3925
+ const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
3926
+ writeJson2(response, 200, { ok: true, data });
3927
+ return;
3928
+ }
3929
+ if (externalHandoffRoute !== void 0) {
3930
+ await handleExternalHandoffBrowserRequest(
3931
+ request,
3932
+ response,
3933
+ externalHandoffRoute,
3934
+ {
3935
+ options: options.options,
3936
+ registry: options.registry,
3937
+ root: options.root,
3938
+ ...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
3939
+ },
3940
+ (target, status, data) => {
3941
+ writeJson2(target, status, { ok: true, data });
3942
+ }
3943
+ );
2460
3944
  return;
2461
3945
  }
2462
3946
  if (agentRoute === void 0) {
2463
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3947
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2464
3948
  }
2465
3949
  await handleAgentRequest(
2466
3950
  request,
@@ -2468,7 +3952,7 @@ function createSpotPatchMiddleware(options) {
2468
3952
  options,
2469
3953
  agentRoute,
2470
3954
  (target, status, data) => {
2471
- writeJson(target, status, { ok: true, data });
3955
+ writeJson2(target, status, { ok: true, data });
2472
3956
  }
2473
3957
  );
2474
3958
  };
@@ -2479,9 +3963,9 @@ function createSpotPatchMiddleware(options) {
2479
3963
  }
2480
3964
 
2481
3965
  // src/server/source-registration.ts
2482
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2483
- import { lstat as lstat3, realpath as realpath6 } from "fs/promises";
2484
- import path7 from "path";
3966
+ import { timingSafeEqual as timingSafeEqual3 } from "crypto";
3967
+ import { lstat as lstat4, realpath as realpath6 } from "fs/promises";
3968
+ import path8 from "path";
2485
3969
  import { createSourceFilter } from "@spotpatch/compiler";
2486
3970
  import { z as z2 } from "zod";
2487
3971
  var REGISTRATION_BODY_LIMIT_BYTES = 4096;
@@ -2502,16 +3986,16 @@ function identitiesMatch(actual, expected) {
2502
3986
  }
2503
3987
  const actualBytes = Buffer.from(actual);
2504
3988
  const expectedBytes = Buffer.from(expected);
2505
- return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
3989
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
2506
3990
  }
2507
3991
  function isWithinRoot(root, candidate) {
2508
- const relative = path7.relative(root, candidate);
2509
- return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
3992
+ const relative = path8.relative(root, candidate);
3993
+ return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
2510
3994
  }
2511
3995
  function hasForbiddenSegment(root, candidate) {
2512
- return path7.relative(root, candidate).split(path7.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
3996
+ return path8.relative(root, candidate).split(path8.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
2513
3997
  }
2514
- function writeJson2(response, statusCode, payload) {
3998
+ function writeJson3(response, statusCode, payload) {
2515
3999
  const body = JSON.stringify(payload);
2516
4000
  response.statusCode = statusCode;
2517
4001
  response.setHeader("Cache-Control", "no-store");
@@ -2531,11 +4015,11 @@ function requestComesFromLoopbackWorker(request) {
2531
4015
  }
2532
4016
  }
2533
4017
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
2534
- if (!path7.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
4018
+ if (!path8.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2535
4019
  return void 0;
2536
4020
  }
2537
4021
  try {
2538
- const sourceStat = await lstat3(requestedPath);
4022
+ const sourceStat = await lstat4(requestedPath);
2539
4023
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
2540
4024
  return void 0;
2541
4025
  }
@@ -2561,14 +4045,14 @@ async function createSourceRegistrationService(input) {
2561
4045
  getSingleHeader3(request, INTERNAL_SECRET_HEADER),
2562
4046
  input.internalSecret
2563
4047
  )) {
2564
- writeJson2(response, 403, { ok: false });
4048
+ writeJson3(response, 403, { ok: false });
2565
4049
  return;
2566
4050
  }
2567
4051
  const parsed = registrationRequestSchema.safeParse(
2568
4052
  await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
2569
4053
  );
2570
4054
  if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
2571
- writeJson2(response, 400, { ok: false });
4055
+ writeJson3(response, 400, { ok: false });
2572
4056
  return;
2573
4057
  }
2574
4058
  const sourcePath = await resolveAuthorizedSource(
@@ -2577,17 +4061,17 @@ async function createSourceRegistrationService(input) {
2577
4061
  (absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
2578
4062
  );
2579
4063
  if (sourcePath === void 0) {
2580
- writeJson2(response, 403, { ok: false });
4064
+ writeJson3(response, 403, { ok: false });
2581
4065
  return;
2582
4066
  }
2583
- writeJson2(response, 200, {
4067
+ writeJson3(response, 200, {
2584
4068
  epoch: input.registryEpoch,
2585
4069
  fileId: input.registry.register(sourcePath)
2586
4070
  });
2587
4071
  };
2588
4072
  void handle().catch(() => {
2589
4073
  if (!response.headersSent) {
2590
- writeJson2(response, 400, { ok: false });
4074
+ writeJson3(response, 400, { ok: false });
2591
4075
  } else {
2592
4076
  response.destroy();
2593
4077
  }
@@ -2597,11 +4081,11 @@ async function createSourceRegistrationService(input) {
2597
4081
  }
2598
4082
 
2599
4083
  // src/session/session.ts
2600
- import { randomBytes as randomBytes4 } from "crypto";
4084
+ import { randomBytes as randomBytes8 } from "crypto";
2601
4085
  function createSession() {
2602
4086
  return Object.freeze({
2603
- id: randomBytes4(16).toString("base64url"),
2604
- token: randomBytes4(16).toString("base64url")
4087
+ id: randomBytes8(16).toString("base64url"),
4088
+ token: randomBytes8(16).toString("base64url")
2605
4089
  });
2606
4090
  }
2607
4091
 
@@ -2611,8 +4095,10 @@ var OPTION_KEYS = Object.freeze([
2611
4095
  "allowLan",
2612
4096
  "budget",
2613
4097
  "debug",
4098
+ "dataFlow",
2614
4099
  "editor",
2615
4100
  "enabled",
4101
+ "externalAgent",
2616
4102
  "exclude",
2617
4103
  "include",
2618
4104
  "locale",
@@ -2698,8 +4184,12 @@ function serializeResolvedSpotPatchOptions(options) {
2698
4184
  allowLan: options.allowLan,
2699
4185
  budget: options.budget,
2700
4186
  debug: options.debug,
4187
+ dataFlow: options.dataFlow.enabled ? Object.freeze({
4188
+ runtime: options.dataFlow.runtime
4189
+ }) : false,
2701
4190
  editor: options.editor,
2702
4191
  enabled: options.enabled,
4192
+ externalAgent: options.externalAgent.enabled,
2703
4193
  exclude: Object.freeze(options.exclude.map(serializeFilter)),
2704
4194
  include: Object.freeze(options.include.map(serializeFilter)),
2705
4195
  locale: options.locale,
@@ -2740,11 +4230,20 @@ function parseBudget(value) {
2740
4230
  );
2741
4231
  return Object.freeze(budget);
2742
4232
  }
4233
+ function parseDataFlow(value) {
4234
+ if (value === false) return false;
4235
+ if (!isRecord2(value) || !hasExactKeys(value, ["runtime"]) || value.runtime !== "dispatch") {
4236
+ throw new TypeError("The SpotPatch data-flow transport is invalid.");
4237
+ }
4238
+ return Object.freeze({
4239
+ runtime: value.runtime
4240
+ });
4241
+ }
2743
4242
  function parseSerializedSpotPatchOptions(value) {
2744
4243
  if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
2745
4244
  throw new TypeError("The SpotPatch options transport is invalid.");
2746
4245
  }
2747
- 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)) {
4246
+ 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)) {
2748
4247
  throw new TypeError("The SpotPatch options transport is invalid.");
2749
4248
  }
2750
4249
  try {
@@ -2753,8 +4252,10 @@ function parseSerializedSpotPatchOptions(value) {
2753
4252
  allowLan: value.allowLan,
2754
4253
  budget: parseBudget(value.budget),
2755
4254
  debug: value.debug,
4255
+ dataFlow: parseDataFlow(value.dataFlow),
2756
4256
  editor: value.editor,
2757
4257
  enabled: value.enabled,
4258
+ externalAgent: value.externalAgent,
2758
4259
  exclude: parseFilterList(value.exclude),
2759
4260
  include: parseFilterList(value.include),
2760
4261
  locale: value.locale,
@@ -2773,8 +4274,10 @@ export {
2773
4274
  DEFAULT_OPTIONS,
2774
4275
  applyIntegrationPlan,
2775
4276
  createAgentJobManager,
4277
+ createExternalHandoffService,
2776
4278
  createIntegrationFileChange,
2777
4279
  createRuntimeAiConfig,
4280
+ createRuntimeDataFlowConfig,
2778
4281
  createSession,
2779
4282
  createSourceRegistrationService,
2780
4283
  createSourceRegistry,