@memberjunction/server 6.1.5 → 6.1.6

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 (189) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/agentSessions/SessionManager.d.ts +5 -2
  3. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  4. package/dist/agentSessions/SessionManager.js +17 -8
  5. package/dist/agentSessions/SessionManager.js.map +1 -1
  6. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  7. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  8. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  9. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  10. package/dist/agentSessions/index.d.ts +1 -0
  11. package/dist/agentSessions/index.d.ts.map +1 -1
  12. package/dist/agentSessions/index.js +1 -0
  13. package/dist/agentSessions/index.js.map +1 -1
  14. package/dist/auth/actionAuthorization.d.ts +40 -0
  15. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  16. package/dist/auth/actionAuthorization.js +94 -0
  17. package/dist/auth/actionAuthorization.js.map +1 -0
  18. package/dist/auth/exampleNewUserSubClass.js +2 -1
  19. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  20. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  21. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  22. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  23. package/dist/auth/newUsers.d.ts.map +1 -1
  24. package/dist/auth/newUsers.js +15 -11
  25. package/dist/auth/newUsers.js.map +1 -1
  26. package/dist/context.d.ts +6 -0
  27. package/dist/context.d.ts.map +1 -1
  28. package/dist/context.js +13 -11
  29. package/dist/context.js.map +1 -1
  30. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  31. package/dist/generated/graphql-schemas/__mj.js +3417 -3428
  32. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  33. package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
  34. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  35. package/dist/generic/CacheInvalidationResolver.js +59 -19
  36. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  37. package/dist/generic/ResolverBase.d.ts +95 -11
  38. package/dist/generic/ResolverBase.d.ts.map +1 -1
  39. package/dist/generic/ResolverBase.js +186 -76
  40. package/dist/generic/ResolverBase.js.map +1 -1
  41. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  42. package/dist/generic/RunViewResolver.js +12 -0
  43. package/dist/generic/RunViewResolver.js.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +8 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/isolatedProvider.d.ts +20 -0
  49. package/dist/isolatedProvider.d.ts.map +1 -0
  50. package/dist/isolatedProvider.js +22 -0
  51. package/dist/isolatedProvider.js.map +1 -0
  52. package/dist/multiTenancy/index.d.ts.map +1 -1
  53. package/dist/multiTenancy/index.js +22 -2
  54. package/dist/multiTenancy/index.js.map +1 -1
  55. package/dist/realtimeWidget/widgetGuestElevation.d.ts +4 -2
  56. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  57. package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
  58. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  59. package/dist/resolvers/ActionResolver.d.ts +31 -5
  60. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  61. package/dist/resolvers/ActionResolver.js +75 -14
  62. package/dist/resolvers/ActionResolver.js.map +1 -1
  63. package/dist/resolvers/AdhocQueryResolver.d.ts +28 -0
  64. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  65. package/dist/resolvers/AdhocQueryResolver.js +46 -0
  66. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  67. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  68. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  69. package/dist/resolvers/DatasetResolver.js +57 -8
  70. package/dist/resolvers/DatasetResolver.js.map +1 -1
  71. package/dist/resolvers/EntityRecordNameResolver.d.ts +8 -23
  72. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  73. package/dist/resolvers/EntityRecordNameResolver.js +11 -48
  74. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  75. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
  76. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  77. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  78. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  79. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  80. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  81. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  82. package/dist/resolvers/GetDataResolver.js +13 -1
  83. package/dist/resolvers/GetDataResolver.js.map +1 -1
  84. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  85. package/dist/resolvers/QuerySystemUserResolver.js +2 -1
  86. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  87. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +39 -16
  88. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  89. package/dist/resolvers/RealtimeClientSessionResolver.js +84 -38
  90. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  91. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  92. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  93. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  94. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  95. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  96. package/dist/resolvers/RunTemplateResolver.js +12 -11
  97. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  98. package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
  99. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  100. package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
  101. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  102. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
  103. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
  104. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
  105. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  106. package/dist/resolvers/SyncDataResolver.js +6 -2
  107. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  108. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  109. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  110. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  111. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  112. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  113. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  114. package/dist/rest/ViewOperationsHandler.js +29 -17
  115. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  116. package/dist/services/TaskGraphActionRunner.d.ts +8 -0
  117. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  118. package/dist/services/TaskGraphActionRunner.js +27 -1
  119. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  120. package/package.json +92 -92
  121. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  122. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +4 -2
  123. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  124. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  125. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  126. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  127. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  128. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  129. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  130. package/src/__tests__/RealtimeClientSessionResolver.test.ts +106 -41
  131. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  132. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
  133. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  134. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  135. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  136. package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
  137. package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
  138. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  139. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  140. package/src/__tests__/SessionManager.test.ts +94 -17
  141. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  142. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -0
  143. package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
  144. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  145. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  146. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  147. package/src/__tests__/actionAuthorization.test.ts +130 -0
  148. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  149. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  150. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  151. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  152. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  153. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  154. package/src/__tests__/multiTenancy.test.ts +1 -1
  155. package/src/__tests__/newUsers.test.ts +62 -5
  156. package/src/agentSessions/SessionManager.ts +23 -25
  157. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  158. package/src/agentSessions/index.ts +1 -0
  159. package/src/auth/actionAuthorization.ts +119 -0
  160. package/src/auth/exampleNewUserSubClass.ts +2 -2
  161. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  162. package/src/auth/newUsers.ts +15 -11
  163. package/src/context.ts +14 -10
  164. package/src/generated/graphql-schemas/__mj.ts +3417 -3428
  165. package/src/generic/CacheInvalidationResolver.ts +74 -19
  166. package/src/generic/ResolverBase.ts +207 -80
  167. package/src/generic/RunViewResolver.ts +17 -0
  168. package/src/index.ts +9 -1
  169. package/src/isolatedProvider.ts +22 -0
  170. package/src/multiTenancy/index.ts +24 -2
  171. package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
  172. package/src/resolvers/ActionResolver.ts +84 -21
  173. package/src/resolvers/AdhocQueryResolver.ts +49 -1
  174. package/src/resolvers/DatasetResolver.ts +70 -10
  175. package/src/resolvers/EntityRecordNameResolver.ts +11 -52
  176. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
  177. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  178. package/src/resolvers/GetDataResolver.ts +14 -1
  179. package/src/resolvers/QuerySystemUserResolver.ts +2 -1
  180. package/src/resolvers/RealtimeClientSessionResolver.ts +103 -42
  181. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  182. package/src/resolvers/RunTemplateResolver.ts +13 -12
  183. package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
  184. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
  185. package/src/resolvers/SyncDataResolver.ts +6 -2
  186. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  187. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  188. package/src/rest/ViewOperationsHandler.ts +37 -18
  189. package/src/services/TaskGraphActionRunner.ts +29 -1
