@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.4.5 → 0.4.8

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.
@@ -23,6 +23,7 @@ import type {
23
23
  Attachment,
24
24
  ClientTtsVoiceSettings,
25
25
  Message,
26
+ RuntimeAITResult,
26
27
  ServicePermission,
27
28
  } from '@bytexbyte/nxtlinq-ai-agent-core-development';
28
29
  import {
@@ -36,11 +37,26 @@ import {
36
37
  ChatBotProps,
37
38
  PresetMessage
38
39
  } from '../types/ChatBotTypes';
39
- import { waitForAITPermissions } from '../aitPermissionPropagation';
40
40
  import { AuthorizationLoadingState } from '../authorizationLoadingState';
41
41
  import { PendingTextToolRetryController } from '../pendingTextToolRetry';
42
42
  import { completePiiScan } from '../piiMessageState';
43
43
  import { filterSuggestions } from '../suggestionState';
44
+ import {
45
+ dispatchRuntimeAITCompleteReplacement,
46
+ removeOneDeniedPermission,
47
+ replaceCurrentMaximumDenyList,
48
+ RuntimeAITMutationQueue,
49
+ RuntimeAITRecomputeCoordinator,
50
+ StaleRuntimeAITResponseError,
51
+ runtimeAITContextKey,
52
+ runtimeAITSubjectKey,
53
+ runtimeAuthorizationMatchesContext,
54
+ runtimeAuthorizationState,
55
+ unavailableRuntimeAuthorization,
56
+ useProviderRuntimeCommit,
57
+ useRuntimeAITContextCommit,
58
+ type RuntimeAITContext,
59
+ } from '../permissionState';
44
60
 
45
61
  const MIC_ENABLED_SESSION_KEY = 'chatbot-mic-enabled';
46
62
 
@@ -71,6 +87,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
71
87
  environment = 'production',
72
88
  onVerifyWallet,
73
89
  permissionGroup,
90
+ roles,
74
91
  children,
75
92
  // AI Model related attributes
76
93
  onModelChange,
@@ -93,9 +110,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
93
110
  debugVoiceRms = false,
94
111
  sttGlossary,
95
112
  }) => {
96
- // Set API hosts immediately based on environment (before any API calls)
97
- setApiHosts(environment);
98
-
99
113
  const nxtlinqApi = React.useMemo(
100
114
  () => (serviceToken || getAuthToken
101
115
  ? createNxtlinqApi({ apiKey, apiSecret, serviceToken, getAuthToken })
@@ -155,7 +169,22 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
155
169
  const [hitAddress, setHitAddress] = React.useState<string | null>(null);
156
170
  const [ait, setAit] = React.useState<AIT | null>(null);
157
171
  const [permissions, setPermissions] = React.useState<string[]>([]);
158
- const [availablePermissions, setAvailablePermissions] = React.useState<ServicePermission[]>([]);
172
+ const [maximumPermissions, setMaximumPermissions] = React.useState<string[]>([]);
173
+ const [deniedPermissions, setDeniedPermissions] = React.useState<string[]>([]);
174
+ const [runtimePermissionsUnavailable, setRuntimePermissionsUnavailable] = React.useState(false);
175
+ const [runtimeAuthorizationContextKey, setRuntimeAuthorizationContextKey] =
176
+ React.useState<string | null>(null);
177
+ const [servicePermissionCatalog, setServicePermissionCatalog] = React.useState<ServicePermission[]>([]);
178
+ const availablePermissions = React.useMemo(
179
+ () => maximumPermissions.map((label) =>
180
+ servicePermissionCatalog.find((permission) => permission.label === label) ?? {
181
+ id: label,
182
+ label,
183
+ description: '',
184
+ groups: [],
185
+ }),
186
+ [maximumPermissions, servicePermissionCatalog],
187
+ );
159
188
  const [showPermissionForm, setShowPermissionForm] = React.useState(false);
160
189
  const [isPermissionFormOpen, setIsPermissionFormOpen] = React.useState(false);
161
190
  const [isAITLoading, setIsAITLoading] = React.useState(false);
@@ -164,6 +193,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
164
193
  const [walletInfo, setWalletInfo] = React.useState<any>(null);
165
194
  const [isWalletLoading, setIsWalletLoading] = React.useState(false);
166
195
  const [isAutoConnecting, setIsAutoConnecting] = React.useState(false);
196
+ const runtimeRolesKey = JSON.stringify(roles ?? null);
167
197
 
168
198
  // Persistent data (always use localStorage)
169
199
  const [nxtlinqAITServiceAccessToken, setNxtlinqAITServiceAccessToken] = useLocalStorage<string>('nxtlinqAITServiceAccessToken', '');
@@ -220,6 +250,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
220
250
  const hitAddressRef = React.useRef(hitAddress);
221
251
  const aitRef = React.useRef(ait);
222
252
  const permissionsRef = React.useRef(permissions);
253
+ const deniedPermissionsRef = React.useRef(deniedPermissions);
223
254
  const nxtlinqAITServiceAccessTokenRef = React.useRef(nxtlinqAITServiceAccessToken);
224
255
  const signerRef = React.useRef(signer);
225
256
  const authorizationLoadingStateRef = React.useRef(
@@ -241,6 +272,14 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
241
272
  );
242
273
  });
243
274
  }, []);
