@agent-native/dispatch 0.14.2 → 0.14.4

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.
Files changed (138) hide show
  1. package/README.md +114 -122
  2. package/dist/actions/approve-dispatch-change.d.ts +10 -10
  3. package/dist/actions/list_apps.d.ts +1 -0
  4. package/dist/actions/list_apps.js +16 -8
  5. package/dist/actions/list_apps.js.map +1 -1
  6. package/dist/actions/provider-api-register.js +64 -2
  7. package/dist/actions/provider-api-register.js.map +1 -1
  8. package/dist/actions/reject-dispatch-change.d.ts +10 -10
  9. package/dist/actions/set-mcp-app-access.js +1 -4
  10. package/dist/actions/set-mcp-app-access.js.map +1 -1
  11. package/dist/components/action-query-error.d.ts +8 -0
  12. package/dist/components/action-query-error.d.ts.map +1 -0
  13. package/dist/components/action-query-error.js +10 -0
  14. package/dist/components/action-query-error.js.map +1 -0
  15. package/dist/components/app-keys-popover.d.ts +4 -0
  16. package/dist/components/app-keys-popover.d.ts.map +1 -1
  17. package/dist/components/app-keys-popover.js +26 -14
  18. package/dist/components/app-keys-popover.js.map +1 -1
  19. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  20. package/dist/components/dispatch-control-plane.js +31 -27
  21. package/dist/components/dispatch-control-plane.js.map +1 -1
  22. package/dist/components/index.d.ts +1 -0
  23. package/dist/components/index.d.ts.map +1 -1
  24. package/dist/components/index.js +1 -0
  25. package/dist/components/index.js.map +1 -1
  26. package/dist/components/layout/Layout.d.ts +1 -0
  27. package/dist/components/layout/Layout.d.ts.map +1 -1
  28. package/dist/components/layout/Layout.js +19 -21
  29. package/dist/components/layout/Layout.js.map +1 -1
  30. package/dist/components/ui/sheet.js +1 -1
  31. package/dist/components/ui/sheet.js.map +1 -1
  32. package/dist/components/ui/toast.js +1 -1
  33. package/dist/components/ui/toast.js.map +1 -1
  34. package/dist/components/workspace-app-card.d.ts.map +1 -1
  35. package/dist/components/workspace-app-card.js +8 -5
  36. package/dist/components/workspace-app-card.js.map +1 -1
  37. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  38. package/dist/components/workspace-resource-effective-stack.js +6 -1
  39. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  40. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -1
  41. package/dist/components/workspace-resource-impact-preview.js +6 -1
  42. package/dist/components/workspace-resource-impact-preview.js.map +1 -1
  43. package/dist/routes/pages/$appId.d.ts.map +1 -1
  44. package/dist/routes/pages/$appId.js +6 -1
  45. package/dist/routes/pages/$appId.js.map +1 -1
  46. package/dist/routes/pages/agents.d.ts.map +1 -1
  47. package/dist/routes/pages/agents.js +5 -8
  48. package/dist/routes/pages/agents.js.map +1 -1
  49. package/dist/routes/pages/approval.d.ts.map +1 -1
  50. package/dist/routes/pages/approval.js +6 -1
  51. package/dist/routes/pages/approval.js.map +1 -1
  52. package/dist/routes/pages/approvals.d.ts.map +1 -1
  53. package/dist/routes/pages/approvals.js +8 -5
  54. package/dist/routes/pages/approvals.js.map +1 -1
  55. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  56. package/dist/routes/pages/apps.$appId.js +4 -2
  57. package/dist/routes/pages/apps.$appId.js.map +1 -1
  58. package/dist/routes/pages/apps.d.ts.map +1 -1
  59. package/dist/routes/pages/apps.js +9 -3
  60. package/dist/routes/pages/apps.js.map +1 -1
  61. package/dist/routes/pages/audit.d.ts.map +1 -1
  62. package/dist/routes/pages/audit.js +4 -2
  63. package/dist/routes/pages/audit.js.map +1 -1
  64. package/dist/routes/pages/chat.d.ts.map +1 -1
  65. package/dist/routes/pages/chat.js +16 -2
  66. package/dist/routes/pages/chat.js.map +1 -1
  67. package/dist/routes/pages/destinations.d.ts.map +1 -1
  68. package/dist/routes/pages/destinations.js +5 -3
  69. package/dist/routes/pages/destinations.js.map +1 -1
  70. package/dist/routes/pages/identities.d.ts.map +1 -1
  71. package/dist/routes/pages/identities.js +5 -3
  72. package/dist/routes/pages/identities.js.map +1 -1
  73. package/dist/routes/pages/integrations.d.ts.map +1 -1
  74. package/dist/routes/pages/integrations.js +9 -3
  75. package/dist/routes/pages/integrations.js.map +1 -1
  76. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  77. package/dist/routes/pages/thread-debug.js +7 -6
  78. package/dist/routes/pages/thread-debug.js.map +1 -1
  79. package/dist/routes/pages/vault.d.ts.map +1 -1
  80. package/dist/routes/pages/vault.js +23 -7
  81. package/dist/routes/pages/vault.js.map +1 -1
  82. package/dist/routes/pages/workspace.d.ts.map +1 -1
  83. package/dist/routes/pages/workspace.js +9 -3
  84. package/dist/routes/pages/workspace.js.map +1 -1
  85. package/dist/server/lib/dispatch-store.d.ts +24 -21
  86. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  87. package/dist/server/lib/dispatch-store.js +59 -23
  88. package/dist/server/lib/dispatch-store.js.map +1 -1
  89. package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
  90. package/dist/server/lib/mcp-access-store.js +30 -2
  91. package/dist/server/lib/mcp-access-store.js.map +1 -1
  92. package/dist/server/lib/vault-store.d.ts.map +1 -1
  93. package/dist/server/lib/vault-store.js +102 -37
  94. package/dist/server/lib/vault-store.js.map +1 -1
  95. package/dist/server/plugins/agent-chat.d.ts +1 -1
  96. package/package.json +2 -3
  97. package/src/actions/list_apps.spec.ts +52 -0
  98. package/src/actions/list_apps.ts +16 -8
  99. package/src/actions/provider-api-register.spec.ts +193 -0
  100. package/src/actions/provider-api-register.ts +66 -1
  101. package/src/actions/set-mcp-app-access.spec.ts +73 -0
  102. package/src/actions/set-mcp-app-access.ts +1 -5
  103. package/src/components/action-query-error.spec.tsx +56 -0
  104. package/src/components/action-query-error.tsx +32 -0
  105. package/src/components/app-keys-popover.spec.tsx +85 -0
  106. package/src/components/app-keys-popover.tsx +76 -50
  107. package/src/components/dispatch-control-plane.spec.tsx +106 -0
  108. package/src/components/dispatch-control-plane.tsx +71 -115
  109. package/src/components/index.ts +1 -0
  110. package/src/components/layout/Layout.spec.tsx +42 -2
  111. package/src/components/layout/Layout.tsx +22 -27
  112. package/src/components/ui/sheet.tsx +1 -1
  113. package/src/components/ui/toast.tsx +1 -1
  114. package/src/components/workspace-app-card.spec.tsx +80 -0
  115. package/src/components/workspace-app-card.tsx +36 -25
  116. package/src/components/workspace-resource-effective-stack.tsx +13 -1
  117. package/src/components/workspace-resource-impact-preview.tsx +12 -1
  118. package/src/routes/pages/$appId.tsx +17 -1
  119. package/src/routes/pages/agents.tsx +24 -11
  120. package/src/routes/pages/approval.tsx +16 -4
  121. package/src/routes/pages/approvals.tsx +108 -89
  122. package/src/routes/pages/apps.$appId.tsx +9 -2
  123. package/src/routes/pages/apps.tsx +20 -7
  124. package/src/routes/pages/audit.tsx +29 -20
  125. package/src/routes/pages/chat.spec.tsx +94 -0
  126. package/src/routes/pages/chat.tsx +30 -10
  127. package/src/routes/pages/destinations.tsx +75 -62
  128. package/src/routes/pages/identities.tsx +74 -63
  129. package/src/routes/pages/integrations.tsx +17 -10
  130. package/src/routes/pages/thread-debug.tsx +18 -12
  131. package/src/routes/pages/vault.tsx +59 -26
  132. package/src/routes/pages/workspace.tsx +69 -55
  133. package/src/server/lib/approval-fencing.spec.ts +420 -0
  134. package/src/server/lib/dispatch-store.ts +94 -21
  135. package/src/server/lib/mcp-access-store.spec.ts +116 -4
  136. package/src/server/lib/mcp-access-store.ts +37 -2
  137. package/src/server/lib/mcp-gateway.spec.ts +79 -3
  138. package/src/server/lib/vault-store.ts +151 -45
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
- import { and, desc, eq, isNull, or } from "@agent-native/core/db/schema";
3
+ import { and, desc, eq, isNull, or, sql } from "@agent-native/core/db/schema";
4
4
  import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