@@ -242,13 +242,44 @@ function makeSessionEntity(overrides: Partial<FakeSession> = {}): FakeSession {
242
242
  };
243
243
  }
244
244
 
245
- /** Provider whose GetEntityObject returns a fixed entity per entity name. */
246
- function makeProvider(factory: (entityName: string) => FakeSession): unknown {
247
- return { GetEntityObject: vi.fn(async (name: string) => factory(name)) };
245
+ /** Co-agent observability and paused-run ids, as a voice session's Config holds them. */
246
+ const CO_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000001';
247
+ const PROMPT_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000002';
248
+ const PAUSED_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000003';
249
+
250
+ /** `RunView`/`RunViews` stubs for a provider. */
251
+ interface RunLookups {
252
+ RunView: ReturnType<typeof vi.fn>;
253
+ RunViews: ReturnType<typeof vi.fn>;
254
+ }
255
+
256
+ /**
257
+ * Answers the session run-id lookups: each id in `runIDs` is a run of the session, and
258
+ * `promptRunID` (when given) is the TargetLogID of the co-agent run's Prompt step.
259
+ */
260
+ function sessionRunLookups(runIDs: string[], promptRunID?: string): RunLookups {
261
+ const runView = vi.fn(async (params: { EntityName?: string }) => {
262
+ if (params.EntityName === 'MJ: AI Agent Runs') {
263
+ return { Success: true, Results: runIDs.map((ID) => ({ ID })) };
264
+ }
265
+ if (params.EntityName === 'MJ: AI Agent Run Steps' && promptRunID) {
266
+ return { Success: true, Results: [{ ID: 'c0c0c0c0-0000-4000-8000-0000000000a1', StepType: 'Prompt', TargetLogID: promptRunID }] };
267
+ }
268
+ return { Success: true, Results: [] };
269
+ });
270
+ const runViews = vi.fn(async (paramsList: Array<{ EntityName?: string }>) => Promise.all(paramsList.map((params) => runView(params))));
271
+ return { RunView: runView, RunViews: runViews };
272
+ }
273
+
274
+ /** Provider whose GetEntityObject returns a fixed entity per entity name, plus optional run-id lookups. */
275
+ function makeProvider(factory: (entityName: string) => FakeSession, lookups?: RunLookups): unknown {
276
+ return { GetEntityObject: vi.fn(async (name: string) => factory(name)), ...lookups };
248
277
  }
249
278
 
250
279
  beforeEach(() => {
251
280
  hasPermissionMock.mockReset();
281
+ // Callers may run the agents in play unless a test says otherwise; relays re-check CanRun on the target.
282
+ hasPermissionMock.mockResolvedValue(true);
252
283
  engineCoAgentsMock.mockReset();
253
284
  engineCoAgentsMock.mockReturnValue([]);
254
285
  engineChannelsMock.mockReset();
@@ -685,10 +716,12 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
685
716
  });
686
717
 
687
718
  it('reads the target + co-agent run id from the session config and returns ResultJson', async () => {
688
- currentProvider = makeProvider(() =>
689
- makeSessionEntity({
690
- Config_: JSON.stringify({ targetAgentID: 'target-from-session', coAgentRunID: 'co-run-77' }),
691
- }),
719
+ currentProvider = makeProvider(
720
+ () =>
721
+ makeSessionEntity({
722
+ Config_: JSON.stringify({ targetAgentID: 'target-from-session', coAgentRunID: CO_RUN_ID }),
723
+ }),
724
+ sessionRunLookups([CO_RUN_ID]),
692
725
  );
693
726
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
694
727
  const resolver = makeResolver();
@@ -711,7 +744,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
711
744
  // Target comes from the session, NOT the client.
712
745
  expect(relayArg.TargetAgentID).toBe('target-from-session');
713
746
  // Delegated run nests under the co-agent observability run from the session config.
714
- expect(relayArg.ParentRunID).toBe('co-run-77');
747
+ expect(relayArg.ParentRunID).toBe(CO_RUN_ID);
715
748
  expect(relayArg.Call).toEqual({ CallID: 'call-1', ToolName: 'invoke-target-agent', Arguments: '{"request":"do it"}' });
716
749
  expect(heartbeatMock).toHaveBeenCalledWith('session-1', USER, currentProvider);
717
750
  });
@@ -775,9 +808,9 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
775
808
 
776
809
  it('consumes pendingFeedbackRunID as ResumeRunID and clears it on the next call', async () => {
777
810
  const session = makeSessionEntity({
778
- Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: 'paused-1' }),
811
+ Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
779
812
  });
780
- currentProvider = makeProvider(() => session);
813
+ currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
781
814
  // Resumed run completes (no new pause).
782
815
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"done":1}', Success: true });
783
816
  const resolver = makeResolver();
@@ -786,7 +819,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
786
819
 
787
820
  // The paused id was passed in to resume the run...
788
821
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
789
- expect(relayArg.ResumeRunID).toBe('paused-1');
822
+ expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
790
823
  // ...and cleared from the session config afterward.
791
824
  expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBeUndefined();
792
825
  expect(session.Save).toHaveBeenCalled();
@@ -794,16 +827,16 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
794
827
 
795
828
  it('re-stores a new pendingFeedbackRunID when a resumed run pauses again', async () => {
796
829
  const session = makeSessionEntity({
797
- Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: 'paused-1' }),
830
+ Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
798
831
  });
799
- currentProvider = makeProvider(() => session);
832
+ currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
800
833
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"q":2}', Success: true, PausedRunID: 'paused-2' });
801
834
  const resolver = makeResolver();
