@hachej/boring-workspace 0.1.54 → 0.1.56

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.
@@ -649,6 +649,406 @@ function normalizeBoringPluginPiPackages(plugins) {
649
649
  );
650
650
  }
651
651
 
652
+ // src/shared/workspace-bridge-rpc.ts
653
+ var WORKSPACE_BRIDGE_URL_ENV = "BORING_WORKSPACE_BRIDGE_URL";
654
+ var WORKSPACE_BRIDGE_TOKEN_ENV = "BORING_WORKSPACE_BRIDGE_TOKEN";
655
+ var WORKSPACE_BRIDGE_TOKEN_URL_ENV = "BORING_WORKSPACE_BRIDGE_TOKEN_URL";
656
+ var WORKSPACE_BRIDGE_REFRESH_TOKEN_ENV = "BORING_WORKSPACE_BRIDGE_REFRESH_TOKEN";
657
+ var WORKSPACE_BRIDGE_DISABLED_ENV = "BORING_WORKSPACE_BRIDGE_DISABLED";
658
+ var WorkspaceBridgeErrorCode = /* @__PURE__ */ ((WorkspaceBridgeErrorCode2) => {
659
+ WorkspaceBridgeErrorCode2["OpNotFound"] = "BRIDGE_OP_NOT_FOUND";
660
+ WorkspaceBridgeErrorCode2["DuplicateOp"] = "BRIDGE_DUPLICATE_OP";
661
+ WorkspaceBridgeErrorCode2["CallerNotAllowed"] = "BRIDGE_CALLER_NOT_ALLOWED";
662
+ WorkspaceBridgeErrorCode2["AuthRequired"] = "BRIDGE_AUTH_REQUIRED";
663
+ WorkspaceBridgeErrorCode2["CapabilityDenied"] = "BRIDGE_CAPABILITY_DENIED";
664
+ WorkspaceBridgeErrorCode2["ResourceScopeDenied"] = "BRIDGE_RESOURCE_SCOPE_DENIED";
665
+ WorkspaceBridgeErrorCode2["SchemaInvalid"] = "BRIDGE_SCHEMA_INVALID";
666
+ WorkspaceBridgeErrorCode2["OutputSchemaInvalid"] = "BRIDGE_OUTPUT_SCHEMA_INVALID";
667
+ WorkspaceBridgeErrorCode2["InputTooLarge"] = "BRIDGE_INPUT_TOO_LARGE";
668
+ WorkspaceBridgeErrorCode2["OutputTooLarge"] = "BRIDGE_OUTPUT_TOO_LARGE";
669
+ WorkspaceBridgeErrorCode2["Timeout"] = "BRIDGE_TIMEOUT";
670
+ WorkspaceBridgeErrorCode2["HandlerFailed"] = "BRIDGE_HANDLER_FAILED";
671
+ WorkspaceBridgeErrorCode2["IdempotencyRequired"] = "BRIDGE_IDEMPOTENCY_REQUIRED";
672
+ WorkspaceBridgeErrorCode2["IdempotencyConflict"] = "BRIDGE_IDEMPOTENCY_CONFLICT";
673
+ WorkspaceBridgeErrorCode2["ReplayRejected"] = "BRIDGE_REPLAY_REJECTED";
674
+ WorkspaceBridgeErrorCode2["RateLimited"] = "BRIDGE_RATE_LIMITED";
675
+ WorkspaceBridgeErrorCode2["InvalidToken"] = "BRIDGE_INVALID_TOKEN";
676
+ WorkspaceBridgeErrorCode2["ExpiredToken"] = "BRIDGE_EXPIRED_TOKEN";
677
+ WorkspaceBridgeErrorCode2["InvalidRequest"] = "BRIDGE_INVALID_REQUEST";
678
+ WorkspaceBridgeErrorCode2["UnsupportedRuntime"] = "BRIDGE_UNSUPPORTED_RUNTIME";
679
+ return WorkspaceBridgeErrorCode2;
680
+ })(WorkspaceBridgeErrorCode || {});
681
+ function createWorkspaceBridgeError(code, message, details) {
682
+ return details === void 0 ? { code, message } : { code, message, details };
683
+ }
684
+
685
+ // src/server/workspaceBridge/json.ts
686
+ function stableStringify(value) {
687
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
688
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
689
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
690
+ }
691
+ function measureJsonBytes(value) {
692
+ try {
693
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
694
+ } catch {
695
+ return Number.POSITIVE_INFINITY;
696
+ }
697
+ }
698
+
699
+ // src/server/workspaceBridge/registry.ts
700
+ var RESERVED_WORKSPACE_BRIDGE_OP_PREFIXES = ["workspace-files.v1."];
701
+ var BRIDGE_CALLER_CLASSES = /* @__PURE__ */ new Set(["browser", "runtime", "server"]);
702
+ var BRIDGE_IDEMPOTENCY_POLICIES = /* @__PURE__ */ new Set(["none", "required", "request-id"]);
703
+ function validateWorkspaceBridgeOperationDefinition(definition) {
704
+ if (!definition || typeof definition !== "object") {
705
+ throw invalidDefinition("WorkspaceBridge operation definition must be an object");
706
+ }
707
+ if (typeof definition.op !== "string" || definition.op.trim().length === 0) {
708
+ throw invalidDefinition("WorkspaceBridge operation definition op must be a non-empty string");
709
+ }
710
+ const reservedPrefix = RESERVED_WORKSPACE_BRIDGE_OP_PREFIXES.find((prefix) => definition.op.startsWith(prefix));
711
+ if (reservedPrefix) {
712
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} uses reserved prefix ${reservedPrefix}`);
713
+ }
714
+ if (!Number.isInteger(definition.version) || definition.version < 1) {
715
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} version must be a positive integer`);
716
+ }
717
+ if (typeof definition.owner !== "string" || definition.owner.trim().length === 0) {
718
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} owner must be a non-empty string`);
719
+ }
720
+ if (!Array.isArray(definition.callerClassesAllowed) || definition.callerClassesAllowed.length === 0) {
721
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} callerClassesAllowed must be a non-empty array`);
722
+ }
723
+ for (const callerClass of definition.callerClassesAllowed) {
724
+ if (!BRIDGE_CALLER_CLASSES.has(callerClass)) {
725
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} callerClassesAllowed contains invalid caller class`);
726
+ }
727
+ }
728
+ if (!Array.isArray(definition.requiredCapabilities)) {
729
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} requiredCapabilities must be an array`);
730
+ }
731
+ if (definition.allowCrossWorkspace !== void 0 && typeof definition.allowCrossWorkspace !== "boolean") {
732
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} allowCrossWorkspace must be a boolean when provided`);
733
+ }
734
+ for (const capability of definition.requiredCapabilities) {
735
+ if (typeof capability !== "string" || capability.trim().length === 0) {
736
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} requiredCapabilities must contain non-empty strings`);
737
+ }
738
+ }
739
+ validateSchemaDefinition(definition.op, "inputSchema", definition.inputSchema, true);
740
+ validateSchemaDefinition(definition.op, "outputSchema", definition.outputSchema, false);
741
+ assertPositiveFiniteNumber(definition.op, "timeoutMs", definition.timeoutMs);
742
+ assertPositiveFiniteNumber(definition.op, "maxInputBytes", definition.maxInputBytes);
743
+ assertPositiveFiniteNumber(definition.op, "maxOutputBytes", definition.maxOutputBytes);
744
+ if (!BRIDGE_IDEMPOTENCY_POLICIES.has(definition.idempotencyPolicy)) {
745
+ throw invalidDefinition(`WorkspaceBridge operation ${definition.op} idempotencyPolicy is invalid`);
746
+ }
747
+ }
748
+ function validateSchemaDefinition(op, field, schema, required) {
749
+ if (schema === void 0) {
750
+ if (required) throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} is required`);
751
+ return;
752
+ }
753
+ if (typeof schema !== "object" && typeof schema !== "function" || schema === null) {
754
+ throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} must be a schema object`);
755
+ }
756
+ if (hasSafeParse(schema)) return;
757
+ validateJsonSchemaDefinition(schema, `${op} ${field}`);
758
+ }
759
+ var SUPPORTED_JSON_SCHEMA_KEYS = /* @__PURE__ */ new Set(["type", "properties", "required", "items", "enum", "const", "additionalProperties", "description", "title"]);
760
+ function validateJsonSchemaDefinition(schema, label) {
761
+ if (!isPlainSchemaObject(schema)) {
762
+ throw invalidDefinition(`WorkspaceBridge operation ${label} must be a schema object`);
763
+ }
764
+ for (const key of Object.keys(schema)) {
765
+ if (!SUPPORTED_JSON_SCHEMA_KEYS.has(key)) {
766
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.${key} is not supported by the bridge schema subset`);
767
+ }
768
+ }
769
+ if (!isSupportedJsonSchemaType(schema.type)) {
770
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.type must be a supported JSON schema type`);
771
+ }
772
+ if (schema.required !== void 0 && (!Array.isArray(schema.required) || schema.required.some((item) => typeof item !== "string"))) {
773
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.required must be an array of strings`);
774
+ }
775
+ if (schema.required !== void 0 && schema.type !== "object") {
776
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.required is only supported for object schemas`);
777
+ }
778
+ if (schema.enum !== void 0 && !Array.isArray(schema.enum)) {
779
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.enum must be an array`);
780
+ }
781
+ if (schema.additionalProperties !== void 0 && schema.additionalProperties !== false && schema.additionalProperties !== true) {
782
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.additionalProperties must be boolean when provided`);
783
+ }
784
+ if (schema.additionalProperties !== void 0 && schema.type !== "object") {
785
+ throw invalidDefinition(`WorkspaceBridge operation ${label}.additionalProperties is only supported for object schemas`);
786
+ }
787
+ if (schema.properties !== void 0) {
788
+ if (schema.type !== "object") throw invalidDefinition(`WorkspaceBridge operation ${label}.properties is only supported for object schemas`);
789
+ if (!isRecord2(schema.properties)) throw invalidDefinition(`WorkspaceBridge operation ${label}.properties must be an object`);
790
+ for (const [key, child] of Object.entries(schema.properties)) validateJsonSchemaDefinition(child, `${label}.properties.${key}`);
791
+ }
792
+ if (schema.items !== void 0) {
793
+ if (schema.type !== "array") throw invalidDefinition(`WorkspaceBridge operation ${label}.items is only supported for array schemas`);
794
+ validateJsonSchemaDefinition(schema.items, `${label}.items`);
795
+ }
796
+ }
797
+ function isSupportedJsonSchemaType(type) {
798
+ return type === "object" || type === "array" || type === "string" || type === "number" || type === "integer" || type === "boolean" || type === "null";
799
+ }
800
+ function hasSafeParse(schema) {
801
+ return (typeof schema === "object" || typeof schema === "function") && schema !== null && typeof schema.safeParse === "function";
802
+ }
803
+ function isPlainSchemaObject(value) {
804
+ return isRecord2(value);
805
+ }
806
+ function isRecord2(value) {
807
+ return !!value && typeof value === "object" && !Array.isArray(value);
808
+ }
809
+ function hasOwn(record, key) {
810
+ return Object.prototype.hasOwnProperty.call(record, key);
811
+ }
812
+ function assertPositiveFiniteNumber(op, field, value) {
813
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
814
+ throw invalidDefinition(`WorkspaceBridge operation ${op} ${field} must be a positive finite number`);
815
+ }
816
+ }
817
+ function invalidDefinition(message) {
818
+ throw createWorkspaceBridgeError("BRIDGE_INVALID_REQUEST" /* InvalidRequest */, message);
819
+ }
820
+ var WorkspaceBridgeRegistry = class {
821
+ handlers = /* @__PURE__ */ new Map();
822
+ logger;
823
+ ownerWorkspaceId;
824
+ constructor(options = {}) {
825
+ if (options.ownerWorkspaceId !== void 0 && options.ownerWorkspaceId.trim().length === 0) {
826
+ throw createWorkspaceBridgeError("BRIDGE_INVALID_REQUEST" /* InvalidRequest */, "WorkspaceBridge registry ownerWorkspaceId must be non-empty when provided");
827
+ }
828
+ this.logger = options.logger;
829
+ this.ownerWorkspaceId = options.ownerWorkspaceId;
830
+ }
831
+ registerHandler(definition, handler, options = {}) {
832
+ validateWorkspaceBridgeOperationDefinition(definition);
833
+ const existing = this.handlers.get(definition.op);
834
+ if (existing && !options.replace) {
835
+ throw createWorkspaceBridgeError(
836
+ "BRIDGE_DUPLICATE_OP" /* DuplicateOp */,
837
+ `WorkspaceBridge operation is already registered: ${definition.op}`
838
+ );
839
+ }
840
+ this.handlers.set(definition.op, {
841
+ definition,
842
+ handler
843
+ });
844
+ }
845
+ getDefinition(op) {
846
+ return this.handlers.get(op)?.definition;
847
+ }
848
+ listDefinitions() {
849
+ return Array.from(this.handlers.values(), ({ definition }) => definition);
850
+ }
851
+ async call(request, context, options = {}) {
852
+ const requestId = request.requestId ?? context.requestId ?? createRequestId();
853
+ const registered = this.handlers.get(request.op);
854
+ if (!registered) {
855
+ return this.failure(request.op, requestId, "BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge operation is not registered");
856
+ }
857
+ const { definition, handler } = registered;
858
+ const logBase = {
859
+ requestId,
860
+ op: request.op,
861
+ callerClass: context.callerClass,
862
+ workspaceId: context.workspaceId,
863
+ sessionId: context.sessionId,
864
+ pluginId: context.pluginId,
865
+ tokenId: context.tokenId,
866
+ capabilities: context.capabilities,
867
+ actor: context.actor
868
+ };
869
+ if (!definition.callerClassesAllowed.includes(context.callerClass)) {
870
+ return this.failure(request.op, requestId, "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */, "Caller class is not allowed for operation", logBase);
871
+ }
872
+ const expectedWorkspaceId = options.expectedWorkspaceId ?? this.ownerWorkspaceId;
873
+ if (expectedWorkspaceId && context.workspaceId !== expectedWorkspaceId && definition.allowCrossWorkspace !== true) {
874
+ return this.failure(request.op, requestId, "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */, "Caller workspace is not authorized for this bridge registry", {
875
+ ...logBase,
876
+ expectedWorkspaceId
877
+ });
878
+ }
879
+ const missingCapability = definition.requiredCapabilities.find((capability) => !context.capabilities.includes(capability));
880
+ if (missingCapability) {
881
+ return this.failure(request.op, requestId, "BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */, "Caller is missing a required capability", {
882
+ ...logBase,
883
+ missingCapability
884
+ });
885
+ }
886
+ const inputBytes = measureJsonBytes(request.input);
887
+ if (inputBytes > definition.maxInputBytes) {
888
+ return this.failure(request.op, requestId, "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */, "Bridge input exceeds operation limit", {
889
+ ...logBase,
890
+ inputBytes,
891
+ maxInputBytes: definition.maxInputBytes
892
+ });
893
+ }
894
+ const inputValidation = validateSchema(definition.inputSchema, request.input);
895
+ if (!inputValidation.success) {
896
+ return this.failure(request.op, requestId, "BRIDGE_SCHEMA_INVALID" /* SchemaInvalid */, "Bridge input failed schema validation", {
897
+ ...logBase,
898
+ schemaMessage: inputValidation.message
899
+ });
900
+ }
901
+ const controller = new AbortController();
902
+ const abortFromCaller = () => controller.abort(context.signal?.reason);
903
+ if (context.signal?.aborted) abortFromCaller();
904
+ else context.signal?.addEventListener("abort", abortFromCaller, { once: true });
905
+ const timeoutResult = /* @__PURE__ */ Symbol("workspace-bridge-timeout");
906
+ let timeout;
907
+ try {
908
+ this.logger?.debug?.("workspace bridge call started", logBase);
909
+ const handlerPromise = Promise.resolve().then(() => handler({
910
+ input: request.input,
911
+ context: { ...context, requestId },
912
+ definition,
913
+ signal: controller.signal,
914
+ emitUiEffect: context.emitUiEffect
915
+ }));
916
+ const timeoutPromise = new Promise((resolve8) => {
917
+ timeout = setTimeout(() => {
918
+ controller.abort("timeout");
919
+ resolve8(timeoutResult);
920
+ }, definition.timeoutMs);
921
+ });
922
+ const output = await Promise.race([handlerPromise, timeoutPromise]);
923
+ if (output === timeoutResult) {
924
+ return this.failure(request.op, requestId, "BRIDGE_TIMEOUT" /* Timeout */, "Bridge handler timed out", logBase);
925
+ }
926
+ const outputValidation = definition.outputSchema ? validateSchema(definition.outputSchema, output) : { success: true };
927
+ if (!outputValidation.success) {
928
+ return this.failure(request.op, requestId, "BRIDGE_OUTPUT_SCHEMA_INVALID" /* OutputSchemaInvalid */, "Bridge output failed schema validation", {
929
+ ...logBase,
930
+ schemaMessage: outputValidation.message
931
+ });
932
+ }
933
+ const outputBytes = measureJsonBytes(output);
934
+ if (outputBytes > definition.maxOutputBytes) {
935
+ return this.failure(request.op, requestId, "BRIDGE_OUTPUT_TOO_LARGE" /* OutputTooLarge */, "Bridge output exceeds operation limit", {
936
+ ...logBase,
937
+ outputBytes,
938
+ maxOutputBytes: definition.maxOutputBytes
939
+ });
940
+ }
941
+ this.logger?.info?.("workspace bridge call completed", logBase);
942
+ return { ok: true, op: request.op, requestId, output };
943
+ } catch (err) {
944
+ if (controller.signal.aborted && controller.signal.reason === "timeout") {
945
+ return this.failure(request.op, requestId, "BRIDGE_TIMEOUT" /* Timeout */, "Bridge handler timed out", logBase);
946
+ }
947
+ const bridgeError = isWorkspaceBridgeError(err) ? err : void 0;
948
+ this.logger?.error?.("workspace bridge call failed", {
949
+ ...logBase,
950
+ errorName: err instanceof Error ? err.name : typeof err,
951
+ errorCode: bridgeError?.code
952
+ });
953
+ return this.failure(request.op, requestId, bridgeError?.code ?? "BRIDGE_HANDLER_FAILED" /* HandlerFailed */, bridgeError?.message ?? "Bridge handler failed", logBase);
954
+ } finally {
955
+ if (timeout) clearTimeout(timeout);
956
+ context.signal?.removeEventListener("abort", abortFromCaller);
957
+ }
958
+ }
959
+ failure(op, requestId, code, message, logFields) {
960
+ const error = createWorkspaceBridgeError(code, message);
961
+ this.logger?.warn?.("workspace bridge call rejected", {
962
+ ...logFields,
963
+ op,
964
+ requestId,
965
+ errorCode: code
966
+ });
967
+ return { ok: false, op, requestId, error };
968
+ }
969
+ };
970
+ function createWorkspaceBridgeRegistry(options = {}) {
971
+ return new WorkspaceBridgeRegistry(options);
972
+ }
973
+ var WORKSPACE_BRIDGE_ERROR_CODES = new Set(Object.values(WorkspaceBridgeErrorCode));
974
+ function isWorkspaceBridgeError(err) {
975
+ if (!err || typeof err !== "object") return false;
976
+ const candidate = err;
977
+ return typeof candidate.code === "string" && WORKSPACE_BRIDGE_ERROR_CODES.has(candidate.code) && typeof candidate.message === "string";
978
+ }
979
+ function validateSchema(schema, value) {
980
+ if (schema === void 0) return { success: true };
981
+ if (hasSafeParse(schema)) {
982
+ const result = schema.safeParse(value);
983
+ return result.success ? { success: true } : { success: false, message: result.error?.message };
984
+ }
985
+ if (isPlainSchemaObject(schema)) return validateJsonSchema(schema, value, "$");
986
+ return { success: false, message: "Unsupported schema" };
987
+ }
988
+ function validateJsonSchema(schema, value, path) {
989
+ if (schema.const !== void 0 && !jsonEqual(value, schema.const)) {
990
+ return { success: false, message: `${path}: Expected const value` };
991
+ }
992
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => jsonEqual(value, candidate))) {
993
+ return { success: false, message: `${path}: Expected one of enum values` };
994
+ }
995
+ const typeValidation = validateJsonSchemaType(schema.type, value, path);
996
+ if (!typeValidation.success) return typeValidation;
997
+ if (schema.type === "object") {
998
+ if (!isRecord2(value)) return { success: false, message: `${path}: Expected object` };
999
+ const properties = isRecord2(schema.properties) ? schema.properties : {};
1000
+ const required = Array.isArray(schema.required) ? schema.required.filter((item) => typeof item === "string") : [];
1001
+ for (const key of required) {
1002
+ if (!hasOwn(value, key)) return { success: false, message: `${path}.${key}: Required property missing` };
1003
+ }
1004
+ if (schema.additionalProperties === false) {
1005
+ for (const key of Object.keys(value)) {
1006
+ if (!hasOwn(properties, key)) return { success: false, message: `${path}.${key}: Additional properties are not allowed` };
1007
+ }
1008
+ }
1009
+ for (const [key, child] of Object.entries(properties)) {
1010
+ if (hasOwn(value, key)) {
1011
+ const childResult = validateJsonSchema(child, value[key], `${path}.${key}`);
1012
+ if (!childResult.success) return childResult;
1013
+ }
1014
+ }
1015
+ }
1016
+ if (schema.type === "array" && schema.items !== void 0) {
1017
+ if (!Array.isArray(value)) return { success: false, message: `${path}: Expected array` };
1018
+ for (let i = 0; i < value.length; i += 1) {
1019
+ const childResult = validateJsonSchema(schema.items, value[i], `${path}[${i}]`);
1020
+ if (!childResult.success) return childResult;
1021
+ }
1022
+ }
1023
+ return { success: true };
1024
+ }
1025
+ function validateJsonSchemaType(type, value, path) {
1026
+ switch (type) {
1027
+ case "object":
1028
+ return isRecord2(value) ? { success: true } : { success: false, message: `${path}: Expected object` };
1029
+ case "array":
1030
+ return Array.isArray(value) ? { success: true } : { success: false, message: `${path}: Expected array` };
1031
+ case "string":
1032
+ return typeof value === "string" ? { success: true } : { success: false, message: `${path}: Expected string` };
1033
+ case "number":
1034
+ return typeof value === "number" && Number.isFinite(value) ? { success: true } : { success: false, message: `${path}: Expected number` };
1035
+ case "integer":
1036
+ return Number.isInteger(value) ? { success: true } : { success: false, message: `${path}: Expected integer` };
1037
+ case "boolean":
1038
+ return typeof value === "boolean" ? { success: true } : { success: false, message: `${path}: Expected boolean` };
1039
+ case "null":
1040
+ return value === null ? { success: true } : { success: false, message: `${path}: Expected null` };
1041
+ default:
1042
+ return { success: false, message: `${path}: Unsupported schema type` };
1043
+ }
1044
+ }
1045
+ function jsonEqual(a, b) {
1046
+ return stableStringify(a) === stableStringify(b);
1047
+ }
1048
+ function createRequestId() {
1049
+ return `bridge_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1050
+ }
1051
+
652
1052
  // src/server/plugins/piPackages.ts
653
1053
  import {
654
1054
  compactPiPackages,
@@ -739,6 +1139,26 @@ function validatePluginAssets(pluginId, assets) {
739
1139
  }
740
1140
  }
741
1141
  }
1142
+ function validateWorkspaceBridgeHandlers(pluginId, handlers) {
1143
+ for (let i = 0; i < handlers.length; i++) {
1144
+ const entry = handlers[i];
1145
+ if (!entry || typeof entry !== "object") {
1146
+ fail(pluginId, `workspaceBridgeHandlers[${i}] must be an object`);
1147
+ }
1148
+ if (!entry.definition || typeof entry.definition !== "object") {
1149
+ fail(pluginId, `workspaceBridgeHandlers[${i}].definition must be an object`);
1150
+ }
1151
+ try {
1152
+ validateWorkspaceBridgeOperationDefinition(entry.definition);
1153
+ } catch (error) {
1154
+ const message = error && typeof error === "object" && "message" in error ? String(error.message) : "invalid WorkspaceBridge operation definition";
1155
+ fail(pluginId, `workspaceBridgeHandlers[${i}].definition invalid: ${message}`);
1156
+ }
1157
+ if (typeof entry.handler !== "function") {
1158
+ fail(pluginId, `workspaceBridgeHandlers[${i}].handler must be a function`);
1159
+ }
1160
+ }
1161
+ }
742
1162
  function validateProvisioning(pluginId, provisioning) {
743
1163
  if (!provisioning || typeof provisioning !== "object") {
744
1164
  fail(pluginId, "provisioning must be an object");
@@ -858,6 +1278,12 @@ function validateServerPlugin(plugin) {
858
1278
  }
859
1279
  validatePluginAssets(plugin.id, plugin.assets);
860
1280
  }
1281
+ if (plugin.workspaceBridgeHandlers !== void 0) {
1282
+ if (!Array.isArray(plugin.workspaceBridgeHandlers)) {
1283
+ fail(plugin.id, "workspaceBridgeHandlers must be an array when provided");
1284
+ }
1285
+ validateWorkspaceBridgeHandlers(plugin.id, plugin.workspaceBridgeHandlers);
1286
+ }
861
1287
  if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
862
1288
  fail(plugin.id, "routes must be a Fastify plugin function when provided");
863
1289
  }
@@ -902,6 +1328,7 @@ function bootstrapServer(options) {
902
1328
  ...plugin.provisioning ? { provisioning: plugin.provisioning } : {}
903
1329
  }));
904
1330
  const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
1331
+ const workspaceBridgeHandlers = finalPlugins.flatMap((p) => p.workspaceBridgeHandlers ?? []);
905
1332
  const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
906
1333
  return {
907
1334
  registered: finalPlugins.map((p) => p.id),
@@ -912,6 +1339,7 @@ function bootstrapServer(options) {
912
1339
  runtimePlugins,
913
1340
  provisioningContributions,
914
1341
  routeContributions,
1342
+ workspaceBridgeHandlers,
915
1343
  preservedUiStateKeys
916
1344
  };
917
1345
  }
@@ -1886,6 +2314,18 @@ async function resolveDirServerPlugin(entry, ctx) {
1886
2314
  function isDirEntry(entry) {
1887
2315
  return typeof entry === "object" && entry !== null && "dir" in entry;
1888
2316
  }
2317
+ function isTrustedWorkspaceBridgeHandlerEntry(entry) {
2318
+ if (!isDirEntry(entry)) return true;
2319
+ return entry.trust === "internal";
2320
+ }
2321
+ function assertWorkspaceBridgeHandlersTrusted(plugin, entry) {
2322
+ if ((plugin.workspaceBridgeHandlers?.length ?? 0) === 0) return;
2323
+ if (isTrustedWorkspaceBridgeHandlerEntry(entry)) return;
2324
+ const label = isDirEntry(entry) ? resolve5(entry.dir) : plugin.id;
2325
+ throw new Error(
2326
+ `server plugin "${plugin.id}" from ${label} declares workspaceBridgeHandlers, but host bridge handlers are only allowed for pre-built host plugins or directory entries marked trust: "internal"`
2327
+ );
2328
+ }
1889
2329
  async function resolveOnePluginEntry(entry, ctx) {
1890
2330
  if (isDirEntry(entry)) return await resolveDirServerPlugin(entry, ctx);
1891
2331
  return entry;
@@ -1964,6 +2404,16 @@ function createInMemoryBridge() {
1964
2404
  pendingCommands.splice(0, pendingCommands.length - MAX_PENDING_COMMANDS);
1965
2405
  }
1966
2406
  }
2407
+ async function dispatchCommand(cmd) {
2408
+ const seq = nextSeq++;
2409
+ const annotated = { ...cmd, seq };
2410
+ let delivered = false;
2411
+ for (const handler of subscribers) {
2412
+ if (handler(annotated) !== false) delivered = true;
2413
+ }
2414
+ if (!delivered) enqueuePending(annotated);
2415
+ return { seq, status: "ok" };
2416
+ }
1967
2417
  return {
1968
2418
  async getState() {
1969
2419
  return state;
@@ -1972,14 +2422,10 @@ function createInMemoryBridge() {
1972
2422
  state = s;
1973
2423
  },
1974
2424
  async postCommand(cmd) {
1975
- const seq = nextSeq++;
1976
- const annotated = { ...cmd, seq };
1977
- let delivered = false;
1978
- for (const handler of subscribers) {
1979
- if (handler(annotated) !== false) delivered = true;
1980
- }
1981
- if (!delivered) enqueuePending(annotated);
1982
- return { seq, status: "ok" };
2425
+ return dispatchCommand(cmd);
2426
+ },
2427
+ async emitUiEffect(cmd) {
2428
+ return dispatchCommand(cmd);
1983
2429
  },
1984
2430
  subscribeCommands(handler) {
1985
2431
  subscribers.add(handler);
@@ -2068,7 +2514,7 @@ async function validateExistingPath(workspaceRoot, relPath) {
2068
2514
  };
2069
2515
  }
2070
2516
  }
2071
- function createGetUiStateTool(uiBridge) {
2517
+ function createGetUiStateTool(workspaceBridge) {
2072
2518
  return {
2073
2519
  name: "get_ui_state",
2074
2520
  readinessRequirements: ["ui-bridge"],
@@ -2094,7 +2540,7 @@ function createGetUiStateTool(uiBridge) {
2094
2540
  },
2095
2541
  async execute() {
2096
2542
  try {
2097
- const state = await uiBridge.getState();
2543
+ const state = await workspaceBridge.getState();
2098
2544
  return {
2099
2545
  content: [{ type: "text", text: JSON.stringify(state ?? {}) }],
2100
2546
  details: state
@@ -2124,7 +2570,7 @@ function isVerified(kind, params, state) {
2124
2570
  }
2125
2571
  return true;
2126
2572
  }
2127
- function createExecUiTool(uiBridge, opts = {}) {
2573
+ function createExecUiTool(workspaceBridge, opts = {}) {
2128
2574
  const { workspaceRoot, resolvePathKind } = opts;
2129
2575
  const verifyDelayMs = opts.verifyDelayMs ?? 250;
2130
2576
  const verifyRetries = opts.verifyRetries ?? 20;
@@ -2281,7 +2727,7 @@ function createExecUiTool(uiBridge, opts = {}) {
2281
2727
  }
2282
2728
  try {
2283
2729
  const command = { kind: effectiveKind, params: cmdParams };
2284
- const result = await uiBridge.postCommand(command);
2730
+ const result = await workspaceBridge.emitUiEffect(command);
2285
2731
  if (result.status === "error") {
2286
2732
  return {
2287
2733
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -2291,11 +2737,11 @@ function createExecUiTool(uiBridge, opts = {}) {
2291
2737
  }
2292
2738
  if (verifyDelayMs > 0 && VERIFIABLE_KINDS.has(effectiveKind)) {
2293
2739
  await new Promise((r) => setTimeout(r, verifyDelayMs));
2294
- let uiState = await uiBridge.getState();
2740
+ let uiState = await workspaceBridge.getState();
2295
2741
  for (let i = 0; i < verifyRetries; i++) {
2296
2742
  if (uiState === null || isVerified(effectiveKind, cmdParams, uiState)) break;
2297
2743
  await new Promise((r) => setTimeout(r, verifyIntervalMs));
2298
- uiState = await uiBridge.getState();
2744
+ uiState = await workspaceBridge.getState();
2299
2745
  }
2300
2746
  const combined = { ...result, uiState };
2301
2747
  return {
@@ -2314,8 +2760,8 @@ function createExecUiTool(uiBridge, opts = {}) {
2314
2760
  }
2315
2761
  };
2316
2762
  }
2317
- function createWorkspaceUiTools(uiBridge, opts = {}) {
2318
- return [createGetUiStateTool(uiBridge), createExecUiTool(uiBridge, opts)];
2763
+ function createWorkspaceUiTools(workspaceBridge, opts = {}) {
2764
+ return [createGetUiStateTool(workspaceBridge), createExecUiTool(workspaceBridge, opts)];
2319
2765
  }
2320
2766
 
2321
2767
  // src/server/ui-control/http/uiRoutes.ts
@@ -2611,6 +3057,728 @@ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
2611
3057
  done();
2612
3058
  }
2613
3059
 
3060
+ // src/server/workspaceBridge/authPolicy.ts
3061
+ function createLocalCliBridgeAuthPolicy(options) {
3062
+ return {
3063
+ resolve(input) {
3064
+ if (input.callerClass !== "browser") {
3065
+ throw createWorkspaceBridgeError(
3066
+ "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */,
3067
+ "Local CLI auth policy only accepts browser callers"
3068
+ );
3069
+ }
3070
+ ensureCallerAllowed(input.definition, "browser");
3071
+ if (input.workspaceId !== options.workspaceId) {
3072
+ throw createWorkspaceBridgeError(
3073
+ "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */,
3074
+ "Local CLI bridge caller is not authorized for workspace"
3075
+ );
3076
+ }
3077
+ const capabilities = options.capabilities ?? input.definition.requiredCapabilities;
3078
+ ensureCapabilities(capabilities, input.requiredCapabilities ?? input.definition.requiredCapabilities);
3079
+ const context = makeContext({
3080
+ callerClass: "browser",
3081
+ workspaceId: input.workspaceId,
3082
+ sessionId: input.sessionId,
3083
+ pluginId: input.pluginId,
3084
+ capabilities,
3085
+ actor: { actorKind: "human", performedBy: { label: "local-cli:user" } }
3086
+ });
3087
+ return {
3088
+ context,
3089
+ effectiveCapabilities: capabilities,
3090
+ principal: { userId: "local-cli" },
3091
+ resourceScope: { workspaceId: input.workspaceId, sessionId: input.sessionId }
3092
+ };
3093
+ }
3094
+ };
3095
+ }
3096
+ function ensureCallerAllowed(definition, callerClass) {
3097
+ if (!definition.callerClassesAllowed.includes(callerClass)) {
3098
+ throw createWorkspaceBridgeError(
3099
+ "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */,
3100
+ "Bridge caller class is not allowed for operation"
3101
+ );
3102
+ }
3103
+ }
3104
+ function ensureCapabilities(actual, required) {
3105
+ const missing = required.find((capability) => !actual.includes(capability));
3106
+ if (missing) {
3107
+ throw createWorkspaceBridgeError(
3108
+ "BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */,
3109
+ "Bridge caller is missing a required capability"
3110
+ );
3111
+ }
3112
+ }
3113
+ function makeContext(context) {
3114
+ return {
3115
+ ...context,
3116
+ capabilities: [...context.capabilities],
3117
+ actor: sanitizeActor(context.actor)
3118
+ };
3119
+ }
3120
+ function sanitizeActor(actor) {
3121
+ return {
3122
+ actorKind: actor.actorKind,
3123
+ performedBy: actor.performedBy ? { label: actor.performedBy.label, id: actor.performedBy.id } : void 0,
3124
+ onBehalfOf: actor.onBehalfOf ? { label: actor.onBehalfOf.label, id: actor.onBehalfOf.id } : void 0
3125
+ };
3126
+ }
3127
+
3128
+ // src/server/workspaceBridge/httpRoutes.ts
3129
+ import { z as z3 } from "zod";
3130
+
3131
+ // src/server/workspaceBridge/idempotency.ts
3132
+ import { createHash as createHash2 } from "crypto";
3133
+ var InMemoryWorkspaceBridgeIdempotencyStore = class {
3134
+ records = /* @__PURE__ */ new Map();
3135
+ lastGcMs = 0;
3136
+ async begin(options) {
3137
+ const prepared = prepareIdempotency(options);
3138
+ if ("error" in prepared) return { action: "reject", error: prepared.error };
3139
+ const nowMs = options.nowMs ?? Date.now();
3140
+ if (nowMs - this.lastGcMs >= DEFAULT_IDEMPOTENCY_GC_INTERVAL_MS) {
3141
+ await this.gc(nowMs);
3142
+ this.lastGcMs = nowMs;
3143
+ }
3144
+ const existing = this.records.get(prepared.scopeKey);
3145
+ if (existing && Date.parse(existing.expiresAt) > nowMs) {
3146
+ if (existing.inputHash !== prepared.inputHash) {
3147
+ return { action: "reject", error: replayRejectedError() };
3148
+ }
3149
+ return { action: "replay", record: existing };
3150
+ }
3151
+ this.records.set(prepared.scopeKey, createPendingRecord(prepared.scopeKey, prepared.inputHash, nowMs, options.ttlMs));
3152
+ return { action: "execute", scopeKey: prepared.scopeKey, inputHash: prepared.inputHash };
3153
+ }
3154
+ async complete(options) {
3155
+ const existing = this.records.get(options.scopeKey);
3156
+ if (!existing || existing.inputHash !== options.inputHash) return;
3157
+ const nowMs = options.nowMs ?? Date.now();
3158
+ this.records.set(options.scopeKey, {
3159
+ ...existing,
3160
+ // Only successful responses are completed; failures release the key
3161
+ // (see runWithWorkspaceBridgeIdempotency), so there is no "failed" record.
3162
+ status: "completed",
3163
+ response: options.response,
3164
+ updatedAt: new Date(nowMs).toISOString(),
3165
+ expiresAt: new Date(nowMs + (options.ttlMs ?? DEFAULT_IDEMPOTENCY_TTL_MS)).toISOString()
3166
+ });
3167
+ }
3168
+ async release(scopeKey, inputHash) {
3169
+ const existing = this.records.get(scopeKey);
3170
+ if (existing && existing.inputHash === inputHash) this.records.delete(scopeKey);
3171
+ }
3172
+ async gc(nowMs = Date.now()) {
3173
+ let removed = 0;
3174
+ for (const [key, record] of this.records) {
3175
+ if (Date.parse(record.expiresAt) <= nowMs) {
3176
+ this.records.delete(key);
3177
+ removed++;
3178
+ }
3179
+ }
3180
+ return removed;
3181
+ }
3182
+ };
3183
+ async function runWithWorkspaceBridgeIdempotency(store, options, execute) {
3184
+ if (options.definition.idempotencyPolicy === "none") return await execute();
3185
+ if (!store) {
3186
+ return {
3187
+ ok: false,
3188
+ op: options.definition.op,
3189
+ requestId: options.request.requestId,
3190
+ error: createWorkspaceBridgeError(
3191
+ "BRIDGE_UNSUPPORTED_RUNTIME" /* UnsupportedRuntime */,
3192
+ "WorkspaceBridge mutation requires an atomic idempotency store"
3193
+ )
3194
+ };
3195
+ }
3196
+ const begin = await store.begin(options);
3197
+ if (begin.action === "reject") {
3198
+ return { ok: false, op: options.definition.op, requestId: options.request.requestId, error: begin.error };
3199
+ }
3200
+ if (begin.action === "replay") {
3201
+ return begin.record.response ?? {
3202
+ ok: false,
3203
+ op: options.definition.op,
3204
+ requestId: options.request.requestId,
3205
+ error: createWorkspaceBridgeError(
3206
+ "BRIDGE_IDEMPOTENCY_CONFLICT" /* IdempotencyConflict */,
3207
+ "WorkspaceBridge idempotency key is already pending"
3208
+ )
3209
+ };
3210
+ }
3211
+ const response = await execute();
3212
+ if (response.ok) {
3213
+ await store.complete({
3214
+ scopeKey: begin.scopeKey,
3215
+ inputHash: begin.inputHash,
3216
+ response,
3217
+ nowMs: options.nowMs,
3218
+ ttlMs: options.ttlMs
3219
+ });
3220
+ } else if (response.error.code !== "BRIDGE_TIMEOUT" /* Timeout */) {
3221
+ await store.release(begin.scopeKey, begin.inputHash);
3222
+ }
3223
+ return response;
3224
+ }
3225
+ var DEFAULT_IDEMPOTENCY_TTL_MS = 15 * 6e4;
3226
+ var DEFAULT_IDEMPOTENCY_GC_INTERVAL_MS = 6e4;
3227
+ function prepareIdempotency(options) {
3228
+ const key = semanticKey(options);
3229
+ if (!key) {
3230
+ return {
3231
+ error: createWorkspaceBridgeError(
3232
+ "BRIDGE_IDEMPOTENCY_REQUIRED" /* IdempotencyRequired */,
3233
+ "WorkspaceBridge operation requires an idempotency key"
3234
+ )
3235
+ };
3236
+ }
3237
+ return {
3238
+ scopeKey: stableStringify({
3239
+ workspaceId: options.auth.workspaceId,
3240
+ sessionId: options.auth.sessionId,
3241
+ pluginId: options.auth.pluginId,
3242
+ op: options.definition.op,
3243
+ key
3244
+ }),
3245
+ inputHash: hashNormalizedInput(options.request.input)
3246
+ };
3247
+ }
3248
+ function semanticKey(options) {
3249
+ if (options.definition.idempotencyPolicy === "none") return "none";
3250
+ if (options.definition.idempotencyPolicy === "required") return options.request.idempotencyKey;
3251
+ if (options.definition.idempotencyPolicy === "request-id") return options.request.requestId ?? options.request.idempotencyKey;
3252
+ return void 0;
3253
+ }
3254
+ function hashNormalizedInput(input) {
3255
+ return hashString(stableStringify(input));
3256
+ }
3257
+ function createPendingRecord(scopeKey, inputHash, nowMs, ttlMs = DEFAULT_IDEMPOTENCY_TTL_MS) {
3258
+ const now = new Date(nowMs).toISOString();
3259
+ return {
3260
+ scopeKey,
3261
+ inputHash,
3262
+ status: "pending",
3263
+ createdAt: now,
3264
+ updatedAt: now,
3265
+ expiresAt: new Date(nowMs + ttlMs).toISOString()
3266
+ };
3267
+ }
3268
+ function replayRejectedError() {
3269
+ return createWorkspaceBridgeError(
3270
+ "BRIDGE_REPLAY_REJECTED" /* ReplayRejected */,
3271
+ "WorkspaceBridge idempotency key was reused with a different payload"
3272
+ );
3273
+ }
3274
+ function hashString(value) {
3275
+ return createHash2("sha256").update(value).digest("hex");
3276
+ }
3277
+
3278
+ // src/server/workspaceBridge/runtimeToken.ts
3279
+ import { createHmac, randomUUID, timingSafeEqual } from "crypto";
3280
+ var WORKSPACE_BRIDGE_TOKEN_AUDIENCE = "workspace-bridge";
3281
+ var WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE = "workspace-bridge-refresh";
3282
+ var DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS = 5 * 6e4;
3283
+ var DEFAULT_WORKSPACE_BRIDGE_RUNTIME_REFRESH_TOKEN_TTL_MS = 60 * 6e4;
3284
+ var MAX_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS = 15 * 6e4;
3285
+ function mintWorkspaceBridgeRuntimeToken(options) {
3286
+ return mintWorkspaceBridgeToken({
3287
+ ...options,
3288
+ audience: WORKSPACE_BRIDGE_TOKEN_AUDIENCE,
3289
+ ttlMs: options.ttlMs ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS
3290
+ });
3291
+ }
3292
+ function mintWorkspaceBridgeRuntimeRefreshToken(options) {
3293
+ return mintWorkspaceBridgeToken({
3294
+ ...options,
3295
+ audience: WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE,
3296
+ // Refresh tokens intentionally outlive short call tokens, but remain
3297
+ // sandbox-bound by workspace/session/runtime/capabilities claims.
3298
+ ttlMs: options.ttlMs ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_REFRESH_TOKEN_TTL_MS,
3299
+ tokenTtlMs: clampWorkspaceBridgeRuntimeTokenTtlMs(options.tokenTtlMs)
3300
+ });
3301
+ }
3302
+ function verifyWorkspaceBridgeRuntimeToken(token, options) {
3303
+ assertUsableSecret(options.secret);
3304
+ const claims = parseAndVerifyToken(token, options.secret);
3305
+ const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
3306
+ ensureLiveTokenClaims(claims, now, WORKSPACE_BRIDGE_TOKEN_AUDIENCE, "Runtime bridge token");
3307
+ const missingCapability = (options.requiredCapabilities ?? []).find(
3308
+ (capability) => !claims.capabilities.includes(capability)
3309
+ );
3310
+ if (missingCapability) {
3311
+ throw bridgeTokenError("BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */, "Runtime bridge token is missing a required capability");
3312
+ }
3313
+ const runtimeClaims = claims;
3314
+ return {
3315
+ claims: runtimeClaims,
3316
+ authContext: runtimeClaimsToBridgeAuthContext(runtimeClaims)
3317
+ };
3318
+ }
3319
+ function verifyWorkspaceBridgeRuntimeRefreshToken(token, options) {
3320
+ assertUsableSecret(options.secret);
3321
+ const claims = parseAndVerifyToken(token, options.secret);
3322
+ const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
3323
+ ensureLiveTokenClaims(claims, now, WORKSPACE_BRIDGE_REFRESH_TOKEN_AUDIENCE, "Runtime bridge refresh token");
3324
+ return { claims };
3325
+ }
3326
+ function clampWorkspaceBridgeRuntimeTokenTtlMs(ttlMs) {
3327
+ if (ttlMs === void 0) return void 0;
3328
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) return void 0;
3329
+ return Math.min(ttlMs, MAX_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS);
3330
+ }
3331
+ function runtimeClaimsToBridgeAuthContext(claims) {
3332
+ return {
3333
+ callerClass: "runtime",
3334
+ workspaceId: claims.workspaceId,
3335
+ sessionId: claims.sessionId,
3336
+ capabilities: claims.capabilities,
3337
+ tokenId: claims.jti,
3338
+ expiresAt: new Date(claims.exp * 1e3).toISOString(),
3339
+ actor: {
3340
+ actorKind: "agent",
3341
+ performedBy: {
3342
+ label: claims.runtimeId ? `runtime:${claims.runtimeId}` : "runtime:agent",
3343
+ id: claims.runtimeId
3344
+ },
3345
+ onBehalfOf: claims.sessionId ? { label: `session:${claims.sessionId}` } : void 0
3346
+ }
3347
+ };
3348
+ }
3349
+ function mintWorkspaceBridgeToken(options) {
3350
+ assertUsableSecret(options.secret);
3351
+ const nowMs = options.nowMs ?? Date.now();
3352
+ const claims = {
3353
+ aud: options.audience,
3354
+ workspaceId: options.workspaceId,
3355
+ sessionId: options.sessionId,
3356
+ runtimeId: options.runtimeId,
3357
+ capabilities: [...options.capabilities],
3358
+ iat: Math.floor(nowMs / 1e3),
3359
+ exp: Math.floor((nowMs + options.ttlMs) / 1e3),
3360
+ jti: options.jti ?? randomUUID(),
3361
+ ...options.tokenTtlMs !== void 0 ? { tokenTtlMs: options.tokenTtlMs } : {}
3362
+ };
3363
+ return signClaims(claims, options.secret);
3364
+ }
3365
+ function ensureLiveTokenClaims(claims, now, expectedAudience, label) {
3366
+ if (claims.aud !== expectedAudience) {
3367
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, `${label} has invalid audience`);
3368
+ }
3369
+ if (claims.exp <= now) {
3370
+ throw bridgeTokenError("BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */, `${label} has expired`);
3371
+ }
3372
+ if (claims.iat > now + 60) {
3373
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, `${label} is not valid yet`);
3374
+ }
3375
+ }
3376
+ function signClaims(claims, secret) {
3377
+ const header = { alg: "HS256", typ: "JWT" };
3378
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
3379
+ const encodedPayload = base64UrlEncode(JSON.stringify(claims));
3380
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
3381
+ return `${signingInput}.${hmac(signingInput, secret)}`;
3382
+ }
3383
+ function parseAndVerifyToken(token, secret) {
3384
+ const parts = token.split(".");
3385
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
3386
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token is malformed");
3387
+ }
3388
+ const [headerPart, payloadPart, signature] = parts;
3389
+ const signingInput = `${headerPart}.${payloadPart}`;
3390
+ const expected = hmac(signingInput, secret);
3391
+ if (!safeEqual(signature, expected)) {
3392
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token signature is invalid");
3393
+ }
3394
+ let header;
3395
+ let payload;
3396
+ try {
3397
+ header = JSON.parse(base64UrlDecode(headerPart));
3398
+ payload = JSON.parse(base64UrlDecode(payloadPart));
3399
+ } catch {
3400
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token payload is invalid");
3401
+ }
3402
+ if (!header || typeof header !== "object" || header.alg !== "HS256") {
3403
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token algorithm is invalid");
3404
+ }
3405
+ return parseClaims(payload);
3406
+ }
3407
+ function parseClaims(payload) {
3408
+ if (!payload || typeof payload !== "object") {
3409
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
3410
+ }
3411
+ const claims = payload;
3412
+ if (typeof claims.aud !== "string" || typeof claims.workspaceId !== "string" || !Array.isArray(claims.capabilities) || !claims.capabilities.every((capability) => typeof capability === "string") || typeof claims.iat !== "number" || typeof claims.exp !== "number" || typeof claims.jti !== "string") {
3413
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
3414
+ }
3415
+ if (claims.tokenTtlMs !== void 0 && (typeof claims.tokenTtlMs !== "number" || !Number.isFinite(claims.tokenTtlMs) || claims.tokenTtlMs <= 0)) {
3416
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token claims are invalid");
3417
+ }
3418
+ return {
3419
+ aud: claims.aud,
3420
+ workspaceId: claims.workspaceId,
3421
+ sessionId: optionalString(claims.sessionId),
3422
+ runtimeId: optionalString(claims.runtimeId),
3423
+ capabilities: [...claims.capabilities],
3424
+ iat: claims.iat,
3425
+ exp: claims.exp,
3426
+ jti: claims.jti,
3427
+ ...typeof claims.tokenTtlMs === "number" ? { tokenTtlMs: clampWorkspaceBridgeRuntimeTokenTtlMs(claims.tokenTtlMs) } : {}
3428
+ };
3429
+ }
3430
+ function bridgeTokenError(code, message) {
3431
+ return createWorkspaceBridgeError(code, message);
3432
+ }
3433
+ function assertUsableSecret(secret) {
3434
+ if (secret.length < 32) {
3435
+ throw bridgeTokenError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "Runtime bridge token secret is too short");
3436
+ }
3437
+ }
3438
+ function hmac(value, secret) {
3439
+ return createHmac("sha256", secret).update(value).digest("base64url");
3440
+ }
3441
+ function safeEqual(actual, expected) {
3442
+ const actualBuffer = Buffer.from(actual);
3443
+ const expectedBuffer = Buffer.from(expected);
3444
+ return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
3445
+ }
3446
+ function base64UrlEncode(value) {
3447
+ return Buffer.from(value).toString("base64url");
3448
+ }
3449
+ function base64UrlDecode(value) {
3450
+ return Buffer.from(value, "base64url").toString("utf8");
3451
+ }
3452
+ function optionalString(value) {
3453
+ return typeof value === "string" ? value : void 0;
3454
+ }
3455
+
3456
+ // src/server/workspaceBridge/refreshTokenStore.ts
3457
+ var InMemoryWorkspaceBridgeRuntimeRefreshTokenStore = class {
3458
+ revoked = /* @__PURE__ */ new Map();
3459
+ rateLimits = /* @__PURE__ */ new Map();
3460
+ lastGcMs = 0;
3461
+ revoke(jti, expiresAtMs) {
3462
+ this.gc();
3463
+ this.revoked.set(jti, expiresAtMs);
3464
+ this.rateLimits.delete(jti);
3465
+ }
3466
+ recordUse(options) {
3467
+ const nowMs = options.nowMs ?? Date.now();
3468
+ this.gc(nowMs);
3469
+ if (this.revoked.has(options.jti)) return { allowed: false, reason: "revoked" };
3470
+ if (options.maxUses <= 0) return { allowed: false, reason: "rate-limited", retryAfterMs: options.windowMs };
3471
+ const existing = this.rateLimits.get(options.jti);
3472
+ if (!existing || nowMs - existing.windowStartMs >= existing.windowMs || isExpired(existing.expiresAtMs, nowMs)) {
3473
+ this.rateLimits.set(options.jti, {
3474
+ windowStartMs: nowMs,
3475
+ windowMs: options.windowMs,
3476
+ count: 1,
3477
+ expiresAtMs: options.expiresAtMs
3478
+ });
3479
+ return { allowed: true };
3480
+ }
3481
+ if (existing.count >= options.maxUses) {
3482
+ return { allowed: false, reason: "rate-limited", retryAfterMs: Math.max(0, existing.windowStartMs + existing.windowMs - nowMs) };
3483
+ }
3484
+ existing.count += 1;
3485
+ existing.windowMs = options.windowMs;
3486
+ existing.expiresAtMs = options.expiresAtMs ?? existing.expiresAtMs;
3487
+ return { allowed: true };
3488
+ }
3489
+ gc(nowMs = Date.now()) {
3490
+ if (nowMs - this.lastGcMs < 6e4) return 0;
3491
+ this.lastGcMs = nowMs;
3492
+ let removed = 0;
3493
+ for (const [jti, record] of this.rateLimits) {
3494
+ if (isExpired(record.expiresAtMs, nowMs) || nowMs - record.windowStartMs >= record.windowMs) {
3495
+ this.rateLimits.delete(jti);
3496
+ removed += 1;
3497
+ }
3498
+ }
3499
+ for (const [jti, expiresAtMs] of this.revoked) {
3500
+ if (isExpired(expiresAtMs, nowMs)) {
3501
+ this.revoked.delete(jti);
3502
+ removed += 1;
3503
+ }
3504
+ }
3505
+ return removed;
3506
+ }
3507
+ };
3508
+ function isExpired(expiresAtMs, nowMs) {
3509
+ return expiresAtMs !== void 0 && expiresAtMs <= nowMs;
3510
+ }
3511
+
3512
+ // src/server/workspaceBridge/httpRoutes.ts
3513
+ var bridgeCallBodySchema = z3.object({
3514
+ op: z3.string().min(1),
3515
+ input: z3.unknown().default({}),
3516
+ requestId: z3.string().optional(),
3517
+ idempotencyKey: z3.string().optional()
3518
+ });
3519
+ var DEFAULT_REFRESH_TOKEN_RATE_LIMIT_MAX_USES = 30;
3520
+ var DEFAULT_REFRESH_TOKEN_RATE_LIMIT_WINDOW_MS = 6e4;
3521
+ function workspaceBridgeHttpRoutes(app, opts, done) {
3522
+ const defaultRefreshTokenStore = opts.runtimeRefreshTokenStore ?? new InMemoryWorkspaceBridgeRuntimeRefreshTokenStore();
3523
+ app.post("/api/v1/workspace-bridge/call", async (request, reply) => {
3524
+ reply.header("Cache-Control", "no-store");
3525
+ const contentType = String(request.headers["content-type"] ?? "");
3526
+ if (!contentType.toLowerCase().startsWith("application/json")) {
3527
+ return sendBridgeError(reply, 415, void 0, "BRIDGE_INVALID_REQUEST" /* InvalidRequest */, "WorkspaceBridge transport requires application/json");
3528
+ }
3529
+ const rawBodySize = measureJsonBytes(request.body);
3530
+ if (opts.maxBodyBytes && rawBodySize > opts.maxBodyBytes) {
3531
+ return sendBridgeError(reply, 413, void 0, "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */, "WorkspaceBridge request body is too large");
3532
+ }
3533
+ const parsed = bridgeCallBodySchema.safeParse(request.body);
3534
+ if (!parsed.success) {
3535
+ return sendBridgeError(reply, 400, void 0, "BRIDGE_SCHEMA_INVALID" /* SchemaInvalid */, "WorkspaceBridge request body is invalid");
3536
+ }
3537
+ const body = { ...parsed.data, input: parsed.data.input ?? {} };
3538
+ try {
3539
+ const registry = await resolveRegistry(request, body, opts);
3540
+ const definition = registry.getDefinition(body.op);
3541
+ if (!definition) {
3542
+ return sendBridgeError(reply, statusForBridgeError("BRIDGE_OP_NOT_FOUND" /* OpNotFound */), body.requestId, "BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge operation is not registered");
3543
+ }
3544
+ const authHeader = firstHeader(request.headers.authorization);
3545
+ const authContext = authHeader?.startsWith("Bearer ") ? resolveRuntimeContext(authHeader.slice("Bearer ".length), opts, definition) : await resolveBrowserContext(request, opts, definition, body);
3546
+ const idempotencyStore = opts.getIdempotencyStore ? await opts.getIdempotencyStore(request, body) : opts.idempotencyStore;
3547
+ const expectedWorkspaceId = opts.getOwnerWorkspaceId ? await opts.getOwnerWorkspaceId(request, body, authContext) : opts.ownerWorkspaceId;
3548
+ const response = await runWithWorkspaceBridgeIdempotency(idempotencyStore, {
3549
+ definition,
3550
+ request: body,
3551
+ auth: authContext
3552
+ }, async () => await registry.call(body, authContext, { expectedWorkspaceId }));
3553
+ return await sendResponse(reply, response);
3554
+ } catch (err) {
3555
+ const bridgeError = isBridgeError(err) ? err : createWorkspaceBridgeError("BRIDGE_HANDLER_FAILED" /* HandlerFailed */, "WorkspaceBridge transport failed");
3556
+ return sendBridgeError(reply, statusForBridgeError(bridgeError.code), body.requestId, bridgeError.code, bridgeError.message);
3557
+ }
3558
+ });
3559
+ app.post("/api/v1/workspace-bridge/token", async (request, reply) => {
3560
+ reply.header("Cache-Control", "no-store");
3561
+ const authHeader = firstHeader(request.headers.authorization);
3562
+ if (!authHeader?.startsWith("Bearer ")) {
3563
+ return sendBridgeError(reply, 401, void 0, "BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "WorkspaceBridge refresh token is required");
3564
+ }
3565
+ if (!opts.runtimeTokenSecret || !opts.runtimeRefreshTokenSecret) {
3566
+ return sendBridgeError(reply, 401, void 0, "BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "WorkspaceBridge token refresh is not configured");
3567
+ }
3568
+ try {
3569
+ const nowMs = Date.now();
3570
+ const verified = verifyWorkspaceBridgeRuntimeRefreshToken(authHeader.slice("Bearer ".length), {
3571
+ secret: opts.runtimeRefreshTokenSecret,
3572
+ nowMs
3573
+ });
3574
+ const store = opts.getRuntimeRefreshTokenStore ? await opts.getRuntimeRefreshTokenStore(request, verified.claims) ?? defaultRefreshTokenStore : defaultRefreshTokenStore;
3575
+ const refreshUse = await store.recordUse({
3576
+ jti: verified.claims.jti,
3577
+ nowMs,
3578
+ maxUses: opts.refreshTokenRateLimit?.maxUses ?? DEFAULT_REFRESH_TOKEN_RATE_LIMIT_MAX_USES,
3579
+ windowMs: opts.refreshTokenRateLimit?.windowMs ?? DEFAULT_REFRESH_TOKEN_RATE_LIMIT_WINDOW_MS,
3580
+ expiresAtMs: verified.claims.exp * 1e3
3581
+ });
3582
+ if (!refreshUse.allowed) {
3583
+ if (refreshUse.reason === "revoked") {
3584
+ return sendBridgeError(reply, 401, void 0, "BRIDGE_INVALID_TOKEN" /* InvalidToken */, "WorkspaceBridge refresh token is revoked");
3585
+ }
3586
+ reply.header("Retry-After", Math.ceil(refreshUse.retryAfterMs / 1e3).toString());
3587
+ return sendBridgeError(reply, 429, void 0, "BRIDGE_RATE_LIMITED" /* RateLimited */, "WorkspaceBridge refresh token rate limit exceeded");
3588
+ }
3589
+ const ttlMs = refreshMintTtlMs(verified.claims, Date.now());
3590
+ if (ttlMs === void 0) {
3591
+ return sendBridgeError(reply, 401, void 0, "BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */, "Runtime bridge refresh token has expired");
3592
+ }
3593
+ const token = mintWorkspaceBridgeRuntimeToken({
3594
+ secret: opts.runtimeTokenSecret,
3595
+ workspaceId: verified.claims.workspaceId,
3596
+ sessionId: verified.claims.sessionId,
3597
+ runtimeId: verified.claims.runtimeId,
3598
+ capabilities: verified.claims.capabilities,
3599
+ ttlMs
3600
+ });
3601
+ return reply.code(200).send({ ok: true, token });
3602
+ } catch (err) {
3603
+ const bridgeError = isBridgeError(err) ? err : createWorkspaceBridgeError("BRIDGE_INVALID_TOKEN" /* InvalidToken */, "WorkspaceBridge refresh token is invalid");
3604
+ return sendBridgeError(reply, statusForBridgeError(bridgeError.code), void 0, bridgeError.code, bridgeError.message);
3605
+ }
3606
+ });
3607
+ done();
3608
+ }
3609
+ async function resolveRegistry(request, body, opts) {
3610
+ const registry = opts.getRegistry ? await opts.getRegistry(request, body) : opts.registry;
3611
+ if (!registry) {
3612
+ throw createWorkspaceBridgeError("BRIDGE_OP_NOT_FOUND" /* OpNotFound */, "WorkspaceBridge registry is not configured");
3613
+ }
3614
+ return registry;
3615
+ }
3616
+ function resolveRuntimeContext(token, opts, definition) {
3617
+ if (!opts.runtimeTokenSecret) {
3618
+ throw createWorkspaceBridgeError("BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "Runtime bridge token auth is not configured");
3619
+ }
3620
+ return verifyWorkspaceBridgeRuntimeToken(token, {
3621
+ secret: opts.runtimeTokenSecret,
3622
+ requiredCapabilities: definition.requiredCapabilities
3623
+ }).authContext;
3624
+ }
3625
+ async function resolveBrowserContext(request, opts, definition, body) {
3626
+ if (!opts.browserAuthPolicy) {
3627
+ throw createWorkspaceBridgeError("BRIDGE_AUTH_REQUIRED" /* AuthRequired */, "Browser bridge auth is not configured");
3628
+ }
3629
+ const workspaceId = firstHeader(request.headers["x-boring-workspace-id"]) ?? "default";
3630
+ const sessionId = firstHeader(request.headers["x-boring-session-id"]);
3631
+ return (await opts.browserAuthPolicy.resolve({
3632
+ callerClass: "browser",
3633
+ definition,
3634
+ workspaceId,
3635
+ sessionId,
3636
+ request: { headers: request.headers, method: request.method, user: request.user },
3637
+ body
3638
+ })).context;
3639
+ }
3640
+ async function sendResponse(reply, response) {
3641
+ return reply.code(response.ok ? 200 : statusForBridgeError(response.error.code)).send(response);
3642
+ }
3643
+ function refreshMintTtlMs(claims, nowMs) {
3644
+ const requested = clampWorkspaceBridgeRuntimeTokenTtlMs(claims.tokenTtlMs) ?? DEFAULT_WORKSPACE_BRIDGE_RUNTIME_TOKEN_TTL_MS;
3645
+ const remaining = claims.exp * 1e3 - nowMs;
3646
+ if (remaining <= 0) return void 0;
3647
+ return Math.min(requested, remaining);
3648
+ }
3649
+ function sendBridgeError(reply, status, requestId, code, message) {
3650
+ reply.header("Cache-Control", "no-store");
3651
+ return reply.code(status).send({ ok: false, requestId, error: { code, message } });
3652
+ }
3653
+ function statusForBridgeError(code) {
3654
+ if (code === "BRIDGE_AUTH_REQUIRED" /* AuthRequired */ || code === "BRIDGE_INVALID_TOKEN" /* InvalidToken */ || code === "BRIDGE_EXPIRED_TOKEN" /* ExpiredToken */) return 401;
3655
+ if (code === "BRIDGE_RATE_LIMITED" /* RateLimited */) return 429;
3656
+ if (code === "BRIDGE_CALLER_NOT_ALLOWED" /* CallerNotAllowed */ || code === "BRIDGE_CAPABILITY_DENIED" /* CapabilityDenied */ || code === "BRIDGE_RESOURCE_SCOPE_DENIED" /* ResourceScopeDenied */) return 403;
3657
+ if (code === "BRIDGE_OP_NOT_FOUND" /* OpNotFound */) return 404;
3658
+ if (code === "BRIDGE_INPUT_TOO_LARGE" /* InputTooLarge */ || code === "BRIDGE_OUTPUT_TOO_LARGE" /* OutputTooLarge */) return 413;
3659
+ return 400;
3660
+ }
3661
+ function firstHeader(value) {
3662
+ return Array.isArray(value) ? value[0] : value;
3663
+ }
3664
+ function isBridgeError(err) {
3665
+ return !!err && typeof err === "object" && "code" in err && "message" in err;
3666
+ }
3667
+
3668
+ // src/server/workspaceBridge/runtimeEnv.ts
3669
+ var BRIDGE_CALL_PATH = "/api/v1/workspace-bridge/call";
3670
+ var BRIDGE_TOKEN_PATH = "/api/v1/workspace-bridge/token";
3671
+ function createWorkspaceBridgeRuntimeEnvContribution(options) {
3672
+ const enabled = options.runtimeEnv?.enabled ?? Boolean(options.runtimeEnv?.bridgeUrl);
3673
+ if (!enabled) return void 0;
3674
+ const bridgeUrl = resolveBridgeCallUrl(options.runtimeEnv?.bridgeUrl);
3675
+ const tokenUrl = resolveBridgeTokenUrl(options.runtimeEnv?.bridgeUrl);
3676
+ const capabilities = options.runtimeEnv?.capabilities;
3677
+ return {
3678
+ id: "workspace-bridge-runtime-env",
3679
+ getEnv: (ctx) => {
3680
+ const runtimePlacement = resolveRuntimePlacement(ctx?.runtimeBundle, options.runtimePlacement);
3681
+ const disabledReason = validateRuntimeBridgeUrl({
3682
+ bridgeUrl,
3683
+ runtimePlacement,
3684
+ allowInsecureHttp: options.runtimeEnv?.allowInsecureHttp,
3685
+ hasRuntimeTokenSecret: Boolean(options.runtimeTokenSecret),
3686
+ hasCapabilities: Array.isArray(capabilities) && capabilities.length > 0
3687
+ });
3688
+ if (disabledReason) {
3689
+ return { [WORKSPACE_BRIDGE_DISABLED_ENV]: disabledReason };
3690
+ }
3691
+ const token = mintWorkspaceBridgeRuntimeToken({
3692
+ secret: options.runtimeTokenSecret,
3693
+ workspaceId: options.workspaceId,
3694
+ sessionId: options.runtimeEnv?.sessionId,
3695
+ runtimeId: options.runtimeMode,
3696
+ capabilities,
3697
+ ttlMs: options.runtimeEnv?.tokenTtlMs
3698
+ });
3699
+ const refreshToken = options.runtimeRefreshTokenSecret && tokenUrl && isRefreshTokenUrlSafe(tokenUrl) ? mintWorkspaceBridgeRuntimeRefreshToken({
3700
+ secret: options.runtimeRefreshTokenSecret,
3701
+ workspaceId: options.workspaceId,
3702
+ sessionId: options.runtimeEnv?.sessionId,
3703
+ runtimeId: options.runtimeMode,
3704
+ capabilities,
3705
+ ttlMs: options.runtimeEnv?.refreshTokenTtlMs,
3706
+ tokenTtlMs: options.runtimeEnv?.tokenTtlMs
3707
+ }) : void 0;
3708
+ return {
3709
+ [WORKSPACE_BRIDGE_URL_ENV]: bridgeUrl,
3710
+ [WORKSPACE_BRIDGE_TOKEN_ENV]: token,
3711
+ ...refreshToken ? {
3712
+ [WORKSPACE_BRIDGE_TOKEN_URL_ENV]: tokenUrl,
3713
+ [WORKSPACE_BRIDGE_REFRESH_TOKEN_ENV]: refreshToken
3714
+ } : {},
3715
+ BORING_WORKSPACE_ID: options.workspaceId,
3716
+ BORING_AGENT_SESSION_ID: options.runtimeEnv?.sessionId ?? options.workspaceId
3717
+ };
3718
+ }
3719
+ };
3720
+ }
3721
+ function resolveBridgeCallUrl(value) {
3722
+ return resolveBridgeUrl(value, BRIDGE_CALL_PATH);
3723
+ }
3724
+ function resolveBridgeTokenUrl(value) {
3725
+ return resolveBridgeUrl(value, BRIDGE_TOKEN_PATH);
3726
+ }
3727
+ function resolveBridgeUrl(value, path) {
3728
+ if (!value?.trim()) return void 0;
3729
+ try {
3730
+ const url = new URL(value);
3731
+ if (url.pathname === "" || url.pathname === "/" || url.pathname === BRIDGE_CALL_PATH || url.pathname === BRIDGE_TOKEN_PATH) {
3732
+ url.pathname = path;
3733
+ }
3734
+ return url.toString();
3735
+ } catch {
3736
+ return void 0;
3737
+ }
3738
+ }
3739
+ function isRefreshTokenUrlSafe(value) {
3740
+ try {
3741
+ const url = new URL(value);
3742
+ return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHost(url.hostname);
3743
+ } catch {
3744
+ return false;
3745
+ }
3746
+ }
3747
+ function isLoopbackHost(hostname) {
3748
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
3749
+ }
3750
+ function resolveRuntimePlacement(runtimeBundle, fallback) {
3751
+ if (runtimeBundle?.filesystem?.kind === "remote-workspace" || runtimeBundle?.bash?.kind === "remote") return "remote";
3752
+ return fallback ?? "local";
3753
+ }
3754
+ function validateRuntimeBridgeUrl(options) {
3755
+ if (!options.bridgeUrl) return "bridge-url-missing";
3756
+ if (!options.hasRuntimeTokenSecret) return "runtime-token-secret-missing";
3757
+ if (!options.hasCapabilities) return "runtime-capabilities-missing";
3758
+ let url;
3759
+ try {
3760
+ url = new URL(options.bridgeUrl);
3761
+ } catch {
3762
+ return "bridge-url-invalid";
3763
+ }
3764
+ const isRemote = options.runtimePlacement === "remote";
3765
+ const isLocalhost = isLoopbackHost(url.hostname);
3766
+ if (isRemote && url.protocol !== "https:") return "remote-bridge-url-must-be-https";
3767
+ if (isRemote && isLocalhost) return "remote-bridge-url-must-not-be-localhost";
3768
+ if (url.protocol === "http:" && !options.allowInsecureHttp && !isLocalhost) return "remote-bridge-url-must-be-https";
3769
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "bridge-url-invalid";
3770
+ return void 0;
3771
+ }
3772
+
3773
+ // src/server/workspaceBridge/runtimeCore.ts
3774
+ function createWorkspaceBridgeRuntimeCore(options = {}) {
3775
+ const registry = options.registry ?? createWorkspaceBridgeRegistry({ ownerWorkspaceId: options.ownerWorkspaceId });
3776
+ for (const entry of options.handlers ?? []) {
3777
+ registry.registerHandler(entry.definition, entry.handler);
3778
+ }
3779
+ return { registry };
3780
+ }
3781
+
2614
3782
  // src/app/server/createWorkspaceAgentServer.ts
2615
3783
  var __dirname = dirname7(fileURLToPath(import.meta.url));
2616
3784
  var require4 = createRequire4(import.meta.url);
@@ -2758,6 +3926,7 @@ function collectWorkspaceAgentServerPlugins(opts = {}) {
2758
3926
  ...result.runtimePlugins
2759
3927
  ],
2760
3928
  routeContributions: result.routeContributions,
3929
+ workspaceBridgeHandlers: result.workspaceBridgeHandlers,
2761
3930
  preservedUiStateKeys: result.preservedUiStateKeys,
2762
3931
  agentOptions: {
2763
3932
  extraTools: result.agentTools,
@@ -2919,6 +4088,23 @@ function readWorkspacePluginPackagePiSnapshot(pluginDirs) {
2919
4088
  return emptyPackageJsonPiSnapshot();
2920
4089
  }
2921
4090
  }
4091
+ function resolveWorkspaceBridgeBrowserAuthPolicy(opts, registry) {
4092
+ if (opts.workspaceBridge?.browserAuthPolicy) return opts.workspaceBridge.browserAuthPolicy;
4093
+ if (opts.workspaceBridge?.allowInsecureLocalCliBrowserAuth !== true) return void 0;
4094
+ emitLocalCliBridgeAuthWarning();
4095
+ return createLocalCliBridgeAuthPolicy({
4096
+ workspaceId: "default",
4097
+ capabilities: registry.listDefinitions().flatMap((definition) => [...definition.requiredCapabilities])
4098
+ });
4099
+ }
4100
+ function emitLocalCliBridgeAuthWarning() {
4101
+ const message = "createWorkspaceAgentServer is using createLocalCliBridgeAuthPolicy for WorkspaceBridge browser calls. This policy is unauthenticated, grants registered bridge capabilities to a fixed local-cli principal, and is intended only for local/dev CLI usage. Provide workspaceBridge.browserAuthPolicy before exposing this server.";
4102
+ if (typeof process.emitWarning === "function") {
4103
+ process.emitWarning(message, { code: "BORING_WORKSPACE_BRIDGE_INSECURE_AUTH" });
4104
+ return;
4105
+ }
4106
+ console.warn(message);
4107
+ }
2922
4108
  async function createWorkspaceAgentServer(opts = {}) {
2923
4109
  const workspaceRoot = opts.workspaceRoot ?? process.cwd();
2924
4110
  const bridge = createInMemoryBridge();
@@ -2937,13 +4123,17 @@ async function createWorkspaceAgentServer(opts = {}) {
2937
4123
  defaultPluginPackages: opts.defaultPluginPackages,
2938
4124
  anchorDir: opts.appRoot
2939
4125
  });
2940
- const defaultPluginDirEntries = defaultPluginPackagePaths.map((dir) => ({ dir, hotReload: true })).filter((entry) => hasDirServerPlugin(entry));
4126
+ const defaultPluginDirEntries = defaultPluginPackagePaths.map((dir) => ({ dir, hotReload: true, trust: "internal" })).filter((entry) => hasDirServerPlugin(entry));
2941
4127
  const allPluginEntries = [
2942
4128
  ...defaultPluginDirEntries,
2943
4129
  ...opts.plugins ?? []
2944
4130
  ];
2945
4131
  const resolvedPlugins = await Promise.all(
2946
- allPluginEntries.map((entry) => resolveOnePluginEntry(entry, ctx))
4132
+ allPluginEntries.map(async (entry) => {
4133
+ const plugin = await resolveOnePluginEntry(entry, ctx);
4134
+ assertWorkspaceBridgeHandlersTrusted(plugin, entry);
4135
+ return plugin;
4136
+ })
2947
4137
  );
2948
4138
  const pluginAuthoringEnabled = externalPluginsEnabled && (opts.installPluginAuthoring ?? workspaceFsCapability === "strong") && !(opts.excludeDefaults ?? []).includes("boring-ui-plugin-cli-package");
2949
4139
  const pluginCollection = collectWorkspaceAgentServerPlugins({
@@ -2951,6 +4141,14 @@ async function createWorkspaceAgentServer(opts = {}) {
2951
4141
  plugins: resolvedPlugins,
2952
4142
  installPluginAuthoring: pluginAuthoringEnabled
2953
4143
  });
4144
+ const { registry: workspaceBridgeRegistry } = createWorkspaceBridgeRuntimeCore({
4145
+ registry: opts.workspaceBridge?.registry,
4146
+ ownerWorkspaceId: "default",
4147
+ handlers: [
4148
+ ...opts.workspaceBridge?.handlers ?? [],
4149
+ ...pluginCollection.workspaceBridgeHandlers ?? []
4150
+ ]
4151
+ });
2954
4152
  const workspacePackagePiPackage = pluginAuthoringEnabled ? createBoringPiPackageSource(workspaceRoot) : void 0;
2955
4153
  const baseStaticPiSkillPaths = [
2956
4154
  ...pluginAuthoringEnabled ? resolveBoringPiSkillPaths(workspaceRoot) : [],
@@ -3027,11 +4225,28 @@ async function createWorkspaceAgentServer(opts = {}) {
3027
4225
  const rebuildPlugins = async () => {
3028
4226
  return rebuildServerPlugins({ entries: allPluginEntries, ctx });
3029
4227
  };
4228
+ const callerRuntimeProvisioner = opts.runtimeProvisioner;
4229
+ const boringUiCliCommandAvailable = opts.provisionWorkspace !== false && pluginCollection.provisioningContributions.some(
4230
+ (entry) => entry.id === "boring-ui-cli-package"
4231
+ );
4232
+ const workspaceBridgeRuntimeEnvContribution = createWorkspaceBridgeRuntimeEnvContribution({
4233
+ workspaceId: "default",
4234
+ runtimeMode: resolvedMode,
4235
+ registry: workspaceBridgeRegistry,
4236
+ runtimeTokenSecret: opts.workspaceBridge?.runtimeTokenSecret,
4237
+ runtimeRefreshTokenSecret: opts.workspaceBridge?.runtimeRefreshTokenSecret,
4238
+ runtimeEnv: opts.workspaceBridge?.runtimeEnv,
4239
+ runtimePlacement: workspaceFsCapability === "strong" ? "local" : "remote"
4240
+ });
3030
4241
  const app = await createAgentApp({
3031
4242
  ...opts,
3032
4243
  mode: resolvedMode,
3033
4244
  workspaceRoot,
3034
4245
  externalPlugins: externalPluginsEnabled,
4246
+ runtimeEnvContributions: [
4247
+ ...opts.runtimeEnvContributions ?? [],
4248
+ ...workspaceBridgeRuntimeEnvContribution ? [workspaceBridgeRuntimeEnvContribution] : []
4249
+ ],
3035
4250
  extraTools: [
3036
4251
  ...opts.extraTools ?? [],
3037
4252
  ...uiTools,
@@ -3114,6 +4329,19 @@ async function createWorkspaceAgentServer(opts = {}) {
3114
4329
  });
3115
4330
  }
3116
4331
  await app.register(uiRoutes, { bridge, preserveStateKeys: pluginCollection.preservedUiStateKeys });
4332
+ await app.register(workspaceBridgeHttpRoutes, {
4333
+ registry: workspaceBridgeRegistry,
4334
+ runtimeTokenSecret: opts.workspaceBridge?.runtimeTokenSecret,
4335
+ runtimeRefreshTokenSecret: opts.workspaceBridge?.runtimeRefreshTokenSecret,
4336
+ ownerWorkspaceId: "default",
4337
+ idempotencyStore: new InMemoryWorkspaceBridgeIdempotencyStore(),
4338
+ browserAuthPolicy: resolveWorkspaceBridgeBrowserAuthPolicy(opts, workspaceBridgeRegistry)
4339
+ });
4340
+ const internals = app;
4341
+ internals.__boringWorkspaceBridgeRegistry = workspaceBridgeRegistry;
4342
+ internals.__boringRebuildPlugins = rebuildPlugins;
4343
+ internals.__boringAssetManager = boringAssetManager;
4344
+ internals.__boringRuntimeBackendRegistry = runtimeBackendRegistry;
3117
4345
  await app.register(boringPluginRoutes, {
3118
4346
  manager: boringAssetManager
3119
4347
  });
@@ -3121,18 +4349,16 @@ async function createWorkspaceAgentServer(opts = {}) {
3121
4349
  for (const { routes } of pluginCollection.routeContributions) {
3122
4350
  await app.register(routes);
3123
4351
  }
3124
- ;
3125
- app.__boringRebuildPlugins = rebuildPlugins;
3126
- app.__boringAssetManager = boringAssetManager;
3127
- app.__boringRuntimeBackendRegistry = runtimeBackendRegistry;
3128
4352
  return app;
3129
4353
  }
3130
4354
  export {
3131
4355
  PLUGIN_AUTHORING_PROVISIONING_IDS,
4356
+ assertWorkspaceBridgeHandlersTrusted,
3132
4357
  buildWorkspaceContextPrompt,
3133
4358
  collectWorkspaceAgentServerPlugins,
3134
4359
  createWorkspaceAgentServer,
3135
4360
  hasDirServerPlugin,
4361
+ isTrustedWorkspaceBridgeHandlerEntry,
3136
4362
  omitPluginAuthoringProvisioning,
3137
4363
  provisionWorkspaceAgentServer,
3138
4364
  readWorkspacePluginPackagePiSnapshot,