275
+ const runtimeRecomputeCoordinatorRef = React.useRef(new RuntimeAITRecomputeCoordinator());
276
+ const runtimeMutationQueueRef = React.useRef(new RuntimeAITMutationQueue());
277
+ useProviderRuntimeCommit(
278
+ hitAddressRef,
279
+ hitAddress,
280
+ environment,
281
+ setApiHosts,
282
+ );
244
283
 
245
284
  // Helper function to combine customUsername with Adilas customUserInfo
246
285
  const getFinalCustomUsername = React.useCallback((username: string | undefined): string | undefined => {
@@ -574,10 +613,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
574
613
  }, []);
575
614
 
576
615
  // Update refs when state changes
577
- React.useEffect(() => {
578
- hitAddressRef.current = hitAddress;
579
- }, [hitAddress]);
580
-
581
616
  React.useEffect(() => {
582
617
  aitRef.current = ait;
583
618
  }, [ait]);
@@ -594,6 +629,10 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
594
629
  authorizationLoadingStateRef.current.setAutoConnecting(isAutoConnecting);
595
630
  }, [isAutoConnecting]);
596
631
 
632
+ React.useEffect(() => {
633
+ deniedPermissionsRef.current = deniedPermissions;
634
+ }, [deniedPermissions]);
635
+
597
636
  React.useEffect(() => {
598
637
  nxtlinqAITServiceAccessTokenRef.current = nxtlinqAITServiceAccessToken;
599
638
  }, [nxtlinqAITServiceAccessToken]);
@@ -1024,7 +1063,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1024
1063
  console.error('Failed to fetch permissions:', result.error);
1025
1064
  return undefined;
1026
1065
  }
1027
- setAvailablePermissions(result.permissions);
1066
+ setServicePermissionCatalog(result.permissions);
1028
1067
  return result.permissions;
1029
1068
  } catch (error) {
1030
1069
  console.error('Error fetching permissions:', error);
@@ -1032,7 +1071,109 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1032
1071
  }
1033
1072
  };
1034
1073
 