802
835
 
803
836
  await resolver.ExecuteRealtimeSessionTool('session-1', 'call-3', 'invoke-target-agent', '{"request":"yes"}', makeCtx(), makePubSub());
804
837
 
805
838
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
806
- expect(relayArg.ResumeRunID).toBe('paused-1');
839
+ expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
807
840
  expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBe('paused-2');
808
841
  });
809
842
 
@@ -2292,10 +2325,11 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2292
2325
  }): { provider: unknown; promptRun: FakeSession } {
2293
2326
  const session =
2294
2327
  opts.session ??
2295
- makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }) });
2296
- const promptRun = opts.promptRun ?? makeSessionEntity({ ID: 'prompt-run-1', TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2328
+ makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }) });
2329
+ const promptRun = opts.promptRun ?? makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2297
2330
  const provider = {
2298
2331
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
2332
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2299
2333
  };
2300
2334
  return { provider, promptRun };
2301
2335
  }
@@ -2321,7 +2355,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2321
2355
  const ok = await resolver.RelayRealtimeUsage('session-1', 30, 5, makeCtx());
2322
2356
 
2323
2357
  expect(ok).toBe(true);
2324
- expect(promptRun.Load).toHaveBeenCalledWith('prompt-run-1');
2358
+ expect(promptRun.Load).toHaveBeenCalledWith(PROMPT_RUN_ID);
2325
2359
  expect(promptRun.TokensPrompt).toBe(130);
