@memberjunction/server 5.50.0 → 6.1.0-edge.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/dist/agentSessions/SessionManager.d.ts +22 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +18 -3
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/actingContextResolver.d.ts +44 -0
- package/dist/auth/actingContextResolver.d.ts.map +1 -0
- package/dist/auth/actingContextResolver.js +18 -0
- package/dist/auth/actingContextResolver.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/sessionUserClone.d.ts +32 -0
- package/dist/auth/sessionUserClone.d.ts.map +1 -0
- package/dist/auth/sessionUserClone.js +50 -0
- package/dist/auth/sessionUserClone.js.map +1 -0
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +43 -6
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +71 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +383 -3
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +5 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +5 -4
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +58 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +42 -3
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +9 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +31 -13
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts +19 -0
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +82 -5
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +22 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +36 -0
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -5
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +13 -16
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +22 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +90 -30
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +19 -26
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +11 -3
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +30 -40
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +90 -112
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +3 -3
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +2 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.js +8 -12
- package/dist/resolvers/VersionHistoryResolver.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +17 -15
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +100 -100
- package/src/__tests__/RealtimeBridgeResolver.test.ts +6 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +420 -0
- package/src/__tests__/SessionManager.test.ts +106 -0
- package/src/__tests__/multiTenancy.security.test.ts +254 -22
- package/src/__tests__/multiTenancy.test.ts +22 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
- package/src/__tests__/resolverBase.rls.test.ts +4 -0
- package/src/__tests__/search-knowledge-tags.test.ts +74 -6
- package/src/__tests__/sessionUserClone.test.ts +161 -0
- package/src/__tests__/widgetGuestElevation.test.ts +70 -2
- package/src/agentSessions/SessionManager.ts +32 -3
- package/src/auth/actingContextResolver.ts +56 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/sessionUserClone.ts +50 -0
- package/src/context.ts +48 -6
- package/src/generated/generated.ts +276 -3
- package/src/generic/FireAndForgetHeartbeat.ts +10 -4
- package/src/generic/PushStatusResolver.ts +79 -4
- package/src/generic/ResolverBase.ts +33 -13
- package/src/index.ts +4 -2
- package/src/multiTenancy/index.ts +92 -5
- package/src/realtimeWidget/widgetGuestElevation.ts +41 -0
- package/src/resolvers/ActionResolver.ts +15 -7
- package/src/resolvers/MCPResolver.ts +13 -15
- package/src/resolvers/RealtimeClientSessionResolver.ts +99 -29
- package/src/resolvers/RemoteBrowserActionResolver.ts +19 -25
- package/src/resolvers/ReportResolver.ts +11 -3
- package/src/resolvers/RunAIAgentResolver.ts +30 -40
- package/src/resolvers/RunTestResolver.ts +90 -112
- package/src/resolvers/SearchKnowledgeResolver.ts +3 -3
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +2 -2
- package/src/resolvers/VersionHistoryResolver.ts +10 -13
- package/src/resolvers/__tests__/ReportResolver.test.ts +279 -0
- package/src/services/TaskOrchestrator.ts +17 -17
|
@@ -60,7 +60,7 @@ import { ResolverBase } from '../generic/ResolverBase.js';
|
|
|
60
60
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
61
61
|
import { GetReadWriteProvider } from '../util.js';
|
|
62
62
|
import { SessionManager } from '../agentSessions/index.js';
|
|
63
|
-
import { resolveWidgetGuestRunContext } from '../realtimeWidget/widgetGuestElevation.js';
|
|
63
|
+
import { resolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
|
|
@@ -349,8 +349,13 @@ export class UploadRealtimeRecordingResult {
|
|
|
349
349
|
*/
|
|
350
350
|
@Resolver()
|
|
351
351
|
export class RealtimeClientSessionResolver extends ResolverBase {
|
|
352
|
-
|
|
352
|
+
// Declaration order matters: `clientSessionService` must be initialized before `sessionManager`
|
|
353
|
+
// reads it, so `SessionManager.CloseSession` finalizes co-agent observability runs (see
|
|
354
|
+
// `finalizeObservabilityRuns`) through the SAME `RealtimeClientSessionService` instance that
|
|
355
|
+
// `AppendPromptRunMessage`/`AccumulatePromptRunUsage` (below) accumulated per-run write-chain
|
|
356
|
+
// state on — otherwise that cleanup silently no-ops on a freshly-constructed instance.
|
|
353
357
|
private readonly clientSessionService = new RealtimeClientSessionService();
|
|
358
|
+
private readonly sessionManager = new SessionManager(this.clientSessionService);
|
|
354
359
|
|
|
355
360
|
/**
|
|
356
361
|
* Start a client-direct realtime voice session targeting `targetAgentId`.
|
|
@@ -495,13 +500,28 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
495
500
|
const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
|
|
496
501
|
const config = this.readSessionConfig(session);
|
|
497
502
|
|
|
503
|
+
// SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
|
|
504
|
+
// run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
|
|
505
|
+
// (the caller's role deliberately holds no grants on the run entities). The lead
|
|
506
|
+
// targetAgentID comes from the session config and was CanRun-gated at start; the colleague
|
|
507
|
+
// union is gated just below, against the CALLER, so elevation never widens agent authority.
|
|
508
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
509
|
+
if (runUser !== contextUser) {
|
|
510
|
+
LogStatus(
|
|
511
|
+
`ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
|
|
512
|
+
'under the system user (scoped-anonymous caller).',
|
|
513
|
+
);
|
|
514
|
+
}
|
|
498
515
|
const { ResultJson, PausedRunID, Artifacts } = await this.clientSessionService.ExecuteRelayedTool(
|
|
499
516
|
{
|
|
500
517
|
AgentSessionID: agentSessionId,
|
|
501
518
|
TargetAgentID: config.targetAgentID,
|
|
502
519
|
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
503
520
|
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
504
|
-
AllowedAgents: config.allowedAgents,
|
|
521
|
+
AllowedAgents: await this.filterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
|
|
522
|
+
// Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
|
|
523
|
+
// row and its context-memory scope must stay the person's, not the system user's.
|
|
524
|
+
AttributionUserID: contextUser.ID,
|
|
505
525
|
// Nest the delegated target-agent run under the co-agent observability run (when present).
|
|
506
526
|
ParentRunID: config.coAgentRunID,
|
|
507
527
|
Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
|
|
@@ -509,7 +529,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
509
529
|
// Resume a previously-paused delegated run (if any) with the user's answer.
|
|
510
530
|
ResumeRunID: config.pendingFeedbackRunID,
|
|
511
531
|
},
|
|
512
|
-
|
|
532
|
+
runUser,
|
|
513
533
|
provider,
|
|
514
534
|
);
|
|
515
535
|
|
|
@@ -519,7 +539,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
519
539
|
|
|
520
540
|
// Junction-link any artifacts the delegated run produced into the session's conversation
|
|
521
541
|
// history (best-effort) — so chat, session review, and resume carryover can all see them.
|
|
522
|
-
|
|
542
|
+
// Runs as `runUser`: the junction entity is not among an anonymous caller's relay grants.
|
|
543
|
+
await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, provider);
|
|
523
544
|
|
|
524
545
|
await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
|
|
525
546
|
return ResultJson;
|
|
@@ -542,18 +563,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
542
563
|
if (!SIGNIFICANT_PROGRESS_STEPS.includes(progress.step)) {
|
|
543
564
|
return;
|
|
544
565
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}),
|
|
555
|
-
sessionId: userPayload.sessionId,
|
|
556
|
-
});
|
|
566
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
567
|
+
resolver: 'RealtimeClientSessionResolver',
|
|
568
|
+
type: 'RealtimeDelegationProgress',
|
|
569
|
+
agentSessionID,
|
|
570
|
+
callID,
|
|
571
|
+
step: progress.step,
|
|
572
|
+
message: progress.message,
|
|
573
|
+
percentage: progress.percentage,
|
|
574
|
+
}), userPayload);
|
|
557
575
|
};
|
|
558
576
|
}
|
|
559
577
|
|
|
@@ -625,6 +643,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
625
643
|
}
|
|
626
644
|
// Mirror the turn onto the co-agent's long-lived prompt run so its Messages capture the full
|
|
627
645
|
// conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
|
|
646
|
+
// The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
|
|
647
|
+
// Conversation Detail above deliberately stays on the caller.
|
|
628
648
|
const promptRunID = this.readPromptRunID(session);
|
|
629
649
|
if (promptRunID) {
|
|
630
650
|
await this.clientSessionService.AppendPromptRunMessage(
|
|
@@ -632,7 +652,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
632
652
|
this.mapTranscriptRoleToChatRole(role),
|
|
633
653
|
text,
|
|
634
654
|
replacesPrevious ?? false,
|
|
635
|
-
contextUser,
|
|
655
|
+
ResolveScopedAnonymousRunUser(contextUser),
|
|
636
656
|
provider,
|
|
637
657
|
);
|
|
638
658
|
}
|
|
@@ -691,13 +711,18 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
691
711
|
return { Success: false, ErrorMessage: 'Recording consent was not granted.' };
|
|
692
712
|
}
|
|
693
713
|
|
|
714
|
+
// SCOPED-ANONYMOUS ELEVATION (issue #3371): past the ownership + consent gates, the store is
|
|
715
|
+
// server-side plumbing over entities (MJ: AI Agents read, MJ: Files, the file-session link)
|
|
716
|
+
// the caller's narrow relay role deliberately does not hold. Attribution flows through the
|
|
717
|
+
// session link, so nothing here depends on the caller's identity.
|
|
718
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
694
719
|
try {
|
|
695
|
-
const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents',
|
|
720
|
+
const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', runUser);
|
|
696
721
|
if (!(await agent.Load(session.AgentID))) {
|
|
697
722
|
return { Success: false, ErrorMessage: `Co-agent ${session.AgentID} for the session could not be loaded.` };
|
|
698
723
|
}
|
|
699
724
|
|
|
700
|
-
const accountID = await resolveRecordingStorageAccountID(agent,
|
|
725
|
+
const accountID = await resolveRecordingStorageAccountID(agent, runUser, provider);
|
|
701
726
|
if (!accountID) {
|
|
702
727
|
return { Success: false, ErrorMessage: 'No recording storage account is configured for this agent.' };
|
|
703
728
|
}
|
|
@@ -714,7 +739,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
714
739
|
StartedAt: session.RecordingStartedAt ?? new Date(),
|
|
715
740
|
StorageAccountID: accountID,
|
|
716
741
|
SessionID: agentSessionId,
|
|
717
|
-
ContextUser:
|
|
742
|
+
ContextUser: runUser,
|
|
718
743
|
Provider: provider,
|
|
719
744
|
// Sanitized capture-time waveform peaks → persisted as a peaks.json sidecar.
|
|
720
745
|
Peaks: this.sanitizePeaks(peaks),
|
|
@@ -722,7 +747,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
722
747
|
|
|
723
748
|
// Canonical consolidated file written — drop the crash-recovery shards (best-effort).
|
|
724
749
|
if (fileID) {
|
|
725
|
-
await deleteRealtimeRecordingSegments(agentSessionId, accountID,
|
|
750
|
+
await deleteRealtimeRecordingSegments(agentSessionId, accountID, runUser);
|
|
726
751
|
}
|
|
727
752
|
|
|
728
753
|
return {
|
|
@@ -762,11 +787,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
762
787
|
try {
|
|
763
788
|
const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
|
|
764
789
|
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
765
|
-
|
|
790
|
+
// Scoped-anonymous elevation (issue #3371) — same rationale as UploadRealtimeRecording.
|
|
791
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
792
|
+
const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', runUser);
|
|
766
793
|
if (!(await agent.Load(session.AgentID))) {
|
|
767
794
|
return false;
|
|
768
795
|
}
|
|
769
|
-
const accountID = await resolveRecordingStorageAccountID(agent,
|
|
796
|
+
const accountID = await resolveRecordingStorageAccountID(agent, runUser, provider);
|
|
770
797
|
if (!accountID) {
|
|
771
798
|
return false;
|
|
772
799
|
}
|
|
@@ -780,7 +807,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
780
807
|
Audio: buffer,
|
|
781
808
|
MimeType: mimeType,
|
|
782
809
|
StorageAccountID: accountID,
|
|
783
|
-
ContextUser:
|
|
810
|
+
ContextUser: runUser,
|
|
784
811
|
});
|
|
785
812
|
} catch (error) {
|
|
786
813
|
LogError(`RealtimeClientSessionResolver.UploadRealtimeRecordingSegment failed for session ${agentSessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -815,7 +842,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
815
842
|
'assistant',
|
|
816
843
|
this.formatToolTurn(toolName, argsJson, resultJson),
|
|
817
844
|
false,
|
|
818
|
-
contextUser,
|
|
845
|
+
ResolveScopedAnonymousRunUser(contextUser),
|
|
819
846
|
provider,
|
|
820
847
|
);
|
|
821
848
|
}
|
|
@@ -913,7 +940,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
913
940
|
}
|
|
914
941
|
// Delegate to the service so usage writes share the per-run serialization with transcript-message
|
|
915
942
|
// appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
|
|
916
|
-
|
|
943
|
+
// Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
|
|
944
|
+
return this.clientSessionService.AccumulatePromptRunUsage(
|
|
945
|
+
promptRunID, inputDelta, outputDelta, ResolveScopedAnonymousRunUser(contextUser), provider,
|
|
946
|
+
);
|
|
917
947
|
}
|
|
918
948
|
|
|
919
949
|
/** Clamps a relayed token delta: negative / non-finite values become 0. */
|
|
@@ -1123,6 +1153,39 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1123
1153
|
}
|
|
1124
1154
|
}
|
|
1125
1155
|
|
|
1156
|
+
/**
|
|
1157
|
+
* Narrows a session's colleague union (`allowedAgents`) to the agents the CALLER may run.
|
|
1158
|
+
*
|
|
1159
|
+
* {@link assertCanRunTarget} gates the LEAD target at session start, but the union it travels
|
|
1160
|
+
* with was never gated at all — a model-named colleague resolves straight to a delegated run.
|
|
1161
|
+
* That was survivable while the run carried the caller's own identity, because base-agent
|
|
1162
|
+
* re-checks `CanRun` against `contextUser`. Once the run user is elevated for a scoped anonymous
|
|
1163
|
+
* caller (issue #3371) that check sees the SYSTEM user, so this is the only remaining place the
|
|
1164
|
+
* caller's own authority is applied to a colleague. It therefore runs for EVERY caller, elevated
|
|
1165
|
+
* or not — the authorization identity must never depend on the elevation decision.
|
|
1166
|
+
*
|
|
1167
|
+
* `HasPermission` reads AIEngineBase's in-memory caches (no DB round trip) and already fails
|
|
1168
|
+
* closed on error, so an unresolvable agent drops OUT of the union rather than becoming runnable.
|
|
1169
|
+
* A filtered-out colleague is not an error: the delegation layer reports it as "not available in
|
|
1170
|
+
* this session" and lists what remains, which is the same answer the model gets for a typo.
|
|
1171
|
+
*
|
|
1172
|
+
* @param allowedAgents The session's persisted colleague union (absent/empty ⇒ single-target).
|
|
1173
|
+
* @param contextUser The ORIGINAL caller — never the elevated run user.
|
|
1174
|
+
* @returns The subset the caller may run, preserving order.
|
|
1175
|
+
*/
|
|
1176
|
+
private async filterAllowedAgentsByCanRun(
|
|
1177
|
+
allowedAgents: RealtimeAllowedAgent[] | undefined,
|
|
1178
|
+
contextUser: UserInfo,
|
|
1179
|
+
): Promise<RealtimeAllowedAgent[] | undefined> {
|
|
1180
|
+
if (!allowedAgents || allowedAgents.length === 0) {
|
|
1181
|
+
return allowedAgents;
|
|
1182
|
+
}
|
|
1183
|
+
const verdicts = await Promise.all(
|
|
1184
|
+
allowedAgents.map((a) => AIAgentPermissionHelper.HasPermission(a.agentId, contextUser, 'run')),
|
|
1185
|
+
);
|
|
1186
|
+
return allowedAgents.filter((_, i) => verdicts[i]);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1126
1189
|
/**
|
|
1127
1190
|
* Resolves the AUTHORITATIVE target agent id under the co-agent's PAIRING CONSTRAINTS
|
|
1128
1191
|
* (`MJ: AI Agent Co Agents`, ordered by `Sequence`):
|
|
@@ -1584,7 +1647,12 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1584
1647
|
ApplicationID: applicationId,
|
|
1585
1648
|
AppContext: appContext,
|
|
1586
1649
|
},
|
|
1587
|
-
|
|
1650
|
+
// SCOPED-ANONYMOUS ELEVATION (issue #3371): the prepare creates the co-agent
|
|
1651
|
+
// observability AIAgentRun/AIPromptRun/run-step, which a scoped anonymous caller's role
|
|
1652
|
+
// deliberately cannot write. `UserID` above stays the CALLER's id, so run attribution
|
|
1653
|
+
// and memory scope remain the visitor's. Authorization (CanRun, runtime overrides)
|
|
1654
|
+
// already ran on the caller in StartRealtimeClientSession.
|
|
1655
|
+
ResolveScopedAnonymousRunUser(contextUser),
|
|
1588
1656
|
provider,
|
|
1589
1657
|
);
|
|
1590
1658
|
|
|
@@ -2190,7 +2258,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2190
2258
|
detail.HiddenToUser = true;
|
|
2191
2259
|
detail.Message = 'Artifacts produced during a realtime session (system anchor).';
|
|
2192
2260
|
detail.AgentSessionID = session.ID;
|
|
2193
|
-
|
|
2261
|
+
// Attribute the anchor to the SESSION owner, not the (possibly elevated) writer — identical
|
|
2262
|
+
// for every non-elevated caller, whose ownership of the session is already proven.
|
|
2263
|
+
detail.UserID = session.UserID;
|
|
2194
2264
|
if (await detail.Save()) {
|
|
2195
2265
|
return detail.ID;
|
|
2196
2266
|
}
|
|
@@ -750,18 +750,15 @@ export class RemoteBrowserActionResolver extends ResolverBase {
|
|
|
750
750
|
agentSessionID: string,
|
|
751
751
|
frame: { DataBase64: string; Width: number; Height: number; SequenceNumber: number },
|
|
752
752
|
): void {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}),
|
|
763
|
-
sessionId: userPayload.sessionId,
|
|
764
|
-
});
|
|
753
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
754
|
+
resolver: 'RemoteBrowserActionResolver',
|
|
755
|
+
type: 'RemoteBrowserScreencastFrame',
|
|
756
|
+
agentSessionID,
|
|
757
|
+
dataBase64: frame.DataBase64,
|
|
758
|
+
width: frame.Width,
|
|
759
|
+
height: frame.Height,
|
|
760
|
+
seq: frame.SequenceNumber,
|
|
761
|
+
}), userPayload);
|
|
765
762
|
}
|
|
766
763
|
|
|
767
764
|
/**
|
|
@@ -775,19 +772,16 @@ export class RemoteBrowserActionResolver extends ResolverBase {
|
|
|
775
772
|
* @param chunk The encoded audio chunk.
|
|
776
773
|
*/
|
|
777
774
|
private publishAudioChunk(pubSub: PubSubEngine, userPayload: UserPayload, agentSessionID: string, chunk: RemoteBrowserAudioChunk): void {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}),
|
|
789
|
-
sessionId: userPayload.sessionId,
|
|
790
|
-
});
|
|
775
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
776
|
+
resolver: 'RemoteBrowserActionResolver',
|
|
777
|
+
type: 'RemoteBrowserAudioChunk',
|
|
778
|
+
agentSessionID,
|
|
779
|
+
dataBase64: chunk.DataBase64,
|
|
780
|
+
codec: chunk.Codec,
|
|
781
|
+
sampleRate: chunk.SampleRate,
|
|
782
|
+
channels: chunk.Channels,
|
|
783
|
+
seq: chunk.SequenceNumber,
|
|
784
|
+
}), userPayload);
|
|
791
785
|
}
|
|
792
786
|
|
|
793
787
|
/**
|
|
@@ -3,7 +3,6 @@ import { Arg, Ctx, Field, Int, Mutation, ObjectType, Query, Resolver } from 'typ
|
|
|
3
3
|
import { AppContext } from '../types.js';
|
|
4
4
|
import { MJConversationDetailEntity, MJReportEntity } from '@memberjunction/core-entities';
|
|
5
5
|
import { DataContext } from '@memberjunction/data-context';
|
|
6
|
-
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
7
6
|
import { z } from 'zod';
|
|
8
7
|
import mssql from 'mssql';
|
|
9
8
|
import { GetReadOnlyProvider, GetReadWriteProvider } from '../util.js';
|
|
@@ -80,7 +79,15 @@ export class ReportResolverExtended extends ResolverBase {
|
|
|
80
79
|
try {
|
|
81
80
|
const md = GetReadWriteProvider(providers);
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
// Resolve via the stamped payload user (GetUserFromPayload), NOT a fresh UserCache
|
|
83
|
+
// lookup by email. UserCache.Users returns the shared, unstamped instance — re-fetching
|
|
84
|
+
// by email here would silently drop any per-request API-key row-filter binding
|
|
85
|
+
// (APIKeyRowFilters / APIKeyActingContext) already stamped onto userPayload.userRecord,
|
|
86
|
+
// so a filtered key's report-creation work would run unfiltered. GetUserFromPayload
|
|
87
|
+
// prefers userPayload.userRecord and only falls back to a UserCache email lookup when
|
|
88
|
+
// no payload user is present (e.g. non-request-scoped callers), matching every other
|
|
89
|
+
// resolver's contextUser resolution.
|
|
90
|
+
const u = this.GetUserFromPayload(userPayload);
|
|
84
91
|
if (!u) throw new Error('Unable to find user');
|
|
85
92
|
|
|
86
93
|
const cde = md.Entities.find((e) => e.Name === 'MJ: Conversation Details');
|
|
@@ -98,9 +105,10 @@ export class ReportResolverExtended extends ResolverBase {
|
|
|
98
105
|
ON
|
|
99
106
|
cd.ConversationID = c.ID
|
|
100
107
|
WHERE
|
|
101
|
-
cd.ID
|
|
108
|
+
cd.ID=@ConversationDetailID`;
|
|
102
109
|
|
|
103
110
|
const request = new mssql.Request(dataSource);
|
|
111
|
+
request.input('ConversationDetailID', mssql.UniqueIdentifier, ConversationDetailID);
|
|
104
112
|
const result = await request.query(sql);
|
|
105
113
|
if (!result || !result.recordset || result.recordset.length === 0) throw new Error('Unable to retrieve converation details');
|
|
106
114
|
const skipData: { title?: string; reportTitle?: string; userExplanation?: string; messages?: unknown[] } = JSON.parse(result.recordset[0].Message);
|
|
@@ -8,7 +8,6 @@ import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult
|
|
|
8
8
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
9
9
|
import { ChatMessage, ChatMessageContent } from '@memberjunction/ai';
|
|
10
10
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
11
|
-
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
12
11
|
import { startLivenessPulse } from '../generic/FireAndForgetHeartbeat.js';
|
|
13
12
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
14
13
|
import { GetReadWriteProvider } from '../util.js';
|
|
@@ -306,28 +305,22 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
private PublishProgressUpdate(pubSub: PubSubEngine, data: any, userPayload: UserPayload) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}),
|
|
316
|
-
sessionId: userPayload.sessionId,
|
|
317
|
-
});
|
|
308
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
309
|
+
resolver: 'RunAIAgentResolver',
|
|
310
|
+
type: 'ExecutionProgress',
|
|
311
|
+
status: 'ok',
|
|
312
|
+
data,
|
|
313
|
+
}), userPayload);
|
|
318
314
|
}
|
|
319
315
|
|
|
320
316
|
|
|
321
317
|
private PublishStreamingUpdate(pubSub: PubSubEngine, data: any, userPayload: UserPayload) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}),
|
|
329
|
-
sessionId: userPayload.sessionId,
|
|
330
|
-
});
|
|
318
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
319
|
+
resolver: 'RunAIAgentResolver',
|
|
320
|
+
type: 'StreamingContent',
|
|
321
|
+
status: 'ok',
|
|
322
|
+
data,
|
|
323
|
+
}), userPayload);
|
|
331
324
|
}
|
|
332
325
|
|
|
333
326
|
/**
|
|
@@ -814,17 +807,15 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
814
807
|
LogStatus(`📬 Notification sent via ${channelList} (ID: ${result.inAppNotificationId})`);
|
|
815
808
|
|
|
816
809
|
// Publish real-time notification event so client updates immediately
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
})
|
|
827
|
-
});
|
|
810
|
+
// NOTE (B49): normalized from a malformed no-sessionId payload
|
|
811
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
812
|
+
type: 'notification',
|
|
813
|
+
notificationId: result.inAppNotificationId,
|
|
814
|
+
action: 'create',
|
|
815
|
+
title: `${agentName} completed your request`,
|
|
816
|
+
message: message,
|
|
817
|
+
conversationId: conversationId
|
|
818
|
+
}), userPayload);
|
|
828
819
|
|
|
829
820
|
LogStatus(`📡 Published notification event to client`);
|
|
830
821
|
} else if (!result.success) {
|
|
@@ -926,16 +917,14 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
926
917
|
LogStatus(`📬 Feedback request notification sent (ID: ${notifResult.inAppNotificationId})`);
|
|
927
918
|
|
|
928
919
|
// Publish real-time notification event
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
})
|
|
938
|
-
});
|
|
920
|
+
// NOTE (B49): normalized from a malformed no-sessionId payload
|
|
921
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
922
|
+
type: 'notification',
|
|
923
|
+
notificationId: notifResult.inAppNotificationId,
|
|
924
|
+
action: 'create',
|
|
925
|
+
title: `${agentName} needs your input`,
|
|
926
|
+
message: truncatedMessage
|
|
927
|
+
}), userPayload);
|
|
939
928
|
} else if (!notifResult.success) {
|
|
940
929
|
LogError(`Feedback request notification failed: ${notifResult.errors?.join(', ')}`);
|
|
941
930
|
}
|
|
@@ -1294,6 +1283,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1294
1283
|
const pulse = startLivenessPulse({
|
|
1295
1284
|
pubSub,
|
|
1296
1285
|
sessionId,
|
|
1286
|
+
ownerUserId: userPayload.userRecord.ID,
|
|
1297
1287
|
resolver: 'RunAIAgentResolver',
|
|
1298
1288
|
readStatus: () => runRef.current
|
|
1299
1289
|
? { runId: runRef.current.ID, status: runRef.current.Status }
|