1035
- // Refresh AIT
1074
+ const clearRuntimeAuthorization = React.useCallback(() => {
1075
+ const cleared = unavailableRuntimeAuthorization(deniedPermissionsRef.current);
1076
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
1077
+ aitRef.current = null;
1078
+ permissionsRef.current = [];
1079
+ setAit(cleared.ait);
1080
+ setPermissions(cleared.effectivePermissions);
1081
+ setMaximumPermissions(cleared.maximumPermissions);
1082
+ setRuntimePermissionsUnavailable(cleared.unavailable);
1083
+ setRuntimeAuthorizationContextKey(
1084
+ activeContext ? runtimeAITContextKey(activeContext) : null,
1085
+ );
1086
+ }, []);
1087
+
1088
+ const applyRuntimeResult = React.useCallback((result: RuntimeAITResult, context: RuntimeAITContext) => {
1089
+ const next = runtimeAuthorizationState(result, context.controller, context.serviceId);
1090
+ aitRef.current = next.ait;
1091
+ permissionsRef.current = next.effectivePermissions;
1092
+ setAit(next.ait);
1093
+ setPermissions(next.effectivePermissions);
1094
+ setMaximumPermissions(next.maximumPermissions);
1095
+ setDeniedPermissions(next.deniedPermissions);
1096
+ deniedPermissionsRef.current = next.deniedPermissions;
1097
+ setRuntimePermissionsUnavailable(false);
1098
+ setRuntimeAuthorizationContextKey(runtimeAITContextKey(context));
1099
+ return result;
1100
+ }, []);
1101
+
1102
+ const runtimeContext = React.useMemo<RuntimeAITContext | null>(() => hitAddress ? {
1103
+ controller: hitAddress,
1104
+ serviceId,
1105
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
1106
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
1107
+ } : null, [hitAddress, permissionGroup, runtimeRolesKey, serviceId]);
1108
+ const invalidateCommittedRuntimeContext = React.useCallback(() => {
1109
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
1110
+ aitRef.current = null;
1111
+ permissionsRef.current = [];
1112
+ deniedPermissionsRef.current = [];
1113
+ setAit(null);
1114
+ setPermissions([]);
1115
+ setMaximumPermissions([]);
1116
+ setDeniedPermissions([]);
1117
+ setRuntimePermissionsUnavailable(activeContext !== null);
1118
+ setRuntimeAuthorizationContextKey(
1119
+ activeContext ? runtimeAITContextKey(activeContext) : null,
1120
+ );
1121
+ }, []);
1122
+ useRuntimeAITContextCommit(
1123
+ runtimeRecomputeCoordinatorRef.current,
1124
+ runtimeContext,
1125
+ invalidateCommittedRuntimeContext,
1126
+ );
1127
+
1128
+ const recomputeRuntimeAIT = React.useCallback(async (context: RuntimeAITContext) => {
1129
+ const capturedContext: RuntimeAITContext = {
1130
+ controller: context.controller,
1131
+ serviceId: context.serviceId,
1132
+ ...(context.roles !== undefined ? { roles: [...context.roles] } : {}),
1133
+ ...(context.permissionGroup !== undefined ? { permissionGroup: context.permissionGroup } : {}),
1134
+ };
1135
+ return runtimeRecomputeCoordinatorRef.current.run(
1136
+ capturedContext,
1137
+ () => nxtlinqApi.ait.recomputeAIT(capturedContext),
1138
+ applyRuntimeResult,
1139
+ clearRuntimeAuthorization,
1140
+ );
1141
+ }, [applyRuntimeResult, clearRuntimeAuthorization, nxtlinqApi]);
1142
+
1143
+ const queueRuntimeAITRefresh = React.useCallback(
1144
+ (context: RuntimeAITContext) => runtimeMutationQueueRef.current.run(
1145
+ runtimeAITSubjectKey(context),
1146
+ async () => {
1147
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
1148
+ return recomputeRuntimeAIT(context);
1149
+ },
1150
+ ),
1151
+ [recomputeRuntimeAIT],
1152
+ );
1153
+
1154
+ const convergeRuntimeAITSubject = React.useCallback(async (subjectKey: string) => {
1155
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
1156
+ if (!activeContext || runtimeAITSubjectKey(activeContext) !== subjectKey) {
1157
+ return;
1158
+ }
1159
+ await recomputeRuntimeAIT(activeContext);
1160
+ }, [recomputeRuntimeAIT]);
1161
+
1162
+ const dispatchRuntimeDenyListReplacement = React.useCallback(async (
1163
+ context: RuntimeAITContext,
1164
+ nextDeniedPermissions: string[],
1165
+ ) => dispatchRuntimeAITCompleteReplacement(
1166
+ runtimeRecomputeCoordinatorRef.current,
1167
+ context,
1168
+ () => nxtlinqApi.ait.updatePermissionDenyList({
1169
+ ...context,
1170
+ deniedPermissions: nextDeniedPermissions,
1171
+ }),
1172
+ convergeRuntimeAITSubject,
1173
+ clearRuntimeAuthorization,
1174
+ ), [clearRuntimeAuthorization, convergeRuntimeAITSubject, nxtlinqApi]);
1175
+
1176
+ // Refresh the authoritative runtime AIT state.
1036
1177
  const refreshAIT = async (forceUpdatePermissions = false) => {
1037
1178
  const currentHitAddress = hitAddressRef.current || hitAddress;
1038
1179
  const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
@@ -1041,8 +1182,10 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1041
1182
  permissionsRef.current = [];
1042
1183
  setAit(null);
1043
1184
  setPermissions([]);
1185
+ setMaximumPermissions([]);
1044
1186
  setWalletInfo(null);
1045
1187
  authorizationLoadingStateRef.current.setAITLoading(false);
1188
+ setRuntimePermissionsUnavailable(false);
1046
1189
  setIsAITLoading(false);
1047
1190
  return;
1048
1191
  }