2326
2360
  expect(promptRun.TokensCompletion).toBe(45);
2327
2361
  expect(promptRun.TokensUsed).toBe(175);
@@ -2418,7 +2452,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2418
2452
  const { provider, promptRun } = makeUsageProvider({
2419
2453
  session: makeSessionEntity({
2420
2454
  Status: 'Closed',
2421
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
2455
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2422
2456
  }),
2423
2457
  });
2424
2458
  currentProvider = provider;
@@ -2815,7 +2849,7 @@ describe('RealtimeClientSessionResolver — app awareness (applicationId / appCo
2815
2849
  expect(relayArg.AllowedAgents?.map(a => a.agentId)).toEqual(['skip-1']);
2816
2850
  });
2817
2851
 
2818
- it('passes the persisted directActions from the session config into ExecuteRelayedTool', async () => {
2852
+ it('does not pass the directActions stored in the session config into ExecuteRelayedTool', async () => {
2819
2853
  currentProvider = makeProvider(() =>
2820
2854
  makeSessionEntity({
2821
2855
  Config_: JSON.stringify({
@@ -2832,7 +2866,8 @@ describe('RealtimeClientSessionResolver — app awareness (applicationId / appCo
2832
2866
  );
2833
2867
 
2834
2868
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { DirectActions?: { enabled: boolean; actionNames: string[] } };
2835
- expect(relayArg.DirectActions).toEqual({ enabled: true, actionNames: ['SendEmail'] });
2869
+ // The owner can edit Config, so the service takes direct actions from server-side state instead.
2870
+ expect(relayArg.DirectActions).toBeUndefined();
2836
2871
  });
2837
2872
  });
2838
2873
 
@@ -2925,7 +2960,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2925
2960
  expect(executeRelayedToolMock.mock.calls[0][1]).toBe(ANON_USER);
2926
2961
  });
2927
2962
 
2928
- it('writes the hidden direct-action tool turn (ExecuteRealtimeSessionTool) as the CALLER, not the system user (#4791)', async () => {
2963
+ it('refuses a direct action from a scoped anonymous caller without dispatching it or writing a turn as the system user (#4791)', async () => {
2929
2964
  const detail = makeSessionEntity({ ID: 'detail-1' });
2930
2965
  currentProvider = {
2931
2966
  GetEntityObject: vi.fn(async (name: string) =>
@@ -2935,17 +2970,16 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2935
2970
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"success":true}', Success: true });
2936
2971
  const resolver = makeAnonResolver();
2937
2972
 
2938
- await resolver.ExecuteRealtimeSessionTool(
2973
+ const out = await resolver.ExecuteRealtimeSessionTool(
2939
2974
  'session-1', 'call-1', 'Some Direct Action', '{}', makeCtx(), makePubSub(),
2940
2975
  );
2941
2976
 
2942
- // The dispatch itself still runs elevated (SYSTEM_USER)…
2943
- expect(executeRelayedToolMock.mock.calls[0][1]).toBe(SYSTEM_USER);
2944
- // …but the hidden Conversation Detail turn is written as the CALLER, never the system user.
2945
- expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Details', ANON_USER);
2977
+ // Scope-limited callers get no direct actions, so nothing is dispatched, elevated or not…
2978
+ expect(JSON.parse(out)).toMatchObject({ success: false });
2979
+ expect(executeRelayedToolMock).not.toHaveBeenCalled();
2980
+ // …and no hidden Conversation Detail turn is written, least of all as the system user.
2946
2981
  expect(getEntityObjectSpy()).not.toHaveBeenCalledWith('MJ: Conversation Details', SYSTEM_USER);
2947
- expect(detail.Save).toHaveBeenCalled();
2948
- expect(detail.UserID).toBe('anon-1');
2982
+ expect(detail.Save).not.toHaveBeenCalled();
2949
2983
  });
2950
2984
 
2951
2985
  it('writes the direct-action tool turn as the caller unchanged for a normal authenticated user', async () => {
@@ -2966,11 +3000,12 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2966
3000
  it('accumulates relayed usage onto the prompt run under the SYSTEM user', async () => {
2967
3001
  const session = makeSessionEntity({
2968
3002
  UserID: 'anon-1',
2969
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3003
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2970
3004
  });
2971
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
3005
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2972
3006
  currentProvider = {
2973
3007
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
3008
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2974
3009
  };
2975
3010
  const resolver = makeAnonResolver();
2976
3011
 
@@ -2985,9 +3020,9 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2985
3020
  it('mirrors the transcript turn onto the prompt run as SYSTEM while the Conversation Detail stays the caller', async () => {
2986
3021
  const session = makeSessionEntity({
2987
3022
  UserID: 'anon-1',
2988
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3023
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2989
3024
  });
2990
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3025
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
2991
3026
  const detail = makeSessionEntity({ ID: 'detail-1' });
2992
3027
  currentProvider = {
2993
3028
  GetEntityObject: vi.fn(async (name: string) => {
@@ -2995,6 +3030,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2995
3030
  if (name === 'MJ: Conversation Details') return detail;
2996
3031
  return session;
2997
3032
  }),
3033
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2998
3034
  };
2999
3035
  const resolver = makeAnonResolver();
3000
3036
 
@@ -3011,11 +3047,12 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3011
3047
  it('records the co-agent tool turn (RelayRealtimeToolTurn) under the SYSTEM user', async () => {
3012
3048
  const session = makeSessionEntity({
3013
3049
  UserID: 'anon-1',
3014
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3050
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
3015
3051
  });
3016
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3052
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
3017
3053
  currentProvider = {
3018
3054
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
3055
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
3019
3056
  };
3020
3057
  const resolver = makeAnonResolver();
3021
3058
 
@@ -3028,9 +3065,9 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3028
3065
  it('writes the hidden co-agent tool turn (RelayRealtimeToolTurn) as the CALLER (#4791)', async () => {
3029
3066
  const session = makeSessionEntity({
3030
3067
  UserID: 'anon-1',
3031
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3068
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
3032
3069
  });
3033
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3070
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
3034
3071
  const detail = makeSessionEntity({ ID: 'detail-3' });
3035
3072
  currentProvider = {
3036
3073
  GetEntityObject: vi.fn(async (name: string) => {
@@ -3038,6 +3075,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3038
3075
  if (name === 'MJ: Conversation Details') return detail;
3039
3076
  return session;
3040
3077
  }),
3078
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
3041
3079
  };
3042
3080
  const resolver = makeAnonResolver();
3043
3081
 
@@ -3080,6 +3118,30 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3080
3118
  expect(prepInput.UserID).toBe('anon-1');
3081
3119
  });
3082
3120
 
3121
+ /** Starts a session as `resolver`'s caller and returns the input the prepare service received. */
3122
+ async function startAndCapturePrepInput(resolver: RealtimeClientSessionResolver, userID: string): Promise<{ ExcludeDirectActions?: boolean }> {
3123
+ currentProvider = makeProvider(() => makeSessionEntity({ UserID: userID }));
3124
+ createSessionMock.mockResolvedValue(makeSessionEntity({ ID: 'session-start', UserID: userID }));
3125
+ prepareClientSessionMock.mockResolvedValue({
3126
+ Success: true,
3127
+ ClientConfig: { Provider: 'openai', Model: 'gpt-realtime', EphemeralToken: 'ek_abc', ExpiresAt: '2026-01-01T00:00:00Z', SessionConfig: {} },
3128
+ });
3129
+ await resolver.StartRealtimeClientSession('target-1', makeCtx());
3130
+ return prepareClientSessionMock.mock.calls[0][0] as { ExcludeDirectActions?: boolean };
3131
+ }
3132
+
3133
+ it('prepares a scope-limited caller\'s session with no direct actions', async () => {
3134
+ const prepInput = await startAndCapturePrepInput(makeAnonResolver(), 'anon-1');
3135
+
3136
+ expect(prepInput.ExcludeDirectActions).toBe(true);
3137
+ });
3138
+
3139
+ it('prepares a caller that is not scope-limited with direct actions', async () => {
3140
+ const prepInput = await startAndCapturePrepInput(makeResolver(), 'user-1');
3141
+
3142
+ expect(prepInput.ExcludeDirectActions).toBe(false);
3143
+ });
3144
+
3083
3145
  it('junction-links delegated artifacts under the SYSTEM user and stamps the hidden anchor with the SESSION owner', async () => {
3084
3146
  const session = makeSessionEntity({ UserID: 'anon-1' });
3085
3147
  const junctions: FakeSession[] = [];
@@ -3220,15 +3282,16 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3220
3282
  function makeToolTurnProvider(detail: FakeSession): unknown {
3221
3283
  const session = makeSessionEntity({
3222
3284
  UserID: 'anon-1',
3223
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3285
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
3224
3286
  });
3225
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3287
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
3226
3288
  return {
3227
3289
  GetEntityObject: vi.fn(async (name: string) => {
3228
3290
  if (name === 'MJ: AI Prompt Runs') return promptRun;
3229
3291
  if (name === 'MJ: Conversation Details') return detail;
3230
3292
  return session;
3231
3293
  }),
3294
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
3232
3295
  };
3233
3296
  }
3234
3297
 
@@ -3388,7 +3451,8 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3388
3451
 
3389
3452
  it('excludes every colleague when the caller can run none of them', async () => {
3390
3453
  currentProvider = makeProvider(() => makeMultiTargetSession('user-1'));
3391
- hasPermissionMock.mockResolvedValue(false);
3454
+ // The caller can run the lead target, and none of the colleagues.
3455
+ hasPermissionMock.mockImplementation((...args: unknown[]) => Promise.resolve(args[0] === 'target-1'));
3392
3456
 
3393
3457
  await makeResolver().ExecuteRealtimeSessionTool(
3394
3458
  'session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub(),
@@ -3397,7 +3461,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3397
3461
  expect(dispatchedAgentIDs()).toEqual([]);
3398
3462
  });
3399
3463
 
3400
- it('passes a single-target session through with no permission checks', async () => {
3464
+ it('passes a single-target session through with only the target permission check', async () => {
3401
3465
  currentProvider = makeProvider(() => makeSessionEntity());
3402
3466
  hasPermissionMock.mockResolvedValue(true);
3403
3467
 
@@ -3407,6 +3471,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3407
3471
 
3408
3472
  const input = executeRelayedToolMock.mock.calls[0][0] as { AllowedAgents?: unknown };
3409
3473
  expect(input.AllowedAgents).toBeUndefined();
3410
- expect(hasPermissionMock).not.toHaveBeenCalled();
3474
+ expect(hasPermissionMock).toHaveBeenCalledTimes(1);
3475
+ expect((hasPermissionMock.mock.calls[0] as unknown[])[0]).toBe('target-1');
3411
3476
  });
3412
3477
  });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `ExecuteRemoteBrowserGoal` attaches a "Browser goal" step to the realtime co-agent run named by the
3
+ * session's `coAgentRunID`. The session owner can edit `Config`, so the run must be a record of the
4
+ * session before a step is attached to it.
5
+ */
6
+ // type-graphql decorators on the resolver need the reflect-metadata polyfill loaded first.
7
+ import 'reflect-metadata';
8
+
9
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
10
+
11
+ // As in RemoteBrowserActionRecovery.test.ts: the decorators become no-ops so the resolver's plain logic runs.
12
+ vi.mock('type-graphql', () => {
13
+ const noopDecorator = () => () => undefined;
14
+ return {
15
+ Resolver: noopDecorator,
16
+ Mutation: noopDecorator,
17
+ Query: noopDecorator,
18
+ Subscription: noopDecorator,
19
+ ObjectType: noopDecorator,
20
+ InputType: noopDecorator,
21
+ Field: noopDecorator,
22
+ Arg: noopDecorator,
23
+ Args: noopDecorator,
24
+ Ctx: noopDecorator,
25
+ PubSub: noopDecorator,
26
+ Root: noopDecorator,
27
+ Float: class {},
28
+ Int: class {},
29
+ ID: class {},
30
+ };
31
+ });
32
+
33
+ const achieveGoalMock = vi.fn(async (_sessionID: string, _goal: string, _options: Record<string, unknown>) => ({ Success: true, Status: 'Done' }));
34
+ vi.mock('@memberjunction/remote-browser-server', () => ({
35
+ NormalizeInstanceKey: (key?: string) => key,
36
+ RemoteBrowserEngine: {
37
+ Instance: {
38
+ AchieveGoal: (sessionID: string, goal: string, options: Record<string, unknown>) => achieveGoalMock(sessionID, goal, options),
39
+ },
40
+ },
41
+ }));
42
+
43
+ const beginStepMock = vi.fn(async (_provider: unknown, _user: unknown, _coAgentRunID: string | undefined, _goal: string) => null);
44
+ vi.mock('../agentSessions/remoteBrowserGoalEngine.js', async (importOriginal) => ({
45
+ ...(await importOriginal<typeof import('../agentSessions/remoteBrowserGoalEngine.js')>()),
46
+ beginBrowserGoalStep: (provider: unknown, user: unknown, coAgentRunID: string | undefined, goal: string) =>
47
+ beginStepMock(provider, user, coAgentRunID, goal),
48
+ finalizeBrowserGoalStep: vi.fn(async () => undefined),
49
+ }));
50
+
51
+ vi.mock('@memberjunction/aiengine', () => ({ AIEngine: { Instance: { Config: vi.fn(), Prompts: [] } } }));
52
+ vi.mock('@memberjunction/ai-prompts', async (importOriginal) => ({
53
+ ...(await importOriginal<typeof import('@memberjunction/ai-prompts')>()),
54
+ AIPromptRunner: class {},
55
+ }));
56
+ vi.mock('@memberjunction/ai-core-plus', async (importOriginal) => ({
57
+ ...(await importOriginal<typeof import('@memberjunction/ai-core-plus')>()),
58
+ AIPromptParams: class {},
59
+ }));
60
+
61
+ import { RemoteBrowserActionResolver } from '../resolvers/RemoteBrowserActionResolver.js';
62
+ import type { AppContext } from '../types.js';
63
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
64
+ import type { MJAIAgentSessionEntity } from '@memberjunction/core-entities';
65
+
66
+ const SESSION_ID = 'c3c3c3c3-2000-4000-8000-000000000001';
67
+ const CO_RUN_ID = 'c3c3c3c3-2000-4000-8000-000000000002';
68
+ /** A run that exists, but belongs to someone else's session. */
69
+ const FOREIGN_RUN_ID = 'c3c3c3c3-2000-4000-8000-0000000000f1';
70
+
71
+ /** The runs the fake lookup reports as records of the session. */
72
+ let sessionRunIDs: string[] = [];
73
+ const runViewsMock = vi.fn(async (paramsList: Array<{ EntityName?: string }>) =>
74
+ paramsList.map((params) => ({
75
+ Success: true,
76
+ Results: params.EntityName === 'MJ: AI Agent Runs' ? sessionRunIDs.map((ID) => ({ ID })) : [],
77
+ })),
78
+ );
79
+
80
+ /** Bypasses the ownership/provider plumbing (covered elsewhere); the session carries `ConfigJson`. */
81
+ class TestableResolver extends RemoteBrowserActionResolver {
82
+ public ConfigJson = '';
83
+ protected override requireUserAndProvider(): { contextUser: UserInfo; provider: IMetadataProvider } {
84
+ return {
85
+ contextUser: { ID: 'c3c3c3c3-2000-4000-8000-0000000000aa' } as unknown as UserInfo,
86
+ provider: { RunViews: runViewsMock } as unknown as IMetadataProvider,
87
+ };
88
+ }
89
+ protected override async loadOwnedSession(): Promise<MJAIAgentSessionEntity> {
90
+ return { ID: SESSION_ID, AgentID: 'agent-1', Config_: this.ConfigJson } as unknown as MJAIAgentSessionEntity;
91
+ }
92
+ protected override async resolveProviderName(): Promise<string | undefined> {
93
+ return 'Self-Hosted Chrome';
94
+ }
95
+ }
96
+
97
+ const ctx = { userPayload: { sessionId: 'push-sess-1' }, providers: [] } as unknown as AppContext;
98
+
99
+ async function startGoal(config: Record<string, unknown>): Promise<void> {
100
+ const resolver = new TestableResolver();
101
+ resolver.ConfigJson = JSON.stringify(config);
102
+ await resolver.ExecuteRemoteBrowserGoal(SESSION_ID, 'open the docs', ctx);
103
+ }
104
+
105
+ beforeEach(() => {
106
+ sessionRunIDs = [];
107
+ runViewsMock.mockClear();
108
+ beginStepMock.mockClear();
109
+ achieveGoalMock.mockClear();
110
+ });
111
+
112
+ describe('RemoteBrowserActionResolver.ExecuteRemoteBrowserGoal — co-agent run from Config', () => {
113
+ it('does not attach the goal to a run that is not a record of the session', async () => {
114
+ await startGoal({ targetAgentID: 'target-1', coAgentRunID: FOREIGN_RUN_ID });
115
+
116
+ expect(beginStepMock).toHaveBeenCalledTimes(1);
117
+ expect(beginStepMock.mock.calls[0][2]).toBeUndefined();
118
+ expect(achieveGoalMock.mock.calls[0][2].AgentRunID).toBeUndefined();
119
+ });
120
+
121
+ it('attaches the goal to the session\'s co-agent run', async () => {
122
+ sessionRunIDs = [CO_RUN_ID];
123
+
124
+ await startGoal({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID });
125
+
126
+ expect(beginStepMock.mock.calls[0][2]).toBe(CO_RUN_ID);
127
+ expect(achieveGoalMock.mock.calls[0][2].AgentRunID).toBe(CO_RUN_ID);
128
+ });
129
+ });