@spotpatch/dev-server 0.5.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) {
@@ -780,7 +1877,8 @@ var DEFAULT_OPTIONS = Object.freeze({
780
1877
  enabled: false,
781
1878
  runtime: "dispatch",
782
1879
  limits: DEFAULT_DATA_FLOW_LIMITS
783
- })
1880
+ }),
1881
+ externalAgent: Object.freeze({ enabled: false })
784
1882
  });
785
1883
  var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
786
1884
  var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
@@ -1106,6 +2204,9 @@ function resolveOptions(options = {}, environmentAi) {
1106
2204
  if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
1107
2205
  throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
1108
2206
  }
2207
+ if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
2208
+ throw new RangeError("SpotPatch externalAgent must be a boolean.");
2209
+ }
1109
2210
  const budget = Object.freeze({
1110
2211
  ...DEFAULT_OPTIONS.budget,
1111
2212
  ...options.budget
@@ -1138,7 +2239,10 @@ function resolveOptions(options = {}, environmentAi) {
1138
2239
  locale,
1139
2240
  maxTargets,
1140
2241
  ai: resolveAiOptions(options.ai ?? environmentAi),
1141
- dataFlow: resolveDataFlowOptions(options.dataFlow)
2242
+ dataFlow: resolveDataFlowOptions(options.dataFlow),
2243
+ externalAgent: Object.freeze({
2244
+ enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
2245
+ })
1142
2246
  };
1143
2247
  if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
1144
2248
  throw new RangeError("SpotPatch shortcut is invalid.");
@@ -1148,9 +2252,9 @@ function resolveOptions(options = {}, environmentAi) {
1148
2252
 
1149
2253
  // src/project-validation.ts
1150
2254
  import { execFile } from "child_process";
1151
- import { access as access2, lstat as lstat2, readFile as readFile2, realpath as realpath2 } from "fs/promises";
2255
+ import { access as access2, lstat as lstat3, readFile as readFile2, realpath as realpath2 } from "fs/promises";
1152
2256
  import { createRequire } from "module";
1153
- import path2 from "path";
2257
+ import path3 from "path";
1154
2258
  import { promisify } from "util";
1155
2259
  var execFileAsync = promisify(execFile);
1156
2260
  var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
@@ -1160,14 +2264,14 @@ function isRecord(value) {
1160
2264
  }
1161
2265
  async function isRegularFile(absolutePath) {
1162
2266
  try {
1163
- const metadata = await lstat2(absolutePath);
2267
+ const metadata = await lstat3(absolutePath);
1164
2268
  return metadata.isFile() && !metadata.isSymbolicLink();
1165
2269
  } catch {
1166
2270
  return false;
1167
2271
  }
1168
2272
  }
1169
2273
  async function readManifest(appRoot) {
1170
- const manifestPath = path2.join(appRoot, "package.json");
2274
+ const manifestPath = path3.join(appRoot, "package.json");
1171
2275
  if (!await isRegularFile(manifestPath)) {
1172
2276
  return void 0;
1173
2277
  }
@@ -1196,8 +2300,8 @@ async function findGitRoot(appRoot) {
1196
2300
  windowsHide: true
1197
2301
  });
1198
2302
  const root = await realpath2(result.stdout.trim());
1199
- const relative = path2.relative(root, appRoot);
1200
- if (relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative)) {
2303
+ const relative = path3.relative(root, appRoot);
2304
+ if (relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative)) {
1201
2305
  return root;
1202
2306
  }
1203
2307
  } catch {
@@ -1206,10 +2310,10 @@ async function findGitRoot(appRoot) {
1206
2310
  return void 0;
1207
2311
  }
1208
2312
  async function resolveTypeScriptCli(appRoot) {
1209
- const resolveFromApplication = createRequire(path2.join(appRoot, "package.json"));
2313
+ const resolveFromApplication = createRequire(path3.join(appRoot, "package.json"));
1210
2314
  try {
1211
2315
  const packagePath = resolveFromApplication.resolve("typescript/package.json");
1212
- const cliPath = path2.join(path2.dirname(packagePath), "bin", "tsc");
2316
+ const cliPath = path3.join(path3.dirname(packagePath), "bin", "tsc");
1213
2317
  await access2(cliPath);
1214
2318
  return await realpath2(cliPath);
1215
2319
  } catch {
@@ -1217,11 +2321,11 @@ async function resolveTypeScriptCli(appRoot) {
1217
2321
  }
1218
2322
  }
1219
2323
  function portableRelativePath(from, to) {
1220
- return path2.relative(from, to).split(path2.sep).join("/");
2324
+ return path3.relative(from, to).split(path3.sep).join("/");
1221
2325
  }
1222
2326
  async function discoverProjectValidationCheck(options) {
1223
2327
  const appRoot = await realpath2(options.appRoot);
1224
- const tsconfigPath = path2.join(appRoot, "tsconfig.json");
2328
+ const tsconfigPath = path3.join(appRoot, "tsconfig.json");
1225
2329
  const [manifest, projectRoot, hasTsconfig] = await Promise.all([
1226
2330
  readManifest(appRoot),
1227
2331
  findGitRoot(appRoot),
@@ -1309,16 +2413,16 @@ async function resolveProjectOptions(input) {
1309
2413
  }
1310
2414
 
1311
2415
  // src/registry/source-registry.ts
1312
- import path3 from "path";
2416
+ import path4 from "path";
1313
2417
 
1314
2418
  // src/registry/source-id.ts
1315
- import { randomBytes as randomBytes3 } from "crypto";
2419
+ import { randomBytes as randomBytes7 } from "crypto";
1316
2420
  var SOURCE_ID_BYTES = 8;
1317
- var createRandomSourceId = () => randomBytes3(SOURCE_ID_BYTES).toString("base64url");
2421
+ var createRandomSourceId = () => randomBytes7(SOURCE_ID_BYTES).toString("base64url");
1318
2422
 
1319
2423
  // src/registry/source-registry.ts
1320
2424
  function normalizeAbsolutePath(absolutePath) {
1321
- return path3.normalize(path3.resolve(absolutePath));
2425
+ return path4.normalize(path4.resolve(absolutePath));
1322
2426
  }
1323
2427
  function createSourceRegistry(options = {}) {
1324
2428
  const createId = options.createId ?? createRandomSourceId;
@@ -1376,39 +2480,42 @@ function createSourceRegistry(options = {}) {
1376
2480
 
1377
2481
  // src/server/middleware.ts
1378
2482
  import {
1379
- ERROR_CODES as ERROR_CODES10,
2483
+ ERROR_CODES as ERROR_CODES15,
1380
2484
  SPOTPATCH_API_BASE,
1381
- SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
1382
- SpotPatchError as SpotPatchError10,
2485
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
2486
+ SpotPatchError as SpotPatchError15,
1383
2487
  openEditorRequestSchema,
1384
2488
  sourceContextRequestSchema
1385
2489
  } from "@spotpatch/shared";
1386
2490
 
1387
- // src/server/agent-http.ts
2491
+ // src/external-handoff/browser-http.ts
1388
2492
  import {
1389
- ERROR_CODES as ERROR_CODES6,
2493
+ ERROR_CODES as ERROR_CODES10,
2494
+ EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
1390
2495
  SPOTPATCH_ENDPOINTS,
1391
- SpotPatchError as SpotPatchError6,
1392
- agentCapabilityRequestSchema,
1393
- agentJobActionRequestSchema,
1394
- agentJobCreateRequestSchema,
1395
- agentWorkspaceHealthRequestSchema
2496
+ SpotPatchError as SpotPatchError10,
2497
+ externalHandoffCapabilityRequestSchema,
2498
+ externalHandoffPublishRequestSchema,
2499
+ externalHandoffResolveDeliveryRequestSchema,
2500
+ externalHandoffStatusRequestSchema
1396
2501
  } from "@spotpatch/shared";
1397
2502
 
1398
- // src/server/agent-request.ts
2503
+ // src/server/annotation-authorizer.ts
1399
2504
  import { realpath as realpath5 } from "fs/promises";
1400
- import path6 from "path";
2505
+ import path7 from "path";
1401
2506
  import {
1402
- ERROR_CODES as ERROR_CODES4,
1403
- SpotPatchError as SpotPatchError4
2507
+ ERROR_CODES as ERROR_CODES9,
2508
+ SpotPatchError as SpotPatchError9,
2509
+ redactSensitiveText,
2510
+ sanitizeUrl
1404
2511
  } from "@spotpatch/shared";
1405
2512
 
1406
2513
  // src/server/source-context.ts
1407
2514
  import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
1408
- import path5 from "path";
2515
+ import path6 from "path";
1409
2516
  import {
1410
- ERROR_CODES as ERROR_CODES3,
1411
- SpotPatchError as SpotPatchError3
2517
+ ERROR_CODES as ERROR_CODES8,
2518
+ SpotPatchError as SpotPatchError8
1412
2519
  } from "@spotpatch/shared";
1413
2520
 
1414
2521
  // src/server/extract-code-context.ts
@@ -1642,15 +2749,8 @@ function extractCodeContext(options) {
1642
2749
 
1643
2750
  // src/server/source-file.ts
1644
2751
  import { realpath as realpath3, stat as stat2 } from "fs/promises";
1645
- import path4 from "path";
1646
- import { ERROR_CODES as ERROR_CODES2, SpotPatchError as SpotPatchError2 } from "@spotpatch/shared";
1647
-
1648
- // src/server/constants.ts
1649
- var MAX_REQUEST_BODY_BYTES = 32 * 1024;
1650
- var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
1651
- var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
1652
-
1653
- // src/server/source-file.ts
2752
+ import path5 from "path";
2753
+ import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
1654
2754
  var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
1655
2755
  function isMissingFileError(error) {
1656
2756
  return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
@@ -1665,51 +2765,51 @@ async function assertInsideRoot(root, candidate) {
1665
2765
  ]);
1666
2766
  } catch (error) {
1667
2767
  if (isMissingFileError(error)) {
1668
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
2768
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
1669
2769
  cause: error
1670
2770
  });
1671
2771
  }
1672
2772
  throw error;
1673
2773
  }
1674
- const relative = path4.relative(realRoot, realCandidate);
1675
- 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);
1676
2776
  if (outside) {
1677
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_OUTSIDE_ROOT);
2777
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
1678
2778
  }
1679
2779
  return realCandidate;
1680
2780
  }
1681
2781
  async function resolveSourceFile(options) {
1682
2782
  const registeredPath = options.registry.resolve(options.fileId);
1683
2783
  if (registeredPath === void 0) {
1684
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2784
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1685
2785
  }
1686
2786
  const sourcePath = await assertInsideRoot(options.root, registeredPath);
1687
- if (!ALLOWED_EXTENSIONS.has(path4.extname(sourcePath).toLowerCase())) {
1688
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2787
+ if (!ALLOWED_EXTENSIONS.has(path5.extname(sourcePath).toLowerCase())) {
2788
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1689
2789
  }
1690
2790
  let sourceStat;
1691
2791
  try {
1692
2792
  sourceStat = await stat2(sourcePath);
1693
2793
  } catch (error) {
1694
2794
  if (isMissingFileError(error)) {
1695
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND, void 0, {
2795
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
1696
2796
  cause: error
1697
2797
  });
1698
2798
  }
1699
2799
  throw error;
1700
2800
  }
1701
2801
  if (!sourceStat.isFile()) {
1702
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_NOT_FOUND);
2802
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
1703
2803
  }
1704
2804
  if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
1705
- throw new SpotPatchError2(ERROR_CODES2.SOURCE_TOO_LARGE);
2805
+ throw new SpotPatchError7(ERROR_CODES7.SOURCE_TOO_LARGE);
1706
2806
  }
1707
2807
  return sourcePath;
1708
2808
  }
1709
2809
 
1710
2810
  // src/server/source-context.ts
1711
2811
  function toDisplayPath(root, sourcePath) {
1712
- return path5.relative(root, sourcePath).split(path5.sep).join("/");
2812
+ return path6.relative(root, sourcePath).split(path6.sep).join("/");
1713
2813
  }
1714
2814
  async function readSourceContext(options) {
1715
2815
  const sourcePath = await resolveSourceFile({
@@ -1722,7 +2822,7 @@ async function readSourceContext(options) {
1722
2822
  source = await readFile3(sourcePath, "utf8");
1723
2823
  } catch (error) {
1724
2824
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1725
- throw new SpotPatchError3(ERROR_CODES3.SOURCE_NOT_FOUND, void 0, {
2825
+ throw new SpotPatchError8(ERROR_CODES8.SOURCE_NOT_FOUND, void 0, {
1726
2826
  cause: error
1727
2827
  });
1728
2828
  }
@@ -1730,9 +2830,9 @@ async function readSourceContext(options) {
1730
2830
  }
1731
2831
  const lines = source.split(/\r?\n/);
1732
2832
  if (options.request.line > lines.length) {
1733
- throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
2833
+ throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
1734
2834
  }
1735
- const extension = path5.extname(sourcePath).toLowerCase();
2835
+ const extension = path6.extname(sourcePath).toLowerCase();
1736
2836
  return extractCodeContext({
1737
2837
  source,
1738
2838
  sourcePath,
@@ -1745,7 +2845,17 @@ async function readSourceContext(options) {
1745
2845
  });
1746
2846
  }
1747
2847
 
1748
- // 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
+ }
1749
2859
  function compactSourceRef(source) {
1750
2860
  return Object.freeze({
1751
2861
  origin: source.origin,
@@ -1759,27 +2869,20 @@ function compactSourceRef(source) {
1759
2869
  async function authorizeSourceRef(source, registry, root) {
1760
2870
  const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
1761
2871
  if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
1762
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2872
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1763
2873
  }
1764
2874
  if (source.fileId === void 0) {
1765
2875
  if (source.origin === "none" && source.relativePath !== void 0) {
1766
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2876
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1767
2877
  }
1768
2878
  return compactSourceRef(source);
1769
2879
  }
1770
- const sourcePath = await resolveSourceFile({
1771
- fileId: source.fileId,
1772
- registry,
1773
- root
1774
- });
1775
- const relativePath = path6.relative(await realpath5(root), sourcePath).split(path6.sep).join("/");
2880
+ const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
2881
+ const relativePath = path7.relative(await realpath5(root), sourcePath).split(path7.sep).join("/");
1776
2882
  if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
1777
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2883
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1778
2884
  }
1779
- return Object.freeze({
1780
- ...compactSourceRef(source),
1781
- relativePath
1782
- });
2885
+ return Object.freeze({ ...compactSourceRef(source), relativePath });
1783
2886
  }
1784
2887
  function freezeMatchedRule(rule) {
1785
2888
  return Object.freeze({
@@ -1813,7 +2916,7 @@ async function authorizeTarget(target, input) {
1813
2916
  maxLines: input.options.budget.maxCodeLines
1814
2917
  });
1815
2918
  if (marker === void 0 && target.code !== void 0) {
1816
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2919
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1817
2920
  }
1818
2921
  const code = marker === void 0 ? void 0 : await readSourceContext({
1819
2922
  request: marker,
@@ -1823,11 +2926,11 @@ async function authorizeTarget(target, input) {
1823
2926
  maxLines: input.options.budget.maxCodeLines
1824
2927
  });
1825
2928
  if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
1826
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2929
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1827
2930
  }
1828
2931
  return Object.freeze({
1829
2932
  instruction: target.instruction,
1830
- ...target.page === void 0 ? {} : { page: Object.freeze({ ...target.page }) },
2933
+ ...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
1831
2934
  source,
1832
2935
  react: Object.freeze({
1833
2936
  supported: target.react.supported,
@@ -1855,76 +2958,138 @@ async function authorizeTarget(target, input) {
1855
2958
  warnings: Object.freeze([...target.warnings])
1856
2959
  });
1857
2960
  }
1858
- async function authorizeAgentJobRequest(input) {
1859
- const requestedTargets = input.request.annotation.targets;
2961
+ async function authorizeAnnotation(input) {
2962
+ const requestedTargets = input.annotation.targets;
1860
2963
  if (requestedTargets.length > input.options.maxTargets) {
1861
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2964
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1862
2965
  }
1863
2966
  const identities = requestedTargets.map(targetIdentity);
1864
2967
  if (new Set(identities).size !== identities.length) {
1865
- throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
2968
+ throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
1866
2969
  }
1867
2970
  const targets = Object.freeze(
1868
2971
  await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
1869
2972
  );
1870
- const annotation = Object.freeze({
2973
+ return Object.freeze({
1871
2974
  schemaVersion: 3,
1872
- id: input.request.annotation.id,
1873
- locale: input.request.annotation.locale,
1874
- page: Object.freeze({ ...input.request.annotation.page }),
2975
+ id: input.annotation.id,
2976
+ locale: input.annotation.locale,
2977
+ page: sanitizePageContext(input.annotation.page),
1875
2978
  targets,
1876
- createdAt: input.request.annotation.createdAt
1877
- });
1878
- return Object.freeze({
1879
- annotation,
1880
- ...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
1881
- providerProfileId: input.request.providerProfileId,
1882
- modelProfileId: input.request.modelProfileId,
1883
- providerDataConsent: true,
1884
- ...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
1885
- workingTreeMode: input.request.workingTreeMode
2979
+ createdAt: input.annotation.createdAt
1886
2980
  });
1887
2981
  }
1888
2982
 
1889
- // src/server/request-body.ts
1890
- import { ERROR_CODES as ERROR_CODES5, SpotPatchError as SpotPatchError5 } from "@spotpatch/shared";
1891
- function isJsonContentType(value) {
1892
- return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
1893
- }
1894
- async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
1895
- if (!isJsonContentType(request.headers["content-type"])) {
1896
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
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";
1897
2990
  }
1898
- const declaredLength = Number(request.headers["content-length"]);
1899
- if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
1900
- 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);
1901
2996
  }
1902
- const chunks = [];
1903
- let byteLength = 0;
1904
- let exceededLimit = false;
1905
- for await (const rawChunk of request) {
1906
- const chunk = rawChunk;
1907
- if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
1908
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
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
+ });
1909
3005
  }
1910
- const buffer = Buffer.from(chunk);
1911
- byteLength += buffer.byteLength;
1912
- if (byteLength > maximumBytes) {
1913
- exceededLimit = true;
1914
- continue;
3006
+ if (error.code === ERROR_CODES10.INVALID_REQUEST) {
3007
+ throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED, void 0, {
3008
+ cause: error
3009
+ });
1915
3010
  }
1916
- chunks.push(buffer);
1917
3011
  }
1918
- if (exceededLimit || byteLength === 0) {
1919
- 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);
1920
3017
  }
1921
- try {
1922
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1923
- } catch (error) {
1924
- throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST, void 0, {
1925
- cause: error
1926
- });
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;
1927
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);
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
+ });
1928
3093
  }
1929
3094
 
1930
3095
  // src/server/agent-http.ts
@@ -1944,21 +3109,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
1944
3109
  "reverted",
1945
3110
  "failed"
1946
3111
  ]);
1947
- function matchAgentRequestPath(path8) {
1948
- if (path8 === SPOTPATCH_ENDPOINTS.agentCapability) {
3112
+ function matchAgentRequestPath(path9) {
3113
+ if (path9 === SPOTPATCH_ENDPOINTS2.agentCapability) {
1949
3114
  return Object.freeze({ kind: "capability" });
1950
3115
  }
1951
- if (path8 === SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
3116
+ if (path9 === SPOTPATCH_ENDPOINTS2.agentWorkspaceHealth) {
1952
3117
  return Object.freeze({ kind: "workspace-health" });
1953
3118
  }
1954
- if (path8 === SPOTPATCH_ENDPOINTS.agentJobs) {
3119
+ if (path9 === SPOTPATCH_ENDPOINTS2.agentJobs) {
1955
3120
  return Object.freeze({ kind: "create-job" });
1956
3121
  }
1957
- const prefix = `${SPOTPATCH_ENDPOINTS.agentJobs}/`;
1958
- if (!path8.startsWith(prefix)) {
3122
+ const prefix = `${SPOTPATCH_ENDPOINTS2.agentJobs}/`;
3123
+ if (!path9.startsWith(prefix)) {
1959
3124
  return void 0;
1960
3125
  }
1961
- const segments = path8.slice(prefix.length).split("/");
3126
+ const segments = path9.slice(prefix.length).split("/");
1962
3127
  const jobId = segments[0];
1963
3128
  const action = segments[1];
1964
3129
  if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
@@ -1972,7 +3137,7 @@ function matchAgentRequestPath(path8) {
1972
3137
  }
1973
3138
  function requireAgentManager(options) {
1974
3139
  if (options.agentManager === void 0 || options.options.ai === false) {
1975
- throw new SpotPatchError6(ERROR_CODES6.AI_DISABLED);
3140
+ throw new SpotPatchError11(ERROR_CODES11.AI_DISABLED);
1976
3141
  }
1977
3142
  return options.agentManager;
1978
3143
  }
@@ -2025,13 +3190,13 @@ function streamAgentJobEvents(response, manager, jobId) {
2025
3190
  }
2026
3191
  async function handleCapability(request, response, options, writeSuccess) {
2027
3192
  if (request.method !== "POST") {
2028
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3193
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2029
3194
  }
2030
3195
  const parsed = agentCapabilityRequestSchema.safeParse(
2031
3196
  await readJsonRequestBody(request)
2032
3197
  );
2033
3198
  if (!parsed.success) {
2034
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3199
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2035
3200
  }
2036
3201
  const controller = new AbortController();
2037
3202
  const abort = () => {
@@ -2050,13 +3215,13 @@ async function handleCapability(request, response, options, writeSuccess) {
2050
3215
  }
2051
3216
  async function handleCreateJob(request, response, options, writeSuccess) {
2052
3217
  if (request.method !== "POST") {
2053
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3218
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2054
3219
  }
2055
3220
  const parsed = agentJobCreateRequestSchema.safeParse(
2056
3221
  await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
2057
3222
  );
2058
3223
  if (!parsed.success) {
2059
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3224
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2060
3225
  }
2061
3226
  const authorizedRequest = await authorizeAgentJobRequest({
2062
3227
  request: parsed.data,
@@ -2069,13 +3234,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
2069
3234
  }
2070
3235
  async function handleWorkspaceHealth(request, response, options, writeSuccess) {
2071
3236
  if (request.method !== "POST") {
2072
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3237
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2073
3238
  }
2074
3239
  const parsed = agentWorkspaceHealthRequestSchema.safeParse(
2075
3240
  await readJsonRequestBody(request)
2076
3241
  );
2077
3242
  if (!parsed.success) {
2078
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3243
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2079
3244
  }
2080
3245
  const controller = new AbortController();
2081
3246
  const abort = () => {
@@ -2092,13 +3257,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
2092
3257
  async function handleJobAction(request, response, options, route, writeSuccess) {
2093
3258
  const manager = requireAgentManager(options);
2094
3259
  if (request.method !== "POST") {
2095
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3260
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2096
3261
  }
2097
3262
  const parsed = agentJobActionRequestSchema.safeParse(
2098
3263
  await readJsonRequestBody(request)
2099
3264
  );
2100
3265
  if (!parsed.success) {
2101
- throw new SpotPatchError6(ERROR_CODES6.INVALID_REQUEST);
3266
+ throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
2102
3267
  }
2103
3268
  if (route.action === "events") {
2104
3269
  streamAgentJobEvents(response, manager, route.jobId);
@@ -2213,14 +3378,14 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
2213
3378
  var launchConfiguredEditor = createEditorLauncher();
2214
3379
 
2215
3380
  // src/server/data-flow-http.ts
2216
- import { createHash as createHash2 } from "crypto";
3381
+ import { createHash as createHash3 } from "crypto";
2217
3382
  import {
2218
3383
  createStaticDataFlowAnalyzer
2219
3384
  } from "@spotpatch/analyzer";
2220
3385
  import {
2221
3386
  DATA_FLOW_SCHEMA_VERSION,
2222
- ERROR_CODES as ERROR_CODES7,
2223
- SpotPatchError as SpotPatchError7,
3387
+ ERROR_CODES as ERROR_CODES12,
3388
+ SpotPatchError as SpotPatchError12,
2224
3389
  dataFlowComponentReportRequestSchema,
2225
3390
  dataFlowPageReportRequestSchema,
2226
3391
  limitDataFlowReportCollections
@@ -2239,7 +3404,7 @@ function limitDataFlowReportToBytes(report, maximumBytes) {
2239
3404
  truncatedBy: "bytes"
2240
3405
  });
2241
3406
  if (envelopeBytes(limited) > maximumBytes) {
2242
- throw new SpotPatchError7(ERROR_CODES7.INTERNAL_ERROR);
3407
+ throw new SpotPatchError12(ERROR_CODES12.INTERNAL_ERROR);
2243
3408
  }
2244
3409
  for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
2245
3410
  const candidate = limitDataFlowReportCollections(structurallyLimited, {
@@ -2268,7 +3433,7 @@ async function analyzeTarget(request, analyzer, options) {
2268
3433
  request.componentSourceId
2269
3434
  );
2270
3435
  if (anchor?.sourceVersion !== request.sourceVersion) {
2271
- throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_SOURCE_STALE);
3436
+ throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_SOURCE_STALE);
2272
3437
  }
2273
3438
  return anchor;
2274
3439
  }
@@ -2285,7 +3450,7 @@ async function analyzeTarget(request, analyzer, options) {
2285
3450
  column: resolvedRequest.column
2286
3451
  });
2287
3452
  if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
2288
- throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_SOURCE_STALE);
3453
+ throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_SOURCE_STALE);
2289
3454
  }
2290
3455
  return limitDataFlowReportToBytes(
2291
3456
  report,
@@ -2294,7 +3459,7 @@ async function analyzeTarget(request, analyzer, options) {
2294
3459
  }
2295
3460
  function requireAnalyzer(analyzer) {
2296
3461
  if (analyzer === void 0) {
2297
- throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_DISABLED);
3462
+ throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_DISABLED);
2298
3463
  }
2299
3464
  return analyzer;
2300
3465
  }
@@ -2306,7 +3471,7 @@ async function handleComponentDataFlowReport(request, analyzer, options) {
2306
3471
  )
2307
3472
  );
2308
3473
  if (!parsed.success) {
2309
- throw new SpotPatchError7(ERROR_CODES7.INVALID_REQUEST);
3474
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2310
3475
  }
2311
3476
  return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
2312
3477
  }
@@ -2318,7 +3483,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
2318
3483
  )
2319
3484
  );
2320
3485
  if (!parsed.success) {
2321
- throw new SpotPatchError7(ERROR_CODES7.INVALID_REQUEST);
3486
+ throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
2322
3487
  }
2323
3488
  const activeAnalyzer = requireAnalyzer(analyzer);
2324
3489
  const componentReports = await Promise.all(
@@ -2342,7 +3507,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
2342
3507
  const analyzedVersions = new Set(
2343
3508
  componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
2344
3509
  );
2345
- const reportId = `page_${createHash2("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
3510
+ const reportId = `page_${createHash3("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
2346
3511
  const complete = componentReports.every((report2) => report2.completeness.complete);
2347
3512
  const report = Object.freeze({
2348
3513
  schemaVersion: DATA_FLOW_SCHEMA_VERSION,
@@ -2389,20 +3554,20 @@ async function handlePageDataFlowReport(request, analyzer, options) {
2389
3554
  }
2390
3555
 
2391
3556
  // src/server/request-security.ts
2392
- import { timingSafeEqual } from "crypto";
3557
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2393
3558
  import { isIP } from "net";
2394
- import { ERROR_CODES as ERROR_CODES8, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError8 } from "@spotpatch/shared";
3559
+ import { ERROR_CODES as ERROR_CODES13, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError13 } from "@spotpatch/shared";
2395
3560
  function getSingleHeader(request, name) {
2396
3561
  const value = request.headers[name.toLowerCase()];
2397
3562
  return Array.isArray(value) ? value[0] : value;
2398
3563
  }
2399
- function tokensMatch(actual, expected) {
3564
+ function tokensMatch2(actual, expected) {
2400
3565
  if (actual === void 0) {
2401
3566
  return false;
2402
3567
  }
2403
3568
  const actualBytes = Buffer.from(actual);
2404
3569
  const expectedBytes = Buffer.from(expected);
2405
- return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
3570
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
2406
3571
  }
2407
3572
  function isLoopbackHostname(hostname) {
2408
3573
  const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
@@ -2437,33 +3602,33 @@ function parseOrigin(value) {
2437
3602
  }
2438
3603
  function assertRequestAuthorized(request, options) {
2439
3604
  const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
2440
- if (!tokensMatch(actualToken, options.sessionToken)) {
2441
- throw new SpotPatchError8(ERROR_CODES8.INVALID_TOKEN);
3605
+ if (!tokensMatch2(actualToken, options.sessionToken)) {
3606
+ throw new SpotPatchError13(ERROR_CODES13.INVALID_TOKEN);
2442
3607
  }
2443
3608
  const hostHeader = getSingleHeader(request, "host");
2444
3609
  const originHeader = getSingleHeader(request, "origin");
2445
3610
  const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
2446
3611
  const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
2447
3612
  if (host === void 0 || origin === void 0) {
2448
- throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
3613
+ throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
2449
3614
  }
2450
3615
  const hostIsLoopback = isLoopbackHostname(host.hostname);
2451
3616
  const originIsLoopback = isLoopbackHostname(origin.hostname);
2452
3617
  if (!options.allowLan) {
2453
3618
  if (!hostIsLoopback || !originIsLoopback) {
2454
- throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
3619
+ throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
2455
3620
  }
2456
3621
  return;
2457
3622
  }
2458
3623
  if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
2459
- throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
3624
+ throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
2460
3625
  }
2461
3626
  }
2462
3627
 
2463
3628
  // src/server/runtime-bootstrap.ts
2464
3629
  import {
2465
- ERROR_CODES as ERROR_CODES9,
2466
- SpotPatchError as SpotPatchError9,
3630
+ ERROR_CODES as ERROR_CODES14,
3631
+ SpotPatchError as SpotPatchError14,
2467
3632
  runtimeBootstrapRequestSchema,
2468
3633
  runtimeConfigSchema
2469
3634
  } from "@spotpatch/shared";
@@ -2493,7 +3658,7 @@ function resolveRuntimeBootstrapOptions(options) {
2493
3658
  function assertRuntimeBootstrapRequest(request, expectedOrigin) {
2494
3659
  const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
2495
3660
  if (request.method !== "POST" || contentType !== "application/json") {
2496
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3661
+ throw new SpotPatchError14(ERROR_CODES14.INVALID_REQUEST);
2497
3662
  }
2498
3663
  const host = getSingleHeader2(request, "host");
2499
3664
  let hostIsLoopback = false;
@@ -2505,7 +3670,7 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
2505
3670
  }
2506
3671
  }
2507
3672
  if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
2508
- throw new SpotPatchError9(ERROR_CODES9.ORIGIN_NOT_ALLOWED);
3673
+ throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
2509
3674
  }
2510
3675
  }
2511
3676
  async function readRuntimeBootstrap(request, options) {
@@ -2514,103 +3679,139 @@ async function readRuntimeBootstrap(request, options) {
2514
3679
  await readJsonRequestBody(request)
2515
3680
  );
2516
3681
  if (!parsedBody.success) {
2517
- throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
3682
+ throw new SpotPatchError14(ERROR_CODES14.INVALID_REQUEST);
2518
3683
  }
2519
3684
  return options.runtimeConfig;
2520
3685
  }
2521
3686
 
2522
3687
  // src/server/middleware.ts
2523
3688
  var STATUS_BY_ERROR = Object.freeze({
2524
- [ERROR_CODES10.INVALID_REQUEST]: 400,
2525
- [ERROR_CODES10.INVALID_TOKEN]: 401,
2526
- [ERROR_CODES10.ORIGIN_NOT_ALLOWED]: 403,
2527
- [ERROR_CODES10.SOURCE_NOT_FOUND]: 404,
2528
- [ERROR_CODES10.SOURCE_OUTSIDE_ROOT]: 403,
2529
- [ERROR_CODES10.SOURCE_TOO_LARGE]: 413,
2530
- [ERROR_CODES10.EDITOR_OPEN_FAILED]: 500,
2531
- [ERROR_CODES10.DATA_FLOW_DISABLED]: 404,
2532
- [ERROR_CODES10.DATA_FLOW_SOURCE_STALE]: 409,
2533
- [ERROR_CODES10.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
2534
- [ERROR_CODES10.AI_DISABLED]: 404,
2535
- [ERROR_CODES10.PROVIDER_NOT_CONFIGURED]: 503,
2536
- [ERROR_CODES10.PROVIDER_AUTH_FAILED]: 502,
2537
- [ERROR_CODES10.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
2538
- [ERROR_CODES10.MODEL_NOT_ALLOWED]: 400,
2539
- [ERROR_CODES10.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
2540
- [ERROR_CODES10.PROVIDER_RATE_LIMITED]: 429,
2541
- [ERROR_CODES10.AGENT_BUSY]: 409,
2542
- [ERROR_CODES10.AGENT_LIMIT_EXCEEDED]: 413,
2543
- [ERROR_CODES10.AGENT_CANCELLED]: 409,
2544
- [ERROR_CODES10.WORKTREE_DIRTY]: 409,
2545
- [ERROR_CODES10.WORKTREE_NOT_REPOSITORY]: 409,
2546
- [ERROR_CODES10.WORKTREE_OPERATION_IN_PROGRESS]: 409,
2547
- [ERROR_CODES10.WORKTREE_CONFLICTED]: 409,
2548
- [ERROR_CODES10.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
2549
- [ERROR_CODES10.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
2550
- [ERROR_CODES10.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
2551
- [ERROR_CODES10.TOOL_DENIED]: 403,
2552
- [ERROR_CODES10.TOOL_INPUT_INVALID]: 422,
2553
- [ERROR_CODES10.TOOL_ARGUMENTS_INVALID]: 422,
2554
- [ERROR_CODES10.TOOL_CALL_ID_CONFLICT]: 422,
2555
- [ERROR_CODES10.TOOL_PATH_DENIED]: 403,
2556
- [ERROR_CODES10.PATCH_REJECTED]: 422,
2557
- [ERROR_CODES10.VALIDATION_FAILED]: 422,
2558
- [ERROR_CODES10.APPLY_CONFLICT]: 409,
2559
- [ERROR_CODES10.INTERNAL_ERROR]: 500
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
2560
3743
  });
2561
3744
  var PUBLIC_MESSAGES = Object.freeze({
2562
- [ERROR_CODES10.INVALID_REQUEST]: "The request is invalid.",
2563
- [ERROR_CODES10.INVALID_TOKEN]: "The session token is invalid.",
2564
- [ERROR_CODES10.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
2565
- [ERROR_CODES10.SOURCE_NOT_FOUND]: "The source file is unavailable.",
2566
- [ERROR_CODES10.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
2567
- [ERROR_CODES10.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
2568
- [ERROR_CODES10.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
2569
- [ERROR_CODES10.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
2570
- [ERROR_CODES10.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
2571
- [ERROR_CODES10.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
2572
- [ERROR_CODES10.AI_DISABLED]: "AI execution is not enabled.",
2573
- [ERROR_CODES10.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
2574
- [ERROR_CODES10.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
2575
- [ERROR_CODES10.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
2576
- [ERROR_CODES10.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
2577
- [ERROR_CODES10.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
2578
- [ERROR_CODES10.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
2579
- [ERROR_CODES10.AGENT_BUSY]: "Another Agent job is already running.",
2580
- [ERROR_CODES10.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
2581
- [ERROR_CODES10.AGENT_CANCELLED]: "The Agent job was cancelled.",
2582
- [ERROR_CODES10.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
2583
- [ERROR_CODES10.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
2584
- [ERROR_CODES10.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
2585
- [ERROR_CODES10.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
2586
- [ERROR_CODES10.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
2587
- [ERROR_CODES10.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
2588
- [ERROR_CODES10.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
2589
- [ERROR_CODES10.TOOL_DENIED]: "The Agent tool request was denied.",
2590
- [ERROR_CODES10.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
2591
- [ERROR_CODES10.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
2592
- [ERROR_CODES10.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
2593
- [ERROR_CODES10.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
2594
- [ERROR_CODES10.PATCH_REJECTED]: "The proposed patch was rejected.",
2595
- [ERROR_CODES10.VALIDATION_FAILED]: "The proposed change failed validation.",
2596
- [ERROR_CODES10.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
2597
- [ERROR_CODES10.INTERNAL_ERROR]: "The request could not be completed."
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."
2598
3799
  });
2599
- function writeJson(response, status, payload) {
3800
+ function writeJson2(response, status, payload) {
2600
3801
  response.statusCode = status;
2601
3802
  response.setHeader("Cache-Control", "no-store");
2602
3803
  response.setHeader("Content-Type", "application/json; charset=utf-8");
2603
3804
  response.end(JSON.stringify(payload));
2604
3805
  }
2605
3806
  function asSpotPatchError(error) {
2606
- return error instanceof SpotPatchError10 ? error : new SpotPatchError10(ERROR_CODES10.INTERNAL_ERROR, void 0, { cause: error });
3807
+ return error instanceof SpotPatchError15 ? error : new SpotPatchError15(ERROR_CODES15.INTERNAL_ERROR, void 0, { cause: error });
2607
3808
  }
2608
3809
  function writeError(response, error, logger) {
2609
3810
  const normalized = asSpotPatchError(error);
2610
- if (normalized.code === ERROR_CODES10.INTERNAL_ERROR) {
3811
+ if (normalized.code === ERROR_CODES15.INTERNAL_ERROR) {
2611
3812
  logger?.warn("[spotpatch:server] Internal request failure.");
2612
3813
  }
2613
- writeJson(response, STATUS_BY_ERROR[normalized.code], {
3814
+ writeJson2(response, STATUS_BY_ERROR[normalized.code], {
2614
3815
  ok: false,
2615
3816
  error: {
2616
3817
  code: normalized.code,
@@ -2630,7 +3831,7 @@ async function handleSourceContext(request, options) {
2630
3831
  await readJsonRequestBody(request)
2631
3832
  );
2632
3833
  if (!parsed.success) {
2633
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3834
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2634
3835
  }
2635
3836
  return readSourceContext({
2636
3837
  request: parsed.data,
@@ -2643,7 +3844,7 @@ async function handleSourceContext(request, options) {
2643
3844
  async function handleOpenEditor(request, options) {
2644
3845
  const parsed = openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
2645
3846
  if (!parsed.success) {
2646
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3847
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2647
3848
  }
2648
3849
  const body = parsed.data;
2649
3850
  const sourcePath = await resolveSourceFile({
@@ -2660,7 +3861,7 @@ async function handleOpenEditor(request, options) {
2660
3861
  options.logger?.warn(
2661
3862
  `[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
2662
3863
  );
2663
- throw new SpotPatchError10(ERROR_CODES10.EDITOR_OPEN_FAILED, void 0, {
3864
+ throw new SpotPatchError15(ERROR_CODES15.EDITOR_OPEN_FAILED, void 0, {
2664
3865
  cause: error
2665
3866
  });
2666
3867
  }
@@ -2669,63 +3870,81 @@ function createSpotPatchMiddleware(options) {
2669
3870
  const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
2670
3871
  const dataFlowAnalyzer = createDataFlowAnalyzer(options);
2671
3872
  return (request, response, next) => {
2672
- const path8 = requestPath(request);
2673
- const agentRoute = matchAgentRequestPath(path8);
2674
- if (path8 !== SPOTPATCH_ENDPOINTS2.sourceContext && path8 !== SPOTPATCH_ENDPOINTS2.openEditor && path8 !== SPOTPATCH_ENDPOINTS2.dataFlowComponentReport && path8 !== SPOTPATCH_ENDPOINTS2.dataFlowPageReport && agentRoute === void 0 && !path8.startsWith(`${SPOTPATCH_API_BASE}/`)) {
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}/`)) {
2675
3877
  next();
2676
3878
  return;
2677
3879
  }
2678
3880
  const handle = async () => {
2679
- if (path8 === SPOTPATCH_ENDPOINTS2.bootstrap && bootstrap !== void 0) {
3881
+ if (path9 === SPOTPATCH_ENDPOINTS3.bootstrap && bootstrap !== void 0) {
2680
3882
  const data = await readRuntimeBootstrap(
2681
3883
  request,
2682
3884
  bootstrap
2683
3885
  );
2684
- writeJson(response, 200, { ok: true, data });
3886
+ writeJson2(response, 200, { ok: true, data });
2685
3887
  return;
2686
3888
  }
2687
3889
  assertRequestAuthorized(request, {
2688
3890
  allowLan: options.options.allowLan,
2689
3891
  sessionToken: options.session.token
2690
3892
  });
2691
- if (path8 === SPOTPATCH_ENDPOINTS2.sourceContext) {
3893
+ if (path9 === SPOTPATCH_ENDPOINTS3.sourceContext) {
2692
3894
  if (request.method !== "POST") {
2693
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3895
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2694
3896
  }
2695
3897
  const data = await handleSourceContext(request, options);
2696
- writeJson(response, 200, { ok: true, data });
3898
+ writeJson2(response, 200, { ok: true, data });
2697
3899
  return;
2698
3900
  }
2699
- if (path8 === SPOTPATCH_ENDPOINTS2.openEditor) {
3901
+ if (path9 === SPOTPATCH_ENDPOINTS3.openEditor) {
2700
3902
  if (request.method !== "POST") {
2701
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3903
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2702
3904
  }
2703
3905
  const data = await handleOpenEditor(request, options);
2704
- writeJson(response, 200, { ok: true, data });
3906
+ writeJson2(response, 200, { ok: true, data });
2705
3907
  return;
2706
3908
  }
2707
- if (path8 === SPOTPATCH_ENDPOINTS2.dataFlowComponentReport) {
3909
+ if (path9 === SPOTPATCH_ENDPOINTS3.dataFlowComponentReport) {
2708
3910
  if (request.method !== "POST") {
2709
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3911
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2710
3912
  }
2711
3913
  const data = await handleComponentDataFlowReport(
2712
3914
  request,
2713
3915
  dataFlowAnalyzer,
2714
3916
  options
2715
3917
  );
2716
- writeJson(response, 200, { ok: true, data });
3918
+ writeJson2(response, 200, { ok: true, data });
2717
3919
  return;
2718
3920
  }
2719
- if (path8 === SPOTPATCH_ENDPOINTS2.dataFlowPageReport) {
3921
+ if (path9 === SPOTPATCH_ENDPOINTS3.dataFlowPageReport) {
2720
3922
  if (request.method !== "POST") {
2721
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3923
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2722
3924
  }
2723
3925
  const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
2724
- writeJson(response, 200, { ok: true, data });
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
+ );
2725
3944
  return;
2726
3945
  }
2727
3946
  if (agentRoute === void 0) {
2728
- throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
3947
+ throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
2729
3948
  }
2730
3949
  await handleAgentRequest(
2731
3950
  request,
@@ -2733,7 +3952,7 @@ function createSpotPatchMiddleware(options) {
2733
3952
  options,
2734
3953
  agentRoute,
2735
3954
  (target, status, data) => {
2736
- writeJson(target, status, { ok: true, data });
3955
+ writeJson2(target, status, { ok: true, data });
2737
3956
  }
2738
3957
  );
2739
3958
  };
@@ -2744,9 +3963,9 @@ function createSpotPatchMiddleware(options) {
2744
3963
  }
2745
3964
 
2746
3965
  // src/server/source-registration.ts
2747
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2748
- import { lstat as lstat3, realpath as realpath6 } from "fs/promises";
2749
- import path7 from "path";
3966
+ import { timingSafeEqual as timingSafeEqual3 } from "crypto";
3967
+ import { lstat as lstat4, realpath as realpath6 } from "fs/promises";
3968
+ import path8 from "path";
2750
3969
  import { createSourceFilter } from "@spotpatch/compiler";
2751
3970
  import { z as z2 } from "zod";
2752
3971
  var REGISTRATION_BODY_LIMIT_BYTES = 4096;
@@ -2767,16 +3986,16 @@ function identitiesMatch(actual, expected) {
2767
3986
  }
2768
3987
  const actualBytes = Buffer.from(actual);
2769
3988
  const expectedBytes = Buffer.from(expected);
2770
- return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
3989
+ return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
2771
3990
  }
2772
3991
  function isWithinRoot(root, candidate) {
2773
- const relative = path7.relative(root, candidate);
2774
- 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);
2775
3994
  }
2776
3995
  function hasForbiddenSegment(root, candidate) {
2777
- 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));
2778
3997
  }
2779
- function writeJson2(response, statusCode, payload) {
3998
+ function writeJson3(response, statusCode, payload) {
2780
3999
  const body = JSON.stringify(payload);
2781
4000
  response.statusCode = statusCode;
2782
4001
  response.setHeader("Cache-Control", "no-store");
@@ -2796,11 +4015,11 @@ function requestComesFromLoopbackWorker(request) {
2796
4015
  }
2797
4016
  }
2798
4017
  async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
2799
- if (!path7.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
4018
+ if (!path8.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
2800
4019
  return void 0;
2801
4020
  }
2802
4021
  try {
2803
- const sourceStat = await lstat3(requestedPath);
4022
+ const sourceStat = await lstat4(requestedPath);
2804
4023
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
2805
4024
  return void 0;
2806
4025
  }
@@ -2826,14 +4045,14 @@ async function createSourceRegistrationService(input) {
2826
4045
  getSingleHeader3(request, INTERNAL_SECRET_HEADER),
2827
4046
  input.internalSecret
2828
4047
  )) {
2829
- writeJson2(response, 403, { ok: false });
4048
+ writeJson3(response, 403, { ok: false });
2830
4049
  return;
2831
4050
  }
2832
4051
  const parsed = registrationRequestSchema.safeParse(
2833
4052
  await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
2834
4053
  );
2835
4054
  if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
2836
- writeJson2(response, 400, { ok: false });
4055
+ writeJson3(response, 400, { ok: false });
2837
4056
  return;
2838
4057
  }
2839
4058
  const sourcePath = await resolveAuthorizedSource(
@@ -2842,17 +4061,17 @@ async function createSourceRegistrationService(input) {
2842
4061
  (absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
2843
4062
  );
2844
4063
  if (sourcePath === void 0) {
2845
- writeJson2(response, 403, { ok: false });
4064
+ writeJson3(response, 403, { ok: false });
2846
4065
  return;
2847
4066
  }
2848
- writeJson2(response, 200, {
4067
+ writeJson3(response, 200, {
2849
4068
  epoch: input.registryEpoch,
2850
4069
  fileId: input.registry.register(sourcePath)
2851
4070
  });
2852
4071
  };
2853
4072
  void handle().catch(() => {
2854
4073
  if (!response.headersSent) {
2855
- writeJson2(response, 400, { ok: false });
4074
+ writeJson3(response, 400, { ok: false });
2856
4075
  } else {
2857
4076
  response.destroy();
2858
4077
  }
@@ -2862,11 +4081,11 @@ async function createSourceRegistrationService(input) {
2862
4081
  }
2863
4082
 
2864
4083
  // src/session/session.ts
2865
- import { randomBytes as randomBytes4 } from "crypto";
4084
+ import { randomBytes as randomBytes8 } from "crypto";
2866
4085
  function createSession() {
2867
4086
  return Object.freeze({
2868
- id: randomBytes4(16).toString("base64url"),
2869
- token: randomBytes4(16).toString("base64url")
4087
+ id: randomBytes8(16).toString("base64url"),
4088
+ token: randomBytes8(16).toString("base64url")
2870
4089
  });
2871
4090
  }
2872
4091
 
@@ -2879,6 +4098,7 @@ var OPTION_KEYS = Object.freeze([
2879
4098
  "dataFlow",
2880
4099
  "editor",
2881
4100
  "enabled",
4101
+ "externalAgent",
2882
4102
  "exclude",
2883
4103
  "include",
2884
4104
  "locale",
@@ -2969,6 +4189,7 @@ function serializeResolvedSpotPatchOptions(options) {
2969
4189
  }) : false,
2970
4190
  editor: options.editor,
2971
4191
  enabled: options.enabled,
4192
+ externalAgent: options.externalAgent.enabled,
2972
4193
  exclude: Object.freeze(options.exclude.map(serializeFilter)),
2973
4194
  include: Object.freeze(options.include.map(serializeFilter)),
2974
4195
  locale: options.locale,
@@ -3022,7 +4243,7 @@ function parseSerializedSpotPatchOptions(value) {
3022
4243
  if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
3023
4244
  throw new TypeError("The SpotPatch options transport is invalid.");
3024
4245
  }
3025
- if (typeof value.enabled !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord2(value.ai)) {
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)) {
3026
4247
  throw new TypeError("The SpotPatch options transport is invalid.");
3027
4248
  }
3028
4249
  try {
@@ -3034,6 +4255,7 @@ function parseSerializedSpotPatchOptions(value) {
3034
4255
  dataFlow: parseDataFlow(value.dataFlow),
3035
4256
  editor: value.editor,
3036
4257
  enabled: value.enabled,
4258
+ externalAgent: value.externalAgent,
3037
4259
  exclude: parseFilterList(value.exclude),
3038
4260
  include: parseFilterList(value.include),
3039
4261
  locale: value.locale,
@@ -3052,6 +4274,7 @@ export {
3052
4274
  DEFAULT_OPTIONS,
3053
4275
  applyIntegrationPlan,
3054
4276
  createAgentJobManager,
4277
+ createExternalHandoffService,
3055
4278
  createIntegrationFileChange,
3056
4279
  createRuntimeAiConfig,
3057
4280
  createRuntimeDataFlowConfig,