@@ -1074,53 +1217,23 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1074
1217
  }
1075
1218
  }
1076
1219
 
1077
- // AIT existence is not identity verification. In strict mode, stop before
1078
- // the legacy AIT lookup can synthesize a custom wallet record.
1079
1220
  if (currentToken && !walletAllowsAITLookup) {
1080
- aitRef.current = null;
1081
- permissionsRef.current = [];
1082
- setAit(null);
1083
- setPermissions([]);
1221
+ clearRuntimeAuthorization();
1084
1222
  return;
1085
1223
  }
1086
1224
 
1087
- // Only try to fetch AIT if we have a token
1225
+ // Recompute is the sole mint/reuse path and the only permission authority.
1088
1226
  if (currentToken) {
1089
- const response = await nxtlinqApi.ait.getAITByServiceIdAndController({
1090
- serviceId,
1091
- controller: currentHitAddress,
1092
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
1093
- }, currentToken);
1094
-
1095
- if ('error' in response) {
1096
- console.error('Failed to fetch AIT:', response.error);
1097
- aitRef.current = null;
1098
- permissionsRef.current = [];
1099
- setAit(null);
1100
- setPermissions([]);
1101
- return;
1102
- }
1103
-
1104
- aitRef.current = response;
1105
- setAit(response);
1106
- if (!isPermissionFormOpen || forceUpdatePermissions) {
1107
- const newPermissions = response.metadata?.permissions || [];
1108
- permissionsRef.current = newPermissions;
1109
- setPermissions(newPermissions);
1227
+ if (runtimeContext) {
1228
+ await queueRuntimeAITRefresh(runtimeContext);
1110
1229
  }
1111
1230
  } else {
1112
- // No token available, clear AIT data
1113
- aitRef.current = null;
1114
- permissionsRef.current = [];
1115
- setAit(null);
1116
- setPermissions([]);
1231
+ clearRuntimeAuthorization();
1117
1232
  }
1118
1233
  } catch (error) {
1119
- console.error('Failed to fetch AIT:', error);
1120
- aitRef.current = null;
1121
- permissionsRef.current = [];
1122
- setAit(null);
1123
- setPermissions([]);
1234
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
1235
+ console.error('Failed to recompute AIT:', error);
1236
+ }
1124
1237
  } finally {
1125
1238
  // Keep the authorization snapshot current before refreshAIT resolves.
1126
1239
  // React may not commit setState until the immediate pending-tool retry.
@@ -1129,50 +1242,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1129
1242
  }
1130
1243
  };
1131
1244
 
1132
- const waitForPersistedAITPermissions = async (
1133
- expectedPermissions: string[],
1134
- ): Promise<boolean> => {
1135
- const currentHitAddress = hitAddressRef.current || hitAddress;
1136
- const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
1137
- if (!currentHitAddress || !currentToken) return false;
1138
-
1139
- authorizationLoadingStateRef.current.setAITLoading(true);
1140
- setIsAITLoading(true);
1141
- try {
1142
- const persistedAIT = await waitForAITPermissions({
1143
- expectedPermissions,
1144
- readAIT: async () => {
1145
- try {
1146
- const response = await nxtlinqApi.ait.getAITByServiceIdAndController({
1147
- serviceId,
1148
- controller: currentHitAddress,
1149
- customUsername: (!requireWalletIDVVerification && customUsername)
1150
- ? getFinalCustomUsername(customUsername)
1151
- : undefined,
1152
- }, currentToken);
1153
- return 'error' in response ? undefined : response;
1154
- } catch (error) {
1155
- console.warn('AIT permission propagation check failed:', error);
1156
- return undefined;
1157
- }
1158
- },
1159
- getPermissions: (currentAIT) => currentAIT.metadata?.permissions || [],
1160
- });
1161
-
1162
- if (!persistedAIT) return false;
1163
-
1164
- const persistedPermissions = persistedAIT.metadata?.permissions || [];
1165
- aitRef.current = persistedAIT;
1166
- permissionsRef.current = persistedPermissions;
1167
- setAit(persistedAIT);
1168
- setPermissions(persistedPermissions);
1169
- return true;
1170
- } finally {
1171
- authorizationLoadingStateRef.current.setAITLoading(false);
1172
- setIsAITLoading(false);
1173
- }
1174
- };
1175
-
1176
1245
  // Check if user needs to sign in