5
5
  import {
6
6
  deleteAppSecret,
@@ -567,8 +567,59 @@ export async function createGrant(
567
567
  if (!secret) throw new Error("Secret not found");
568
568
 
569
569
  const timestamp = now();
570
- const grantId = id();
571
570
  const actor = ctx.ownerEmail;
571
+ const [existing] = await db
572
+ .select()
573
+ .from(schema.vaultGrants)
574
+ .where(
575
+ and(
576
+ eq(schema.vaultGrants.secretId, secretId),
577
+ eq(schema.vaultGrants.appId, appId),
578
+ ctxScope(schema.vaultGrants, ctx),
579
+ ),
580
+ )
581
+ .orderBy(desc(schema.vaultGrants.updatedAt))
582
+ .limit(1);
583
+
584
+ if (existing?.status === "active") {
585
+ return existing;
586
+ }
587
+
588
+ if (existing) {
589
+ await db
590
+ .update(schema.vaultGrants)
591
+ .set({
592
+ grantedBy: actor,
593
+ status: "active",
594
+ syncedAt: null,
595
+ updatedAt: timestamp,
596
+ })
597
+ .where(
598
+ and(
599
+ eq(schema.vaultGrants.id, existing.id),
600
+ ctxScope(schema.vaultGrants, ctx),
601
+ ),
602
+ );
603
+
604
+ await recordVaultAudit({
605
+ action: "grant.reinstated",
606
+ secretId,
607
+ appId,
608
+ summary: `Reinstated "${secret.name}" (${secret.credentialKey}) for ${appId}`,
609
+ metadata: { grantId: existing.id },
610
+ });
611
+
612
+ await recordAudit({
613
+ action: "vault.grant.reinstated",
614
+ targetType: "vault-grant",
615
+ targetId: existing.id,
616
+ summary: `Reinstated vault secret "${secret.name}" for ${appId}`,
617
+ });
618
+
619
+ return getGrant(existing.id, ctx);
620
+ }
621
+
622
+ const grantId = id();
572
623
 
573
624
  await db.insert(schema.vaultGrants).values({
574
625
  id: grantId,
@@ -956,65 +1007,115 @@ export async function approveRequest(
956
1007
  const db = getDb();
957
1008
  const request = await getRequest(requestId, ctx);
958
1009
  if (!request) throw new Error("Request not found");
959
- if (request.status !== "pending") {
960
- throw new Error("Only pending requests can be approved");
961
- }
962
1010
 
963
1011
  const timestamp = now();
964
1012
  const reviewer = ctx.ownerEmail;
965
-
966
- // Update request status — scoped to caller's tenant.
967
- await db
1013
+ const staleApplyingBefore = timestamp - 5 * 60 * 1000;
1014
+
1015
+ // Fence the transition on the current status — scoped to caller's tenant —
1016
+ // so a concurrent approve can't both win: only the caller that flips
1017
+ // pending -> applying proceeds to create the secret/grant. A crashed worker
1018
+ // leaves an applying row behind, so a later reviewer may reclaim a lease
1019
+ // that has been idle for five minutes. See
1020
+ // claimAgentTeamRun in packages/core/src/server/agent-teams-run-queue.ts
1021
+ // for the same pattern.
1022
+ const claimed = await db
968
1023
  .update(schema.vaultRequests)
969
1024
  .set({
970
- status: "approved",
971
- reviewedBy: reviewer,
972
- reviewedAt: timestamp,
1025
+ status: "applying",
973
1026
  updatedAt: timestamp,
974
1027
  })
975
1028
  .where(
976
1029
  and(
977
1030
  eq(schema.vaultRequests.id, requestId),
978
1031
  ctxScope(schema.vaultRequests, ctx),
1032
+ or(
1033
+ eq(schema.vaultRequests.status, "pending"),
1034
+ and(
1035
+ eq(schema.vaultRequests.status, "applying"),
1036
+ sql`${schema.vaultRequests.updatedAt} < ${staleApplyingBefore}`,
1037
+ ),
1038
+ ),
979
1039
  ),
980
- );
1040
+ )
1041
+ .returning();
1042
+
1043
+ if (claimed.length === 0) {
1044
+ const current = await getRequest(requestId, ctx);
1045
+ if (current?.status === "applying") {
1046
+ throw new Error("Vault request is already being applied");
1047
+ }
1048
+ return current;
1049
+ }
1050
+
1051
+ const claimedRequest = claimed[0];
981
1052
 
982
1053
  // Secret + grant must land in the REQUEST's tenant, not the approver's
983
1054
  // (the approver may be acting on behalf of another user in the same org).
984
- const requestCtx = ctxForRow(request);
1055
+ const requestCtx = ctxForRow(claimedRequest);
985
1056
 
986
- // Check if secret already exists in the request's tenant for this key.
987
- const existingSecrets = await db
988
- .select()
989
- .from(schema.vaultSecrets)
1057
+ try {
1058
+ // Check if secret already exists in the request's tenant for this key.
1059
+ const existingSecrets = await db
1060
+ .select()
1061
+ .from(schema.vaultSecrets)
1062
+ .where(
1063
+ and(
1064
+ eq(schema.vaultSecrets.credentialKey, claimedRequest.credentialKey),
1065
+ ctxScope(schema.vaultSecrets, requestCtx),
1066
+ ),
1067
+ );
1068
+ let secret = existingSecrets[0] ?? null;
1069
+
1070
+ if (!secret) {
1071
+ secret = await createSecret(
1072
+ {
1073
+ credentialKey: claimedRequest.credentialKey,
1074
+ value: secretValue,
1075
+ name: secretName || claimedRequest.credentialKey,
1076
+ },
1077
+ requestCtx,
1078
+ );
1079
+ }
1080
+
1081
+ if (secret) {
1082
+ // Create the grant in the request's tenant as well.
1083
+ await createGrant(secret.id, claimedRequest.appId, requestCtx);
1084
+ }
1085
+ } catch (error) {
1086
+ await db
1087
+ .update(schema.vaultRequests)
1088
+ .set({ status: "pending", updatedAt: now() })
1089
+ .where(
1090
+ and(
1091
+ eq(schema.vaultRequests.id, requestId),
1092
+ ctxScope(schema.vaultRequests, ctx),
1093
+ eq(schema.vaultRequests.status, "applying"),
1094
+ ),
1095
+ );
1096
+ throw error;
1097
+ }
1098
+
1099
+ await db
1100
+ .update(schema.vaultRequests)
1101
+ .set({
1102
+ status: "approved",
1103
+ reviewedBy: reviewer,
1104
+ reviewedAt: timestamp,
1105
+ updatedAt: now(),
1106
+ })
990
1107
  .where(
991
1108
  and(
992
- eq(schema.vaultSecrets.credentialKey, request.credentialKey),
993
- ctxScope(schema.vaultSecrets, requestCtx),
1109
+ eq(schema.vaultRequests.id, requestId),
1110
+ ctxScope(schema.vaultRequests, ctx),
1111
+ eq(schema.vaultRequests.status, "applying"),
994
1112
  ),
995
1113
  );
996
- let secret = existingSecrets[0] ?? null;
997
-
998
- if (!secret) {
999
- secret = await createSecret(
1000
- {
1001
- credentialKey: request.credentialKey,
1002
- value: secretValue,
1003
- name: secretName || request.credentialKey,
1004
- },
1005
- requestCtx,
1006
- );
1007
- }
1008
-
1009
- if (secret) {
1010
- // Create the grant in the request's tenant as well.
1011
- await createGrant(secret.id, request.appId, requestCtx);
1012
- }
1013
1114
 
1014
1115
  await recordVaultAudit({
1015
1116
  action: "request.approved",
1016
- appId: request.appId,
1017
- summary: `Approved ${request.credentialKey} for ${request.appId} (requested by ${request.requestedBy})`,
1117
+ appId: claimedRequest.appId,
1118
+ summary: `Approved ${claimedRequest.credentialKey} for ${claimedRequest.appId} (requested by ${claimedRequest.requestedBy})`,
1018
1119
  metadata: { requestId, reviewer },
1019
1120
  });
1020
1121
 
@@ -1029,14 +1130,11 @@ export async function denyRequest(
1029
1130
  const db = getDb();
1030
1131
  const request = await getRequest(requestId, ctx);
1031
1132
  if (!request) throw new Error("Request not found");
1032
- if (request.status !== "pending") {
1033
- throw new Error("Only pending requests can be denied");
1034
- }
1035
1133
 
1036
1134
  const timestamp = now();
1037
1135
  const reviewer = ctx.ownerEmail;
1038
1136
 
1039
- await db
1137
+ const claimed = await db
1040
1138
  .update(schema.vaultRequests)
1041
1139
  .set({
1042
1140
  status: "denied",
@@ -1048,13 +1146,21 @@ export async function denyRequest(
1048
1146
  and(
1049
1147
  eq(schema.vaultRequests.id, requestId),
1050
1148
  ctxScope(schema.vaultRequests, ctx),
1149
+ eq(schema.vaultRequests.status, "pending"),
1051
1150
  ),
1052
- );
1151
+ )
1152
+ .returning();
1153
+
1154
+ if (claimed.length === 0) {
1155
+ return getRequest(requestId, ctx);
1156
+ }
1157
+
1158
+ const claimedRequest = claimed[0];
1053
1159
 
1054
1160
  await recordVaultAudit({
1055
1161
  action: "request.denied",
1056
- appId: request.appId,
1057
- summary: `Denied ${request.credentialKey} for ${request.appId} (requested by ${request.requestedBy})`,
1162
+ appId: claimedRequest.appId,
1163
+ summary: `Denied ${claimedRequest.credentialKey} for ${claimedRequest.appId} (requested by ${claimedRequest.requestedBy})`,
1058
1164
  metadata: { requestId, reviewer, reason },
1059
1165
  });
1060
1166