@memberjunction/server 5.51.2 → 5.51.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/dist/agentSessions/SessionManager.d.ts +5 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +17 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +136 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +1 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +1 -0
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.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/scopeLimitedPrincipal.d.ts +39 -0
- package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
- package/dist/auth/scopeLimitedPrincipal.js +49 -0
- package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -1
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -34
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3262 -3273
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +124 -4
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +170 -15
- package/dist/generic/CacheInvalidationResolver.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 +148 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +287 -30
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +12 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -50
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +2 -2
- package/dist/realtimeWidget/widgetGuestElevation.js +2 -2
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +39 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +97 -157
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +14 -4
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +18 -6
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +13 -1
- package/dist/resolvers/GetDataResolver.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 +26 -14
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +63 -46
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +24 -27
- package/dist/resolvers/RemoteBrowserActionResolver.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/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.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/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +11 -2
- package/dist/resolvers/SyncRolesUsersResolver.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/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +29 -17
- package/dist/rest/ViewOperationsHandler.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 +90 -89
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +136 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +318 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +66 -27
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +134 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +221 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +173 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +116 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +351 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +164 -0
- package/src/__tests__/SessionManager.test.ts +88 -13
- package/src/__tests__/SessionRunIDVerifier.test.ts +255 -0
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +155 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +241 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +180 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUserRolesDefault.test.ts +81 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
- package/src/__tests__/resolverBase.refusalMessage.test.ts +88 -0
- package/src/__tests__/resolverBase.rls.test.ts +2 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/agentSessions/SessionManager.ts +24 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +179 -0
- package/src/agentSessions/index.ts +1 -0
- package/src/auth/actionAuthorization.ts +119 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +34 -1
- package/src/context.ts +25 -34
- package/src/generated/generated.ts +3262 -3273
- package/src/generic/CacheInvalidationResolver.ts +206 -15
- package/src/generic/FireAndForgetHeartbeat.ts +10 -4
- package/src/generic/PushStatusResolver.ts +79 -4
- package/src/generic/ResolverBase.ts +339 -31
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/index.ts +65 -52
- package/src/multiTenancy/index.ts +24 -2
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +2 -2
- package/src/resolvers/ActionResolver.ts +85 -22
- package/src/resolvers/AdhocQueryResolver.ts +110 -177
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +24 -7
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +14 -1
- package/src/resolvers/MCPResolver.ts +13 -15
- package/src/resolvers/RealtimeClientSessionResolver.ts +82 -50
- package/src/resolvers/RemoteBrowserActionResolver.ts +30 -26
- package/src/resolvers/RunAIAgentResolver.ts +30 -40
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/RunTestResolver.ts +90 -112
- package/src/resolvers/SyncRolesUsersResolver.ts +11 -2
- package/src/resolvers/VersionHistoryResolver.ts +10 -13
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/__tests__/ReportResolver.test.ts +2 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/rest/ViewOperationsHandler.ts +37 -18
- package/src/services/TaskOrchestrator.ts +17 -17
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -27
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -42
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -50
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* and **executes** every tool call the browser relays back. The three mutations here cover the MVP:
|
|
12
12
|
*
|
|
13
13
|
* 1. {@link RealtimeClientSessionResolver.StartRealtimeClientSession} — authorize, create the
|
|
14
|
-
* session (storing the target agent id
|
|
15
|
-
* 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
|
|
16
|
-
*
|
|
14
|
+
* session (storing the target agent id on it), mint the ephemeral config.
|
|
15
|
+
* 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
|
|
16
|
+
* for the target agent stored on the session, re-authorized for the caller on every call.
|
|
17
17
|
* 3. {@link RealtimeClientSessionResolver.RelayRealtimeTranscript} — persist a transcript turn as a
|
|
18
18
|
* `Conversation Detail`.
|
|
19
19
|
*
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* - **Start**: the caller must have `CanRun` on the *target* agent (the Realtime Co-Agent is an internal
|
|
22
22
|
* orchestration agent, so the meaningful gate is the agent doing the real work).
|
|
23
23
|
* - **Execute / Relay**: inbound ownership — the session's `UserID` must equal `contextUser.ID`.
|
|
24
|
+
* - **Session `Config`**: the owner can edit it through the generic entity API, so nothing in it is
|
|
25
|
+
* trusted. Execute re-checks `CanRun` on its target, and its run ids are used only when
|
|
26
|
+
* {@link SessionRunIDVerifier} finds them to be records of the session.
|
|
24
27
|
*
|
|
25
28
|
* @module @memberjunction/server
|
|
26
29
|
*/
|
|
@@ -60,6 +63,7 @@ import { ResolverBase } from '../generic/ResolverBase.js';
|
|
|
60
63
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
61
64
|
import { GetReadWriteProvider } from '../util.js';
|
|
62
65
|
import { SessionManager } from '../agentSessions/index.js';
|
|
66
|
+
import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
|
|
63
67
|
import { resolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
|
|
64
68
|
|
|
65
69
|
/**
|
|
@@ -135,9 +139,9 @@ const MAX_PRIOR_TRANSCRIPT_CHARS = 8_000;
|
|
|
135
139
|
const MAX_PRIOR_TRANSCRIPT_LEGS = 5;
|
|
136
140
|
|
|
137
141
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
142
|
+
* Shape persisted in `AIAgentSession.Config_` for a client-direct voice session. The server writes
|
|
143
|
+
* it, but the session owner can also edit it through the generic entity API, so relays re-check
|
|
144
|
+
* what they read from it (see the authorization model above).
|
|
141
145
|
*/
|
|
142
146
|
interface RealtimeSessionConfig {
|
|
143
147
|
/** The top-level target agent the co-agent voices on behalf of. */
|
|
@@ -351,6 +355,7 @@ export class UploadRealtimeRecordingResult {
|
|
|
351
355
|
export class RealtimeClientSessionResolver extends ResolverBase {
|
|
352
356
|
private readonly sessionManager = new SessionManager();
|
|
353
357
|
private readonly clientSessionService = new RealtimeClientSessionService();
|
|
358
|
+
private readonly runIDVerifier = new SessionRunIDVerifier();
|
|
354
359
|
|
|
355
360
|
/**
|
|
356
361
|
* Start a client-direct realtime voice session targeting `targetAgentId`.
|
|
@@ -362,7 +367,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
362
367
|
* agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
|
|
363
368
|
* see {@link RealtimeClientSessionResolver.resolveCoAgentID} for the full contract.
|
|
364
369
|
* 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
|
|
365
|
-
* config
|
|
370
|
+
* config — the target later relays use, re-authorized for the caller on each relay.
|
|
366
371
|
* 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
|
|
367
372
|
* On failure the just-created session is closed so no half-open session leaks.
|
|
368
373
|
*
|
|
@@ -477,8 +482,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
477
482
|
* Execute a single tool call the browser relayed from its provider socket and return the
|
|
478
483
|
* serialized result for the browser to relay back to the model.
|
|
479
484
|
*
|
|
480
|
-
* Ownership-gated
|
|
481
|
-
*
|
|
485
|
+
* Ownership-gated. The target agent id is read from the session config and `CanRun` on it is
|
|
486
|
+
* checked for the caller on every call, because the owner can edit the config. A direct-action
|
|
487
|
+
* call from a scope-limited caller is refused before any elevation. Heartbeats the session on
|
|
488
|
+
* success.
|
|
482
489
|
*
|
|
483
490
|
* @returns The serialized tool result JSON.
|
|
484
491
|
*/
|
|
@@ -494,13 +501,20 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
494
501
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
495
502
|
const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
|
|
496
503
|
const config = this.readSessionConfig(session);
|
|
504
|
+
await this.assertCanRunTarget(config.targetAgentID, contextUser, provider);
|
|
497
505
|
|
|
498
506
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
|
|
499
507
|
// run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
|
|
500
508
|
// (the caller's role deliberately holds no grants on the run entities). The lead
|
|
501
|
-
// targetAgentID
|
|
502
|
-
//
|
|
509
|
+
// targetAgentID is CanRun-gated against the CALLER just above, and the colleague union
|
|
510
|
+
// just below, so elevation never widens agent authority.
|
|
503
511
|
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
512
|
+
const runIDs = await this.runIDVerifier.Verify(
|
|
513
|
+
session.ID,
|
|
514
|
+
{ CoAgentRunID: config.coAgentRunID, PendingFeedbackRunID: config.pendingFeedbackRunID },
|
|
515
|
+
runUser,
|
|
516
|
+
provider,
|
|
517
|
+
);
|
|
504
518
|
if (runUser !== contextUser) {
|
|
505
519
|
LogStatus(
|
|
506
520
|
`ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
|
|
@@ -518,11 +532,11 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
518
532
|
// row and its context-memory scope must stay the person's, not the system user's.
|
|
519
533
|
AttributionUserID: contextUser.ID,
|
|
520
534
|
// Nest the delegated target-agent run under the co-agent observability run (when present).
|
|
521
|
-
ParentRunID:
|
|
535
|
+
ParentRunID: runIDs.CoAgentRunID,
|
|
522
536
|
Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
|
|
523
537
|
OnProgress: this.buildDelegationProgressCallback(pubSub, userPayload, agentSessionId, callId),
|
|
524
538
|
// Resume a previously-paused delegated run (if any) with the user's answer.
|
|
525
|
-
ResumeRunID:
|
|
539
|
+
ResumeRunID: runIDs.PendingFeedbackRunID,
|
|
526
540
|
},
|
|
527
541
|
runUser,
|
|
528
542
|
provider,
|
|
@@ -558,21 +572,19 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
558
572
|
if (!SIGNIFICANT_PROGRESS_STEPS.includes(progress.step)) {
|
|
559
573
|
return;
|
|
560
574
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}),
|
|
571
|
-
sessionId: userPayload.sessionId,
|
|
572
|
-
});
|
|
575
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
576
|
+
resolver: 'RealtimeClientSessionResolver',
|
|
577
|
+
type: 'RealtimeDelegationProgress',
|
|
578
|
+
agentSessionID,
|
|
579
|
+
callID,
|
|
580
|
+
step: progress.step,
|
|
581
|
+
message: progress.message,
|
|
582
|
+
percentage: progress.percentage,
|
|
583
|
+
}), userPayload);
|
|
573
584
|
};
|
|
574
585
|
}
|
|
575
586
|
|
|
587
|
+
|
|
576
588
|
/**
|
|
577
589
|
* Rolls the session's `pendingFeedbackRunID` forward after a relayed tool call. The id we just
|
|
578
590
|
* consumed (resumed or none) is dropped; a new paused run id is stored only when the run paused
|
|
@@ -643,14 +655,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
643
655
|
// conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
|
|
644
656
|
// The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
|
|
645
657
|
// Conversation Detail above deliberately stays on the caller.
|
|
646
|
-
const
|
|
658
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
659
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
647
660
|
if (promptRunID) {
|
|
648
661
|
await this.clientSessionService.AppendPromptRunMessage(
|
|
649
662
|
promptRunID,
|
|
650
663
|
this.mapTranscriptRoleToChatRole(role),
|
|
651
664
|
text,
|
|
652
665
|
replacesPrevious ?? false,
|
|
653
|
-
|
|
666
|
+
runUser,
|
|
654
667
|
provider,
|
|
655
668
|
);
|
|
656
669
|
}
|
|
@@ -831,7 +844,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
831
844
|
): Promise<boolean> {
|
|
832
845
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
833
846
|
const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
|
|
834
|
-
const
|
|
847
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
848
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
835
849
|
if (!promptRunID) {
|
|
836
850
|
return false;
|
|
837
851
|
}
|
|
@@ -840,7 +854,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
840
854
|
'assistant',
|
|
841
855
|
this.formatToolTurn(toolName, argsJson, resultJson),
|
|
842
856
|
false,
|
|
843
|
-
|
|
857
|
+
runUser,
|
|
844
858
|
provider,
|
|
845
859
|
);
|
|
846
860
|
}
|
|
@@ -932,16 +946,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
932
946
|
return true; // nothing to add — a no-op flush is a success, not a failure
|
|
933
947
|
}
|
|
934
948
|
|
|
935
|
-
|
|
949
|
+
// Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
|
|
950
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
951
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
936
952
|
if (!promptRunID) {
|
|
937
953
|
return false; // no observability prompt run for this session — logged in the helper
|
|
938
954
|
}
|
|
939
955
|
// Delegate to the service so usage writes share the per-run serialization with transcript-message
|
|
940
956
|
// appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
|
|
941
|
-
|
|
942
|
-
return this.clientSessionService.AccumulatePromptRunUsage(
|
|
943
|
-
promptRunID, inputDelta, outputDelta, ResolveScopedAnonymousRunUser(contextUser), provider,
|
|
944
|
-
);
|
|
957
|
+
return this.clientSessionService.AccumulatePromptRunUsage(promptRunID, inputDelta, outputDelta, runUser, provider);
|
|
945
958
|
}
|
|
946
959
|
|
|
947
960
|
/** Clamps a relayed token delta: negative / non-finite values become 0. */
|
|
@@ -968,24 +981,40 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
968
981
|
|
|
969
982
|
/**
|
|
970
983
|
* Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
|
|
971
|
-
* throw-on-missing-target semantics
|
|
972
|
-
*
|
|
984
|
+
* throw-on-missing-target semantics, and keeps it only when it is the prompt run of the
|
|
985
|
+
* session's co-agent run (the owner can edit the config). Usage relay is best-effort, so a
|
|
986
|
+
* missing/malformed config, an absent id, or an id of another session just logs and returns `null`.
|
|
987
|
+
*
|
|
988
|
+
* @param runUser The user the prompt-run write runs as; the check runs as the same user.
|
|
973
989
|
*/
|
|
974
|
-
private readPromptRunID(
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
990
|
+
private async readPromptRunID(
|
|
991
|
+
session: MJAIAgentSessionEntity,
|
|
992
|
+
runUser: UserInfo,
|
|
993
|
+
provider: IMetadataProvider,
|
|
994
|
+
): Promise<string | null> {
|
|
995
|
+
const config = this.tryReadSessionConfig(session);
|
|
996
|
+
if (!config) {
|
|
997
|
+
LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
if (!config.promptRunID) {
|
|
980
1001
|
LogStatus(
|
|
981
1002
|
`RelayRealtimeUsage: session ${session.ID} has no co-agent promptRunID (observability run ` +
|
|
982
1003
|
'creation was skipped or failed) — usage delta dropped.',
|
|
983
1004
|
);
|
|
984
1005
|
return null;
|
|
985
|
-
}
|
|
986
|
-
|
|
1006
|
+
}
|
|
1007
|
+
const verified = await this.runIDVerifier.Verify(
|
|
1008
|
+
session.ID,
|
|
1009
|
+
{ CoAgentRunID: config.coAgentRunID, PromptRunID: config.promptRunID },
|
|
1010
|
+
runUser,
|
|
1011
|
+
provider,
|
|
1012
|
+
);
|
|
1013
|
+
if (!verified.PromptRunID) {
|
|
1014
|
+
LogError(`RelayRealtimeUsage: session ${session.ID} names a promptRunID that is not its co-agent prompt run — ignored.`);
|
|
987
1015
|
return null;
|
|
988
1016
|
}
|
|
1017
|
+
return verified.PromptRunID;
|
|
989
1018
|
}
|
|
990
1019
|
|
|
991
1020
|
/**
|
|
@@ -1135,8 +1164,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1135
1164
|
}
|
|
1136
1165
|
|
|
1137
1166
|
/**
|
|
1138
|
-
* Authorization gate for
|
|
1139
|
-
*
|
|
1167
|
+
* Authorization gate for the **target** agent: the caller must be able to run it. Denial throws.
|
|
1168
|
+
* {@link RealtimeClientSessionResolver.StartRealtimeClientSession} runs it before any session is
|
|
1169
|
+
* created, and {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} runs it on every
|
|
1170
|
+
* relayed call, because the owner can edit the target in the session config.
|
|
1140
1171
|
*/
|
|
1141
1172
|
private async assertCanRunTarget(
|
|
1142
1173
|
targetAgentId: string,
|
|
@@ -1683,7 +1714,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1683
1714
|
|
|
1684
1715
|
/**
|
|
1685
1716
|
* Writes the co-agent observability run ids into the session's `Config_` alongside the
|
|
1686
|
-
*
|
|
1717
|
+
* `targetAgentID`, then saves the session. These ids are read back on close to
|
|
1687
1718
|
* finalize the runs (and on relay to nest delegated runs). Best-effort: a save failure is logged,
|
|
1688
1719
|
* not thrown — the voice session still proceeds, it just won't carry the run ids.
|
|
1689
1720
|
*/
|
|
@@ -2416,9 +2447,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2416
2447
|
}
|
|
2417
2448
|
|
|
2418
2449
|
/**
|
|
2419
|
-
* Parses the session's persisted config, returning
|
|
2420
|
-
*
|
|
2421
|
-
*
|
|
2450
|
+
* Parses the session's persisted config, returning its `targetAgentID` plus any observability run
|
|
2451
|
+
* ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed or carries no
|
|
2452
|
+
* target — a relay cannot proceed without a target. The owner can edit the config, so callers
|
|
2453
|
+
* re-check what they act on.
|
|
2422
2454
|
*/
|
|
2423
2455
|
private readSessionConfig(session: MJAIAgentSessionEntity): RealtimeSessionConfig {
|
|
2424
2456
|
const raw = session.Config_;
|
|
@@ -24,6 +24,7 @@ import { MJAIAgentSessionEntity, MJAIAgentEntity } from '@memberjunction/core-en
|
|
|
24
24
|
import { RemoteBrowserEngine } from '@memberjunction/remote-browser-server';
|
|
25
25
|
import { beginBrowserGoalStep, finalizeBrowserGoalStep, extractCoAgentRunID } from '../agentSessions/remoteBrowserGoalEngine.js';
|
|
26
26
|
import { RemoteBrowserGoalRegistry } from '../agentSessions/remoteBrowserGoalRegistry.js';
|
|
27
|
+
import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
|
|
27
28
|
import { randomUUID } from 'node:crypto';
|
|
28
29
|
import {
|
|
29
30
|
RemoteBrowserAction,
|
|
@@ -240,6 +241,9 @@ export class RemoteBrowserActionResolver extends ResolverBase {
|
|
|
240
241
|
*/
|
|
241
242
|
private startedScreencasts = new Set<string>();
|
|
242
243
|
|
|
244
|
+
/** Checks the co-agent run id read from a session's owner-editable `Config_` before a step is attached to it. */
|
|
245
|
+
private readonly runIDVerifier = new SessionRunIDVerifier();
|
|
246
|
+
|
|
243
247
|
/**
|
|
244
248
|
* Agent-session ids whose live tab-audio stream this resolver has already started. Keyed by
|
|
245
249
|
* `agentSessionID` so a re-issued {@link RemoteBrowserActionResolver.StartRemoteBrowserAudioStream}
|
|
@@ -327,7 +331,13 @@ export class RemoteBrowserActionResolver extends ResolverBase {
|
|
|
327
331
|
const providerName = await this.resolveProviderName(session, contextUser, provider);
|
|
328
332
|
// Observability: nest this goal's many prompt runs under ONE "Browser goal" step on the realtime
|
|
329
333
|
// co-agent run (when the session has one). Best-effort — a null step just means the goal runs unlinked.
|
|
330
|
-
|
|
334
|
+
// The owner can edit Config_, so its run id is used only when it is a run of this session.
|
|
335
|
+
const { CoAgentRunID: coAgentRunID } = await this.runIDVerifier.Verify(
|
|
336
|
+
session.ID,
|
|
337
|
+
{ CoAgentRunID: extractCoAgentRunID(session.Config_) },
|
|
338
|
+
contextUser,
|
|
339
|
+
provider,
|
|
340
|
+
);
|
|
331
341
|
const goalStep = await beginBrowserGoalStep(provider, contextUser, coAgentRunID, goal);
|
|
332
342
|
|
|
333
343
|
// ASYNC START: a goal loop can run for minutes; do NOT hold this request open for it (browser
|
|
@@ -750,18 +760,15 @@ export class RemoteBrowserActionResolver extends ResolverBase {
|
|
|
750
760
|
agentSessionID: string,
|
|
751
761
|
frame: { DataBase64: string; Width: number; Height: number; SequenceNumber: number },
|
|
752
762
|
): void {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}),
|
|
763
|
-
sessionId: userPayload.sessionId,
|
|
764
|
-
});
|
|
763
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
764
|
+
resolver: 'RemoteBrowserActionResolver',
|
|
765
|
+
type: 'RemoteBrowserScreencastFrame',
|
|
766
|
+
agentSessionID,
|
|
767
|
+
dataBase64: frame.DataBase64,
|
|
768
|
+
width: frame.Width,
|
|
769
|
+
height: frame.Height,
|
|
770
|
+
seq: frame.SequenceNumber,
|
|
771
|
+
}), userPayload);
|
|
765
772
|
}
|
|
766
773
|
|
|
767
774
|
/**
|
|
@@ -775,19 +782,16 @@ export class RemoteBrowserActionResolver extends ResolverBase {
|
|
|
775
782
|
* @param chunk The encoded audio chunk.
|
|
776
783
|
*/
|
|
777
784
|
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
|
-
});
|
|
785
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
786
|
+
resolver: 'RemoteBrowserActionResolver',
|
|
787
|
+
type: 'RemoteBrowserAudioChunk',
|
|
788
|
+
agentSessionID,
|
|
789
|
+
dataBase64: chunk.DataBase64,
|
|
790
|
+
codec: chunk.Codec,
|
|
791
|
+
sampleRate: chunk.SampleRate,
|
|
792
|
+
channels: chunk.Channels,
|
|
793
|
+
seq: chunk.SequenceNumber,
|
|
794
|
+
}), userPayload);
|
|
791
795
|
}
|
|
792
796
|
|
|
793
797
|
/**
|
|
@@ -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 }
|
|
@@ -2,7 +2,7 @@ import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
|
|
|
2
2
|
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { LogError, LogStatus, Metadata, RunView } from '@memberjunction/core';
|
|
4
4
|
import { MJTemplateContentEntity, MJTemplateEntityExtended } from '@memberjunction/core-entities';
|
|
5
|
-
import { TemplateEngineServer } from '@memberjunction/templates';
|
|
5
|
+
import { GetTemplateRunRefusal, TemplateEngineServer } from '@memberjunction/templates';
|
|
6
6
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
7
|
import { GetReadWriteProvider } from '../util.js';
|
|
8
8
|
|
|
@@ -45,6 +45,18 @@ export class RunTemplateResolver extends ResolverBase {
|
|
|
45
45
|
try {
|
|
46
46
|
LogStatus(`=== RUNNING TEMPLATE FOR ID: ${templateId} ===`);
|
|
47
47
|
|
|
48
|
+
// Same rule as the Template.Run remote operation, checked before anything is loaded
|
|
49
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
50
|
+
const p = GetReadWriteProvider(providers);
|
|
51
|
+
const refusal = GetTemplateRunRefusal(currentUser, p);
|
|
52
|
+
if (refusal) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: refusal,
|
|
56
|
+
executionTimeMs: Date.now() - startTime
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
// Parse context data (JSON string)
|
|
49
61
|
let data = {};
|
|
50
62
|
if (contextData) {
|
|
@@ -59,17 +71,6 @@ export class RunTemplateResolver extends ResolverBase {
|
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
// Get current user from payload
|
|
63
|
-
const currentUser = this.GetUserFromPayload(userPayload);
|
|
64
|
-
if (!currentUser) {
|
|
65
|
-
return {
|
|
66
|
-
success: false,
|
|
67
|
-
error: 'Unable to determine current user',
|
|
68
|
-
executionTimeMs: Date.now() - startTime
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const p = GetReadWriteProvider(providers);
|
|
73
74
|
// Load the template entity
|
|
74
75
|
const templateEntity = await p.GetEntityObject<MJTemplateEntityExtended>('MJ: Templates', currentUser);
|
|
75
76
|
await templateEntity.Load(templateId);
|