1177
1246
  const isNeedSignInWithWallet = React.useMemo(() => {
1178
1247
  if (!hitAddress) return false;
@@ -1369,10 +1438,13 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1369
1438
  const { accessToken } = response;
1370
1439
  setNxtlinqAITServiceAccessToken(accessToken);
1371
1440
  nxtlinqAITServiceAccessTokenRef.current = accessToken;
1372
-
1373
- // Wait for state to update before refreshing AIT
1374
- await new Promise(resolve => setTimeout(resolve, 100));
1375
- await refreshAIT();
1441
+ localStorage.setItem('nxtlinqAITServiceAccessToken', JSON.stringify(accessToken));
1442
+ await queueRuntimeAITRefresh({
1443
+ controller: addressToUse,
1444
+ serviceId,
1445
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
1446
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
1447
+ });
1376
1448
 
1377
1449
  if (autoShowSuccessMessage) {
1378
1450
  showSuccess(walletTextUtils.getWalletText('Successfully signed in with your HIT wallet. You can now use the AI agent.', serviceId));
@@ -1411,6 +1483,8 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1411
1483
  loading: authorizationLoadingStateRef.current.isLoading(),
1412
1484
  },
1413
1485
  ...authFields(),
1486
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
1487
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
1414
1488
  customUsername: (!requireWalletIDVVerification && customUsername)
1415
1489
  ? getFinalCustomUsername(customUsername)
1416
1490
  : undefined,
@@ -1858,6 +1932,14 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1858
1932
 
1859
1933
  // Typed preflight already emitted the only permitted recovery UI.
1860
1934
  if (!isToolAllowed) {
1935
+ // Do not leave suggestions from the previous turn visible while
1936
+ // authorization recovery short-circuits the normal response path.
1937
+ setSuggestions([]);
1938
+ void updateSuggestions(
1939
+ pseudoId,
1940
+ localStorage.getItem('walletAddress') || undefined,
1941
+ content,
1942
+ );
1861
1943
  pendingTextToolRetryRef.current.set({
1862
1944
  content,
1863
1945
  isPresetMessage,
@@ -2482,108 +2564,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2482
2564
  }
2483
2565
  };
2484
2566
 
2485
- // Generate and register AIT
2486
- const generateAndRegisterAIT = async (newPermissions?: string[], isFromAIAgent = false) => {
2487
- let currentSigner = signer;
2488
- let currentAddress = hitAddress;
2489
-
2490
- // If we have an address but no signer, try to auto-connect wallet
2491
- if (currentAddress && !currentSigner) {
2492
- const storedWalletAddress = localStorage.getItem('walletAddress');
2493
- if (storedWalletAddress === currentAddress) {
2494
- try {
2495
- const autoConnectResult = await connectWallet(false); // Don't show sign-in message yet
2496
- if (autoConnectResult) {
2497
- // Wait for state to update
2498
- await new Promise(resolve => setTimeout(resolve, 1000));
2499
- // Get the updated signer and address
2500
- currentSigner = signerRef.current || signer;
2501
- currentAddress = hitAddressRef.current || hitAddress;
2502
- }
2503
- } catch (error) {
2504
- console.error('Auto-connect failed during AIT generation:', error);
2505
- }
2506
- }
2507
- }
2508
-
2509
- if (!currentSigner || !currentAddress) {
2510
- throw new Error('Missing signer or wallet address');
2511
- }
2512
-
2513
- return generateAndRegisterAITWithSigner(newPermissions, isFromAIAgent, currentSigner, currentAddress);
2514
- };
2515
-
2516
- // Generate and register AIT with explicit signer and address
2517
- const generateAndRegisterAITWithSigner = async (
2518
- newPermissions?: string[],
2519
- isFromAIAgent = false,
2520
- explicitSigner?: ethers.JsonRpcSigner,
2521
- explicitAddress?: string
2522
- ) => {
2523
- const currentSigner = explicitSigner || signer;
2524
- const currentAddress = explicitAddress || hitAddress;
2525
-
2526
- if (!currentSigner || !currentAddress) {
2527
- throw new Error('Missing signer or wallet address');
2528
- }
2529
-
2530
- // If AI Agent is creating, must check if user has existing AIT
2531
- if (isFromAIAgent && !aitRef.current) {
2532
- throw new Error('You must have an existing AIT before AI Agent can create new AITs');
2533
- }
2534
-
2535
- const timestamp = Math.floor(Date.now() / 1000);
2536
- const aitId = `did:polygon:nxtlinq:${currentAddress}:${timestamp}`;
2537
-
2538
- const metadata = {
2539
- permissions: newPermissions || permissions,
2540
- issuedBy: currentAddress,
2541
- };
2542
-
2543
- const metadataStr = stringify(metadata);
2544
- // ethers v6: utils.keccak256 -> keccak256, utils.toUtf8Bytes -> toUtf8Bytes
2545
- const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(metadataStr));
2546
-
2547
- const uploadResponse = await nxtlinqApi.metadata.createMetadata(metadata, nxtlinqAITServiceAccessToken || '');
2548
- if ('error' in uploadResponse) {
2549
- throw new Error(`Failed to upload metadata: ${uploadResponse.error}`);
2550
- }
2551
-
2552
- const { metadataCid } = uploadResponse;
2553
-
2554
- const createAITParams = {
2555
- aitId,
2556
- controller: currentAddress,
2557
- serviceId,
2558
- metadataHash,
2559
- metadataCid,
2560
- isFromAIAgent: isFromAIAgent,
2561
- parentAITId: isFromAIAgent ? aitRef.current?.aitId : undefined,
2562
- };
2563
-
2564
- const createAITResponse = await nxtlinqApi.ait.createAIT(createAITParams, nxtlinqAITServiceAccessToken || '');
2565
-
2566
- if ('error' in createAITResponse) {
2567
- throw new Error(`Failed to create AIT: ${createAITResponse.error}`);
2568
- }
2569
-
2570
- const aitInfo = {
2571
- aitId,
2572
- controller: currentAddress,
2573
- metadata,
2574
- metadataHash,
2575
- metadataCid,
2576
- };
2577
-
2578
- const nextPermissions = newPermissions || permissions;
2579
- aitRef.current = aitInfo;
2580
- permissionsRef.current = nextPermissions;
2581
- setAit(aitInfo);
2582
- setPermissions(nextPermissions);
2583
- };
2584
-
2585
-
2586
-
2587
2567
  // Auto enable AIT permission
2588
2568
  const enableAIT = async (toolName: string) => {
2589
2569
  if (isAITEnabling) return false; // Prevent duplicate
@@ -2650,35 +2630,31 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2650
2630
  }
2651
2631
 
2652
2632
  try {
2653
- // Get available permissions to find the tool
2654
- const availablePermissionLabels = availablePermissions.map(p => p.label);
2655
- if (!availablePermissionLabels.includes(toolName)) {
2656
- showError(`Tool ${toolName} is not available for your current identity provider`);
2657
- setIsAITEnabling(false);
2658
- return false;
2659
- }
2660
-
2661
- // Get current permissions from AIT metadata instead of React state
2662
- // This ensures we don't lose existing permissions when enabling new ones
2663
- const currentAITPermissions = aitRef.current?.metadata?.permissions || permissions;
2664
- const newPermissions = [...currentAITPermissions];
2665
- if (!newPermissions.includes(toolName)) {
2666
- newPermissions.push(toolName);
2667
- }
2668
-
2669
- // Filter out deleted permissions before saving
2670
- const validPermissions = newPermissions.filter(permission =>
2671
- availablePermissionLabels.includes(permission)
2633
+ const context: RuntimeAITContext = {
2634
+ controller: currentHitAddress,
2635
+ serviceId,
2636
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2637
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2638
+ };
2639
+ const enabled = await runtimeMutationQueueRef.current.run(
2640
+ runtimeAITSubjectKey(context),
2641
+ async () => {
2642
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2643
+ const current = await recomputeRuntimeAIT(context);
2644
+ if (!current.maximumPermissions.includes(toolName)) {
2645
+ return false;
2646
+ }
2647
+ const nextDeniedPermissions = removeOneDeniedPermission(
2648
+ current.deniedPermissions,
2649
+ toolName,
2650
+ );
2651
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2652
+ await dispatchRuntimeDenyListReplacement(context, nextDeniedPermissions);
2653
+ return true;
2654
+ },
2672
2655
  );
2673
-
2674
- // Generate and register AIT with new permissions
2675
- // For auto-enable, we should create a regular AIT (not AI Agent AIT) if user doesn't have existing AIT
2676
- const shouldCreateAsAIAgent = !!aitRef.current; // Only create as AI Agent if user already has an AIT
2677
-
2678
- await generateAndRegisterAITWithSigner(validPermissions, shouldCreateAsAIAgent, currentSigner, currentHitAddress);
2679
- const permissionsPersisted = await waitForPersistedAITPermissions(validPermissions);
2680
- if (!permissionsPersisted) {
2681
- showWarning('AIT permission was submitted but is still synchronizing. Please try again shortly.');
2656
+ if (!enabled) {
2657
+ showError(`Tool ${toolName} is not available for your current identity provider`);
2682
2658
  setIsAITEnabling(false);
2683
2659
  return false;
2684
2660
  }
@@ -2686,9 +2662,13 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2686
2662
  setIsAITEnabling(false);
2687
2663
  return true;
2688
2664
  } catch (error) {
2689
- console.error('Failed to auto-enable AIT:', error);
2665
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
2666
+ console.error('Failed to auto-enable AIT:', error);
2667
+ }
2690
2668
  setIsAITEnabling(false);
2691
- if (error instanceof Error) {
2669
+ if (error instanceof StaleRuntimeAITResponseError) {
2670
+ return false;
2671
+ } else if (error instanceof Error) {
2692
2672
  showError(error.message);
2693
2673
  } else {
2694
2674
  showError('Failed to enable AIT permission. Please try again.');
@@ -2701,23 +2681,43 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2701
2681
  const savePermissions = async (newPermissions?: string[]) => {
2702
2682
  setIsDisabled(true);
2703
2683
  try {
2704
- // Filter out deleted permissions before saving
2705
- const permissionsToSave = newPermissions || permissions;
2706
- const availablePermissionLabels = availablePermissions.map(p => p.label);
2707
- const validPermissions = permissionsToSave.filter(permission =>
2708
- availablePermissionLabels.includes(permission)
2684
+ if (!hitAddress) {
2685
+ throw new Error('Please connect your wallet first');
2686
+ }
2687
+ const context: RuntimeAITContext = {
2688
+ controller: hitAddress,
2689
+ serviceId,
2690
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2691
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2692
+ };
2693
+ const checkedPermissions = [...(newPermissions ?? permissions)];
2694
+ await runtimeMutationQueueRef.current.run(
2695
+ runtimeAITSubjectKey(context),
2696
+ async () => {
2697
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2698
+ const fresh = await recomputeRuntimeAIT(context);
2699
+ const nextDeniedPermissions = replaceCurrentMaximumDenyList(
2700
+ fresh.deniedPermissions,
2701
+ fresh.maximumPermissions,
2702
+ checkedPermissions,
2703
+ );
2704
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2705
+ await dispatchRuntimeDenyListReplacement(context, nextDeniedPermissions);
2706
+ },
2709
2707
  );
2710
-
2711
- await generateAndRegisterAIT(validPermissions, false);
2712
2708
  showSuccess('AIT permissions saved successfully! You can now use the AI agent with your configured permissions.');
2713
2709
  setShowPermissionForm(false);
2714
2710
  setIsPermissionFormOpen(false);
2715
2711
  await refreshAIT(true);
2716
2712
  await retryPendingTextTool();
2717
2713
  } catch (error) {
2718
- console.error('Failed to generate AIT:', error);
2714
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
2715
+ console.error('Failed to update AIT deny-list:', error);
2716
+ }
2719
2717
  setIsDisabled(false);
2720
- if (error instanceof Error) {
2718
+ if (error instanceof StaleRuntimeAITResponseError) {
2719
+ return;
2720
+ } else if (error instanceof Error) {
2721
2721
  showError(error.message);
2722
2722
  } else {
2723
2723
  showError('Failed to save permissions. Please try again.');
@@ -2766,15 +2766,12 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2766
2766
  const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
2767
2767
  if (!('error' in walletResponse)) {
2768
2768
  setWalletInfo(walletResponse);
2769
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({
2770
- serviceId,
2769
+ await queueRuntimeAITRefresh({
2771
2770
  controller: address,
2772
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2773
- }, token);
2774
- if (!('error' in aitResponse)) {
2775
- aitRef.current = aitResponse;
2776
- setAit(aitResponse);
2777
- }
2771
+ serviceId,
2772
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2773
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2774
+ });
2778
2775
  }
2779
2776
  } finally {
2780
2777
  setIsWalletLoading(false);
@@ -2806,15 +2803,12 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2806
2803
  const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
2807
2804
  if (!('error' in walletResponse)) {
2808
2805
  setWalletInfo(walletResponse);
2809
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({
2810
- serviceId,
2806
+ await queueRuntimeAITRefresh({
2811
2807
  controller: address,
2812
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2813
- }, token);
2814
- if (!('error' in aitResponse)) {
2815
- aitRef.current = aitResponse;
2816
- setAit(aitResponse);
2817
- }
2808
+ serviceId,
2809
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2810
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2811
+ });
2818
2812
  }
2819
2813
  } finally {
2820
2814
  setIsWalletLoading(false);
@@ -2924,7 +2918,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2924
2918
  if (hitAddress && nxtlinqAITServiceAccessToken) {
2925
2919
  refreshAIT();
2926
2920
  }
2927
- }, [hitAddress, nxtlinqAITServiceAccessToken]);
2921
+ }, [hitAddress, nxtlinqAITServiceAccessToken, permissionGroup, runtimeRolesKey, serviceId]);
2928
2922
 
2929
2923
  // Set loading state when permission form opens
2930
2924
  React.useEffect(() => {
@@ -3013,6 +3007,8 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3013
3007
  updateExternalId()
3014
3008
  }, [hitAddress, nxtlinqApi, authFields, pseudoId]);
3015
3009
 
3010
+ const runtimeAuthorizationMatchesRenderedContext =
3011
+ runtimeAuthorizationMatchesContext(runtimeAuthorizationContextKey, runtimeContext);
3016
3012
  const contextValue: ChatBotContextType = {
3017
3013
  // State
3018
3014
  messages,
@@ -3020,9 +3016,16 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3020
3016
  isLoading,
3021
3017
  isOpen,
3022
3018
  hitAddress,
3023
- ait,
3024
- permissions,
3025
- availablePermissions,
3019
+ ait: runtimeAuthorizationMatchesRenderedContext ? ait : null,
3020
+ permissions: runtimeAuthorizationMatchesRenderedContext ? permissions : [],
3021
+ maximumPermissions: runtimeAuthorizationMatchesRenderedContext ? maximumPermissions : [],
3022
+ deniedPermissions: runtimeAuthorizationMatchesRenderedContext ? deniedPermissions : [],
3023
+ runtimePermissionsUnavailable: runtimeAuthorizationMatchesRenderedContext
3024
+ ? runtimePermissionsUnavailable
3025
+ : runtimeContext !== null,
3026
+ availablePermissions: runtimeAuthorizationMatchesRenderedContext
3027
+ ? availablePermissions
3028
+ : [],
3026
3029
  showPermissionForm,
3027
3030
  isPermissionFormOpen,
3028
3031
  isAITLoading,
@@ -3059,7 +3062,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3059
3062
  setIsOpen,
3060
3063
  setShowPermissionForm,
3061
3064
  setIsPermissionFormOpen,
3062
- setPermissions,
3063
3065
  setIsDisabled,
3064
3066
  setIsWalletLoading,
3065
3067
  setNotification,
@@ -3110,6 +3112,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3110
3112
  onVerifyWallet: (method: 'berifyme' | 'custom') => handleVerifyWalletClick(method),
3111
3113
  serviceId,
3112
3114
  permissionGroup,
3115
+ roles,
3113
3116
 
3114
3117
  // Props
3115
3118
  props: {
@@ -3128,6 +3131,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3128
3131
  getAuthToken,
3129
3132
  onVerifyWallet,
3130
3133
  permissionGroup,
3134
+ roles,
3131
3135
  onModelChange,
3132
3136
  storageMode,
3133
3137
  requireWalletIDVVerification,