@memberjunction/server 6.1.4 → 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.
- package/README.md +30 -0
- package/dist/.tsbuildinfo +1 -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 +129 -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/exampleNewUserSubClass.js +2 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +17 -15
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +55 -38
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts +14 -33
- package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.js +20 -52
- package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +15 -11
- package/dist/auth/newUsers.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/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +3417 -3428
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +95 -11
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +186 -76
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -45
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- 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 +4 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- 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 +35 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +92 -166
- 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 +8 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +11 -48
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
- package/dist/resolvers/FetchEntityVectorsResolver.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/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +2 -1
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +83 -22
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +174 -72
- 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 +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +2 -1
- 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/SearchKnowledgeResolver.d.ts +1 -0
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.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/TaskGraphActionRunner.d.ts +21 -8
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +43 -16
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/package.json +92 -92
- 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 +3 -3
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- 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 +137 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +310 -31
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
- package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SessionManager.test.ts +94 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +131 -0
- package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLink.test.ts +15 -76
- package/src/__tests__/magicLinkService.consume.test.ts +230 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUsers.test.ts +62 -5
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/agentSessions/SessionManager.ts +23 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +1 -0
- package/src/auth/actionAuthorization.ts +119 -0
- package/src/auth/exampleNewUserSubClass.ts +2 -2
- package/src/auth/magicLink/MagicLinkService.ts +61 -39
- package/src/auth/magicLink/magicLinkCore.ts +19 -56
- package/src/auth/newUsers.ts +15 -11
- package/src/context.ts +25 -34
- package/src/generated/graphql-schemas/__mj.ts +3417 -3428
- package/src/generic/CacheInvalidationResolver.ts +74 -19
- package/src/generic/ResolverBase.ts +207 -80
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/index.ts +49 -46
- package/src/isolatedProvider.ts +22 -0
- package/src/multiTenancy/index.ts +24 -2
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +105 -187
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +11 -52
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +14 -1
- package/src/resolvers/QuerySystemUserResolver.ts +2 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +196 -77
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +4 -1
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
- package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/rest/ViewOperationsHandler.ts +37 -18
- package/src/services/TaskGraphActionRunner.ts +45 -16
- 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
|
@@ -23,9 +23,9 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
23
23
|
* and **executes** every tool call the browser relays back. The three mutations here cover the MVP:
|
|
24
24
|
*
|
|
25
25
|
* 1. {@link RealtimeClientSessionResolver.StartRealtimeClientSession} — authorize, create the
|
|
26
|
-
* session (storing the target agent id
|
|
27
|
-
* 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
|
|
28
|
-
*
|
|
26
|
+
* session (storing the target agent id on it), mint the ephemeral config.
|
|
27
|
+
* 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
|
|
28
|
+
* for the target agent stored on the session, re-authorized for the caller on every call.
|
|
29
29
|
* 3. {@link RealtimeClientSessionResolver.RelayRealtimeTranscript} — persist a transcript turn as a
|
|
30
30
|
* `Conversation Detail`.
|
|
31
31
|
*
|
|
@@ -33,6 +33,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
33
33
|
* - **Start**: the caller must have `CanRun` on the *target* agent (the Realtime Co-Agent is an internal
|
|
34
34
|
* orchestration agent, so the meaningful gate is the agent doing the real work).
|
|
35
35
|
* - **Execute / Relay**: inbound ownership — the session's `UserID` must equal `contextUser.ID`.
|
|
36
|
+
* - **Session `Config`**: the owner can edit it through the generic entity API, so nothing in it is
|
|
37
|
+
* trusted. Execute re-checks `CanRun` on its target, direct actions never come from it, and its
|
|
38
|
+
* run ids are used only when {@link SessionRunIDVerifier} finds them to be records of the session.
|
|
39
|
+
* - **Scope-limited callers** (anonymous and resource-scoped magic-link sessions, including public
|
|
40
|
+
* web-widget guests): no direct actions. Their scope exists only as row-level filters, which an
|
|
41
|
+
* action can read around, and a scoped anonymous caller's relay runs as the system user. They
|
|
42
|
+
* reach the target agent only through `invoke-target-agent`.
|
|
36
43
|
*
|
|
37
44
|
* @module @memberjunction/server
|
|
38
45
|
*/
|
|
@@ -41,10 +48,12 @@ import { AuthorizationEvaluator, LogError, LogStatus, RunView } from '@memberjun
|
|
|
41
48
|
import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
|
|
42
49
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
43
50
|
import { AIAgentPermissionHelper, AIEngineBase } from '@memberjunction/ai-engine-base';
|
|
44
|
-
import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
|
|
51
|
+
import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, INVOKE_TARGET_AGENT_TOOL_NAME, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
|
|
45
52
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
46
53
|
import { GetReadWriteProvider } from '../util.js';
|
|
47
54
|
import { SessionManager } from '../agentSessions/index.js';
|
|
55
|
+
import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
|
|
56
|
+
import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
|
|
48
57
|
import { resolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
|
|
49
58
|
/**
|
|
50
59
|
* Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
|
|
@@ -265,6 +274,31 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
265
274
|
// state on — otherwise that cleanup silently no-ops on a freshly-constructed instance.
|
|
266
275
|
this.clientSessionService = new RealtimeClientSessionService();
|
|
267
276
|
this.sessionManager = new SessionManager(this.clientSessionService);
|
|
277
|
+
this.runIDVerifier = new SessionRunIDVerifier();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
|
|
281
|
+
* `Message`, `Error` and every `Errors[]` entry (validation errors included, since `Save()`
|
|
282
|
+
* runs `Validate()` and records its errors there), so there is no separate `Validate()` call.
|
|
283
|
+
* When a subclass refused the write without recording anything, say so explicitly instead of
|
|
284
|
+
* "unknown error", which is what hid MJ#4791.
|
|
285
|
+
*
|
|
286
|
+
* Only a NULL `LatestResult` proves the refusal happened before the provider (nothing was
|
|
287
|
+
* registered at all). A SUCCESS `LatestResult` is an earlier save on the same instance — the
|
|
288
|
+
* refusal registered nothing — so its text must never be reported as this failure's reason.
|
|
289
|
+
*/
|
|
290
|
+
describeSaveFailure(entity) {
|
|
291
|
+
const latest = entity.LatestResult;
|
|
292
|
+
if (!latest) {
|
|
293
|
+
return `no failure detail recorded (LatestResult null; ResultHistory length ${entity.ResultHistory.length}) ` +
|
|
294
|
+
'— the write was refused before reaching the provider';
|
|
295
|
+
}
|
|
296
|
+
if (latest.Success) {
|
|
297
|
+
return 'no failure detail recorded (latest result entry is a prior success; ' +
|
|
298
|
+
`ResultHistory length ${entity.ResultHistory.length})`;
|
|
299
|
+
}
|
|
300
|
+
const detail = latest.CompleteMessage?.trim();
|
|
301
|
+
return detail || `no failure detail recorded (LatestResult empty; ResultHistory length ${entity.ResultHistory.length})`;
|
|
268
302
|
}
|
|
269
303
|
/**
|
|
270
304
|
* Start a client-direct realtime voice session targeting `targetAgentId`.
|
|
@@ -276,7 +310,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
276
310
|
* agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
|
|
277
311
|
* see {@link RealtimeClientSessionResolver.resolveCoAgentID} for the full contract.
|
|
278
312
|
* 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
|
|
279
|
-
* config
|
|
313
|
+
* config — the target later relays use, re-authorized for the caller on each relay.
|
|
280
314
|
* 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
|
|
281
315
|
* On failure the just-created session is closed so no half-open session leaks.
|
|
282
316
|
*
|
|
@@ -363,8 +397,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
363
397
|
* Execute a single tool call the browser relayed from its provider socket and return the
|
|
364
398
|
* serialized result for the browser to relay back to the model.
|
|
365
399
|
*
|
|
366
|
-
* Ownership-gated
|
|
367
|
-
*
|
|
400
|
+
* Ownership-gated. The target agent id is read from the session config and `CanRun` on it is
|
|
401
|
+
* checked for the caller on every call, because the owner can edit the config. A direct-action
|
|
402
|
+
* call from a scope-limited caller is refused before any elevation. Heartbeats the session on
|
|
403
|
+
* success.
|
|
368
404
|
*
|
|
369
405
|
* @returns The serialized tool result JSON.
|
|
370
406
|
*/
|
|
@@ -372,12 +408,17 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
372
408
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
373
409
|
const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
|
|
374
410
|
const config = this.readSessionConfig(session);
|
|
411
|
+
await this.assertCanRunTarget(config.targetAgentID, contextUser, provider);
|
|
412
|
+
if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME && IsScopeLimitedPrincipal(contextUser)) {
|
|
413
|
+
return this.refuseDirectAction(agentSessionId, toolName);
|
|
414
|
+
}
|
|
375
415
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
|
|
376
416
|
// run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
|
|
377
417
|
// (the caller's role deliberately holds no grants on the run entities). The lead
|
|
378
|
-
// targetAgentID
|
|
379
|
-
//
|
|
418
|
+
// targetAgentID is CanRun-gated against the CALLER just above, and the colleague union
|
|
419
|
+
// just below, so elevation never widens agent authority.
|
|
380
420
|
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
421
|
+
const runIDs = await this.runIDVerifier.Verify(session.ID, { CoAgentRunID: config.coAgentRunID, PendingFeedbackRunID: config.pendingFeedbackRunID }, runUser, provider);
|
|
381
422
|
if (runUser !== contextUser) {
|
|
382
423
|
LogStatus(`ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
|
|
383
424
|
'under the system user (scoped-anonymous caller).');
|
|
@@ -390,20 +431,21 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
390
431
|
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
391
432
|
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
392
433
|
AllowedAgents: await this.filterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
|
|
393
|
-
DirectActions:
|
|
434
|
+
// No DirectActions: the service takes them from the session's start-time projection
|
|
435
|
+
// or the target agent's metadata, never from the owner-editable config.
|
|
394
436
|
// Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
|
|
395
437
|
// row and its context-memory scope must stay the person's, not the system user's.
|
|
396
438
|
AttributionUserID: contextUser.ID,
|
|
397
439
|
// Nest the delegated target-agent run under the co-agent observability run (when present).
|
|
398
|
-
ParentRunID:
|
|
440
|
+
ParentRunID: runIDs.CoAgentRunID,
|
|
399
441
|
Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
|
|
400
442
|
OnProgress: this.buildDelegationProgressCallback(pubSub, userPayload, agentSessionId, callId),
|
|
401
443
|
// Resume a previously-paused delegated run (if any) with the user's answer.
|
|
402
|
-
ResumeRunID:
|
|
444
|
+
ResumeRunID: runIDs.PendingFeedbackRunID,
|
|
403
445
|
}, runUser, provider);
|
|
404
446
|
const durationMs = Date.now() - startTime;
|
|
405
447
|
LogStatus(`ExecuteRealtimeSessionTool: completed relayed tool '${toolName}' (callId: ${callId}) in ${durationMs}ms`);
|
|
406
|
-
if (toolName !==
|
|
448
|
+
if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME) {
|
|
407
449
|
await this.persistDirectActionTurn(session, callId, toolName, argsJson, ResultJson, durationMs, contextUser, provider);
|
|
408
450
|
}
|
|
409
451
|
// Roll the paused-run id forward in the session config: clear the one we just consumed, and
|
|
@@ -412,7 +454,9 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
412
454
|
// Junction-link any artifacts the delegated run produced into the session's conversation
|
|
413
455
|
// history (best-effort) — so chat, session review, and resume carryover can all see them.
|
|
414
456
|
// Runs as `runUser`: the junction entity is not among an anonymous caller's relay grants.
|
|
415
|
-
|
|
457
|
+
// The hidden anchor detail it may need to create, though, is written by the CALLER (#4791)
|
|
458
|
+
// — same reason `persistDirectActionTurn` is: the System user is not the conversation owner.
|
|
459
|
+
await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, contextUser, provider);
|
|
416
460
|
await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
|
|
417
461
|
return ResultJson;
|
|
418
462
|
}
|
|
@@ -439,6 +483,18 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
439
483
|
}), userPayload);
|
|
440
484
|
};
|
|
441
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* The tool result for a direct-action call from a scope-limited caller (see the authorization
|
|
488
|
+
* model above). It has the broker's result shape, so the model hears that the tool is not
|
|
489
|
+
* available and can ask the target agent through `invoke-target-agent` instead.
|
|
490
|
+
*/
|
|
491
|
+
refuseDirectAction(agentSessionId, toolName) {
|
|
492
|
+
LogStatus(`ExecuteRealtimeSessionTool: refused direct action '${toolName}' for a scope-limited caller on session ${agentSessionId}.`);
|
|
493
|
+
return JSON.stringify({
|
|
494
|
+
success: false,
|
|
495
|
+
output: `The tool '${toolName}' is not available in this session. Ask the agent to do it with ${INVOKE_TARGET_AGENT_TOOL_NAME}.`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
442
498
|
/**
|
|
443
499
|
* Rolls the session's `pendingFeedbackRunID` forward after a relayed tool call. The id we just
|
|
444
500
|
* consumed (resumed or none) is dropped; a new paused run id is stored only when the run paused
|
|
@@ -463,7 +519,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
463
519
|
};
|
|
464
520
|
session.Config_ = JSON.stringify(next);
|
|
465
521
|
if (!(await session.Save())) {
|
|
466
|
-
LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed
|
|
522
|
+
LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
|
|
467
523
|
}
|
|
468
524
|
}
|
|
469
525
|
/**
|
|
@@ -492,9 +548,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
492
548
|
// conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
|
|
493
549
|
// The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
|
|
494
550
|
// Conversation Detail above deliberately stays on the caller.
|
|
495
|
-
const
|
|
551
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
552
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
496
553
|
if (promptRunID) {
|
|
497
|
-
await this.clientSessionService.AppendPromptRunMessage(promptRunID, this.mapTranscriptRoleToChatRole(role), text, replacesPrevious ?? false,
|
|
554
|
+
await this.clientSessionService.AppendPromptRunMessage(promptRunID, this.mapTranscriptRoleToChatRole(role), text, replacesPrevious ?? false, runUser, provider);
|
|
498
555
|
}
|
|
499
556
|
await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
|
|
500
557
|
return true;
|
|
@@ -647,11 +704,12 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
647
704
|
// Persist the tool turn facts into a hidden ConversationDetail turn so session review
|
|
648
705
|
// can reconstruct the tool card dynamically on load (Option 1). Best-effort.
|
|
649
706
|
await this.persistDirectActionTurn(session, undefined, toolName, argsJson, resultJson, undefined, contextUser, provider);
|
|
650
|
-
const
|
|
707
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
708
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
651
709
|
if (!promptRunID) {
|
|
652
710
|
return false;
|
|
653
711
|
}
|
|
654
|
-
return this.clientSessionService.AppendPromptRunMessage(promptRunID, 'assistant', this.formatToolTurn(toolName, argsJson, resultJson), false,
|
|
712
|
+
return this.clientSessionService.AppendPromptRunMessage(promptRunID, 'assistant', this.formatToolTurn(toolName, argsJson, resultJson), false, runUser, provider);
|
|
655
713
|
}
|
|
656
714
|
/**
|
|
657
715
|
* Formats a co-agent tool call into a compact, human-readable run line: `🔧 <tool> <args> → <result>`,
|
|
@@ -726,14 +784,15 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
726
784
|
if (inputDelta === 0 && outputDelta === 0) {
|
|
727
785
|
return true; // nothing to add — a no-op flush is a success, not a failure
|
|
728
786
|
}
|
|
729
|
-
|
|
787
|
+
// Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
|
|
788
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
789
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
730
790
|
if (!promptRunID) {
|
|
731
791
|
return false; // no observability prompt run for this session — logged in the helper
|
|
732
792
|
}
|
|
733
793
|
// Delegate to the service so usage writes share the per-run serialization with transcript-message
|
|
734
794
|
// appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
|
|
735
|
-
|
|
736
|
-
return this.clientSessionService.AccumulatePromptRunUsage(promptRunID, inputDelta, outputDelta, ResolveScopedAnonymousRunUser(contextUser), provider);
|
|
795
|
+
return this.clientSessionService.AccumulatePromptRunUsage(promptRunID, inputDelta, outputDelta, runUser, provider);
|
|
737
796
|
}
|
|
738
797
|
/** Clamps a relayed token delta: negative / non-finite values become 0. */
|
|
739
798
|
clampTokenDelta(value) {
|
|
@@ -757,23 +816,29 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
757
816
|
}
|
|
758
817
|
/**
|
|
759
818
|
* Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
|
|
760
|
-
* throw-on-missing-target semantics
|
|
761
|
-
*
|
|
819
|
+
* throw-on-missing-target semantics, and keeps it only when it is the prompt run of the
|
|
820
|
+
* session's co-agent run (the owner can edit the config). Usage relay is best-effort, so a
|
|
821
|
+
* missing/malformed config, an absent id, or an id of another session just logs and returns `null`.
|
|
822
|
+
*
|
|
823
|
+
* @param runUser The user the prompt-run write runs as; the check runs as the same user.
|
|
762
824
|
*/
|
|
763
|
-
readPromptRunID(session) {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
825
|
+
async readPromptRunID(session, runUser, provider) {
|
|
826
|
+
const config = this.tryReadSessionConfig(session);
|
|
827
|
+
if (!config) {
|
|
828
|
+
LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
if (!config.promptRunID) {
|
|
769
832
|
LogStatus(`RelayRealtimeUsage: session ${session.ID} has no co-agent promptRunID (observability run ` +
|
|
770
833
|
'creation was skipped or failed) — usage delta dropped.');
|
|
771
834
|
return null;
|
|
772
835
|
}
|
|
773
|
-
|
|
774
|
-
|
|
836
|
+
const verified = await this.runIDVerifier.Verify(session.ID, { CoAgentRunID: config.coAgentRunID, PromptRunID: config.promptRunID }, runUser, provider);
|
|
837
|
+
if (!verified.PromptRunID) {
|
|
838
|
+
LogError(`RelayRealtimeUsage: session ${session.ID} names a promptRunID that is not its co-agent prompt run — ignored.`);
|
|
775
839
|
return null;
|
|
776
840
|
}
|
|
841
|
+
return verified.PromptRunID;
|
|
777
842
|
}
|
|
778
843
|
/**
|
|
779
844
|
* Persist an interactive channel's state of record (e.g. the live whiteboard's serialized
|
|
@@ -888,8 +953,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
888
953
|
return { contextUser, provider: GetReadWriteProvider(providers) };
|
|
889
954
|
}
|
|
890
955
|
/**
|
|
891
|
-
* Authorization gate for
|
|
892
|
-
*
|
|
956
|
+
* Authorization gate for the **target** agent: the caller must be able to run it. Denial throws.
|
|
957
|
+
* {@link RealtimeClientSessionResolver.StartRealtimeClientSession} runs it before any session is
|
|
958
|
+
* created, and {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} runs it on every
|
|
959
|
+
* relayed call, because the owner can edit the target in the session config.
|
|
893
960
|
*/
|
|
894
961
|
async assertCanRunTarget(targetAgentId, contextUser, _provider) {
|
|
895
962
|
const canRun = await AIAgentPermissionHelper.HasPermission(targetAgentId, contextUser, 'run');
|
|
@@ -1315,6 +1382,9 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1315
1382
|
// (allowed-agent union); the snapshot is injected into the system prompt at mint.
|
|
1316
1383
|
ApplicationID: applicationId,
|
|
1317
1384
|
AppContext: appContext,
|
|
1385
|
+
// Scope-limited callers get no direct actions (see the authorization model above), so
|
|
1386
|
+
// the model is not given tools that the relay refuses.
|
|
1387
|
+
ExcludeDirectActions: IsScopeLimitedPrincipal(contextUser),
|
|
1318
1388
|
},
|
|
1319
1389
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): the prepare creates the co-agent
|
|
1320
1390
|
// observability AIAgentRun/AIPromptRun/run-step, which a scoped anonymous caller's role
|
|
@@ -1346,9 +1416,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1346
1416
|
}
|
|
1347
1417
|
/**
|
|
1348
1418
|
* Writes the co-agent observability run ids into the session's `Config_` alongside the
|
|
1349
|
-
*
|
|
1350
|
-
* finalize the runs (and on relay to nest delegated runs).
|
|
1351
|
-
* not thrown — the voice session still proceeds,
|
|
1419
|
+
* `targetAgentID`, then saves the session. These ids are read back on close to
|
|
1420
|
+
* finalize the runs (and on relay to nest delegated runs). Relays never read the direct actions
|
|
1421
|
+
* from it. Best-effort: a save failure is logged, not thrown — the voice session still proceeds,
|
|
1422
|
+
* it just won't carry the run ids.
|
|
1352
1423
|
*/
|
|
1353
1424
|
async persistObservabilityRunIDs(session, targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID, applicationID, allowedAgents, directActions) {
|
|
1354
1425
|
// Preserve any server-authoritative voice deadline stamped at session start (this rebuilds the
|
|
@@ -1367,7 +1438,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1367
1438
|
session.Config_ = JSON.stringify(config);
|
|
1368
1439
|
const saved = await session.Save();
|
|
1369
1440
|
if (!saved) {
|
|
1370
|
-
LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed
|
|
1441
|
+
LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
|
|
1371
1442
|
}
|
|
1372
1443
|
}
|
|
1373
1444
|
/**
|
|
@@ -1451,7 +1522,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1451
1522
|
const saved = await row.Save();
|
|
1452
1523
|
if (!saved) {
|
|
1453
1524
|
LogError(`SaveSessionChannelState: save failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
1454
|
-
`${row
|
|
1525
|
+
`${this.describeSaveFailure(row)}`);
|
|
1455
1526
|
}
|
|
1456
1527
|
return saved;
|
|
1457
1528
|
}
|
|
@@ -1675,7 +1746,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1675
1746
|
artifact.UserID = contextUser.ID;
|
|
1676
1747
|
artifact.Visibility = 'Always';
|
|
1677
1748
|
if (!(await artifact.Save())) {
|
|
1678
|
-
const message = `SaveSessionChannelArtifact: artifact save failed: ${artifact
|
|
1749
|
+
const message = `SaveSessionChannelArtifact: artifact save failed for session ${session.ID}: ${this.describeSaveFailure(artifact)}`;
|
|
1679
1750
|
LogError(message);
|
|
1680
1751
|
return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
|
|
1681
1752
|
}
|
|
@@ -1686,7 +1757,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1686
1757
|
version.Content = contentJson;
|
|
1687
1758
|
version.UserID = contextUser.ID;
|
|
1688
1759
|
if (!(await version.Save())) {
|
|
1689
|
-
const message = `SaveSessionChannelArtifact: artifact version save failed
|
|
1760
|
+
const message = `SaveSessionChannelArtifact: artifact version save failed for session ${session.ID} (artifact ${artifact.ID}): ${this.describeSaveFailure(version)}`;
|
|
1690
1761
|
LogError(message);
|
|
1691
1762
|
return { Success: false, ErrorMessage: message, ArtifactID: artifact.ID, ConversationDetailLinked: false };
|
|
1692
1763
|
}
|
|
@@ -1736,21 +1807,29 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1736
1807
|
* queries (which scan junctions across ALL details) still surface the artifact. This is the
|
|
1737
1808
|
* least-invasive correct anchor — no fake visible message, no orphaned artifact.
|
|
1738
1809
|
*
|
|
1810
|
+
* TWO PRINCIPALS, deliberately: the lookup (`findLatestSessionDetailID`) and the junction row
|
|
1811
|
+
* itself (`saveDetailArtifactJunction`, `MJ: Conversation Detail Artifacts`) run as `runUser`
|
|
1812
|
+
* (elevated for a scoped-anonymous caller) — that junction entity is outside an anonymous
|
|
1813
|
+
* caller's relay grants. The hidden ANCHOR `Conversation Detail` this may need to create runs as
|
|
1814
|
+
* `callerUser` instead: like every other `MJ: Conversation Details` write, the System user is
|
|
1815
|
+
* refused because it is not the conversation's owner (see {@link persistDirectActionTurn},
|
|
1816
|
+
* issue #4791) — only the actual caller can create it.
|
|
1817
|
+
*
|
|
1739
1818
|
* Strictly best-effort: every failure path logs and returns; a relayed tool call NEVER fails
|
|
1740
1819
|
* because history linking did.
|
|
1741
1820
|
*/
|
|
1742
|
-
async linkDelegatedArtifactsToConversation(session, artifacts,
|
|
1821
|
+
async linkDelegatedArtifactsToConversation(session, artifacts, runUser, callerUser, provider) {
|
|
1743
1822
|
if (!artifacts || artifacts.length === 0 || !session.ConversationID) {
|
|
1744
1823
|
return;
|
|
1745
1824
|
}
|
|
1746
1825
|
try {
|
|
1747
|
-
const detailID = (await this.findLatestSessionDetailID(session,
|
|
1748
|
-
(await this.createHiddenSessionAnchorDetail(session,
|
|
1826
|
+
const detailID = (await this.findLatestSessionDetailID(session, runUser, provider)) ??
|
|
1827
|
+
(await this.createHiddenSessionAnchorDetail(session, callerUser, provider));
|
|
1749
1828
|
if (!detailID) {
|
|
1750
1829
|
return; // anchor unavailable — logged in the helpers
|
|
1751
1830
|
}
|
|
1752
1831
|
for (const artifact of artifacts) {
|
|
1753
|
-
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID,
|
|
1832
|
+
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, runUser, provider);
|
|
1754
1833
|
}
|
|
1755
1834
|
}
|
|
1756
1835
|
catch (error) {
|
|
@@ -1781,22 +1860,22 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1781
1860
|
* {@link linkDelegatedArtifactsToConversation}). Returns the new detail's id, or `null`
|
|
1782
1861
|
* (logged) when the save fails.
|
|
1783
1862
|
*/
|
|
1784
|
-
async createHiddenSessionAnchorDetail(session,
|
|
1785
|
-
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY,
|
|
1863
|
+
async createHiddenSessionAnchorDetail(session, callerUser, provider) {
|
|
1864
|
+
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, callerUser);
|
|
1786
1865
|
detail.NewRecord();
|
|
1787
1866
|
detail.ConversationID = session.ConversationID;
|
|
1788
1867
|
detail.Role = 'AI';
|
|
1789
1868
|
detail.HiddenToUser = true;
|
|
1790
1869
|
detail.Message = 'Artifacts produced during a realtime session (system anchor).';
|
|
1791
1870
|
detail.AgentSessionID = session.ID;
|
|
1792
|
-
// Attribute
|
|
1793
|
-
//
|
|
1871
|
+
// Attribute to the SESSION owner — the caller writing this row, whose ownership
|
|
1872
|
+
// `loadOwnedActiveSession` already proved.
|
|
1794
1873
|
detail.UserID = session.UserID;
|
|
1795
1874
|
if (await detail.Save()) {
|
|
1796
1875
|
return detail.ID;
|
|
1797
1876
|
}
|
|
1798
1877
|
LogError(`ExecuteRealtimeSessionTool: hidden anchor detail save failed for session ${session.ID}: ` +
|
|
1799
|
-
`${detail
|
|
1878
|
+
`${this.describeSaveFailure(detail)}`);
|
|
1800
1879
|
return null;
|
|
1801
1880
|
}
|
|
1802
1881
|
/**
|
|
@@ -1812,7 +1891,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1812
1891
|
const saved = await junction.Save();
|
|
1813
1892
|
if (!saved) {
|
|
1814
1893
|
LogError(`RealtimeClientSessionResolver: artifact junction save failed for detail ${conversationDetailID} / version ${artifactVersionID}: ` +
|
|
1815
|
-
`${junction
|
|
1894
|
+
`${this.describeSaveFailure(junction)}`);
|
|
1816
1895
|
}
|
|
1817
1896
|
return saved;
|
|
1818
1897
|
}
|
|
@@ -1841,7 +1920,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1841
1920
|
row.LastActiveAt = new Date();
|
|
1842
1921
|
if (!(await row.Save())) {
|
|
1843
1922
|
LogError(`SaveSessionChannelArtifact: LastActiveAt stamp failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
1844
|
-
`${row
|
|
1923
|
+
`${this.describeSaveFailure(row)}`);
|
|
1845
1924
|
}
|
|
1846
1925
|
}
|
|
1847
1926
|
catch (error) {
|
|
@@ -1922,9 +2001,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1922
2001
|
tool.ParametersSchema !== null && typeof tool.ParametersSchema === 'object' && !Array.isArray(tool.ParametersSchema));
|
|
1923
2002
|
}
|
|
1924
2003
|
/**
|
|
1925
|
-
* Parses the session's persisted config, returning
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
2004
|
+
* Parses the session's persisted config, returning its `targetAgentID` plus any observability run
|
|
2005
|
+
* ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed or carries no
|
|
2006
|
+
* target — a relay cannot proceed without a target. The owner can edit the config, so callers
|
|
2007
|
+
* re-check what they act on.
|
|
1928
2008
|
*/
|
|
1929
2009
|
readSessionConfig(session) {
|
|
1930
2010
|
const raw = session.Config_;
|
|
@@ -1974,12 +2054,6 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1974
2054
|
const minutes = elevation?.widget.VoiceMaxSessionMinutes;
|
|
1975
2055
|
return minutes && minutes > 0 ? minutes * 60 : undefined;
|
|
1976
2056
|
}
|
|
1977
|
-
/**
|
|
1978
|
-
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
1979
|
-
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
1980
|
-
*
|
|
1981
|
-
* @returns The boolean save result (logs `CompleteMessage` on failure).
|
|
1982
|
-
*/
|
|
1983
2057
|
/**
|
|
1984
2058
|
* Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
|
|
1985
2059
|
* just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
|
|
@@ -2001,9 +2075,16 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2001
2075
|
session.RecordingStartedAt = startedAt;
|
|
2002
2076
|
session.RecordingMedia = 'Audio';
|
|
2003
2077
|
if (!(await session.Save())) {
|
|
2004
|
-
LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed
|
|
2078
|
+
LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
|
|
2005
2079
|
}
|
|
2006
2080
|
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
2083
|
+
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
2084
|
+
*
|
|
2085
|
+
* @returns The boolean save result. On failure it logs the session, the write user and the
|
|
2086
|
+
* reason from {@link describeSaveFailure}.
|
|
2087
|
+
*/
|
|
2007
2088
|
async persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs) {
|
|
2008
2089
|
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
|
|
2009
2090
|
detail.NewRecord();
|
|
@@ -2020,7 +2101,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2020
2101
|
}
|
|
2021
2102
|
const saved = await detail.Save();
|
|
2022
2103
|
if (!saved) {
|
|
2023
|
-
LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed
|
|
2104
|
+
LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed for session ${session.ID} (write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`);
|
|
2024
2105
|
}
|
|
2025
2106
|
return saved;
|
|
2026
2107
|
}
|
|
@@ -2051,8 +2132,13 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2051
2132
|
// ⚠️ ELEVATING INSTEAD WAS TRIED AND IS A TRAP. `ResolveScopedAnonymousRunUser` returns
|
|
2052
2133
|
// `UserCache.GetSystemUser()`, which on this deployment resolves to the unconfigured
|
|
2053
2134
|
// placeholder `not.set@nowhere.com` ("Configured provisioning user not found; falling back
|
|
2054
|
-
// to an Owner").
|
|
2055
|
-
//
|
|
2135
|
+
// to an Owner"). That user EXISTS — it is not a missing/invalid principal — and even carries
|
|
2136
|
+
// full CRUD on `MJ: Conversation Details`. It is refused anyway:
|
|
2137
|
+
// `MJConversationDetailEntityExtended`'s owner gate accepts only the conversation's OWNER or
|
|
2138
|
+
// an Edit/Owner Resource Permission grantee, and the System user is neither. Measured on
|
|
2139
|
+
// MJ#4791 (before the core-entities fix that now records the denial): `Save()` → `false`,
|
|
2140
|
+
// `LatestResult` null — indistinguishable from a permission denial, and just as silent. The
|
|
2141
|
+
// denial itself still happens; that fix only made it stop being silent.
|
|
2056
2142
|
const writeUser = contextUser;
|
|
2057
2143
|
const rv = RunView.FromMetadataProvider(provider);
|
|
2058
2144
|
const result = await rv.RunView({
|
|
@@ -2070,8 +2156,8 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2070
2156
|
//
|
|
2071
2157
|
// `ResultType: 'entity_object'` hands back hydrated entities, but they do not carry the
|
|
2072
2158
|
// context user the way `GetEntityObject(entity, user)` does — so `Save()` ran with no
|
|
2073
|
-
// principal and failed with an EMPTY message ("unknown error"), which is what
|
|
2074
|
-
//
|
|
2159
|
+
// principal and failed with an EMPTY message ("unknown error"), which is what an empty-message
|
|
2160
|
+
// failure here looked like before the re-load. The insert path beside this one has
|
|
2075
2161
|
// always used `GetEntityObject`; this now matches it, which is also why they now succeed
|
|
2076
2162
|
// and fail for the same reasons.
|
|
2077
2163
|
const previous = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, writeUser);
|
|
@@ -2097,7 +2183,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2097
2183
|
LogError(`RealtimeClientSessionResolver.replacePreviousTranscriptTurn save failed for session `
|
|
2098
2184
|
+ `${session.ID} — the ${mappedRole} turn keeps its PREVIOUS, shorter text and the `
|
|
2099
2185
|
+ `transcript now understates what was said: `
|
|
2100
|
-
+ `${
|
|
2186
|
+
+ `${this.describeSaveFailure(previous)}`);
|
|
2101
2187
|
}
|
|
2102
2188
|
return saved;
|
|
2103
2189
|
}
|
|
@@ -2114,14 +2200,30 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2114
2200
|
* Stored as stable facts (Role: 'AI', HiddenToUser: true, ExternalID: callId, CompletionTime: durationMs,
|
|
2115
2201
|
* Message: JSON-serialized tool facts) so that session review can reconstruct the action card dynamically
|
|
2116
2202
|
* on load (Option 1) without polluting user-facing speech chat bubbles.
|
|
2203
|
+
*
|
|
2204
|
+
* Writes as the CALLER, never the elevated run user (issue #4791). Owning the session
|
|
2205
|
+
* (`loadOwnedActiveSession`'s gate has already run on every path here) does NOT imply owning
|
|
2206
|
+
* the conversation: the conversation id was client-supplied at session start. What authorizes
|
|
2207
|
+
* this write is the save itself, evaluated for the caller — the conversation owner gate in
|
|
2208
|
+
* `MJConversationDetailEntityExtended`, plus the caller's own entity permission and Create RLS
|
|
2209
|
+
* on `MJ: Conversation Details` (for the magic-link role that RLS scopes by the invite's
|
|
2210
|
+
* `ScopeResourceID`, and the role belongs to a downstream app, not MJ core). That is why
|
|
2211
|
+
* writing as the caller is safe even though the conversation id originated client-side.
|
|
2212
|
+
* Elevating to the System user would bypass RLS entirely, and the owner gate refuses it anyway
|
|
2213
|
+
* because the System user is not the conversation owner. Measured on MJ#4791 (before the
|
|
2214
|
+
* core-entities fix that now records the denial): `Save()` returned `false` with a NULL `LatestResult` and an empty
|
|
2215
|
+
* `ResultHistory` — indistinguishable from any other silent failure. Elevating this write is
|
|
2216
|
+
* what silently dropped both hidden tool-execution turns of a real magic-link session; the
|
|
2217
|
+
* denial itself still happens today, only its silence was fixed separately.
|
|
2218
|
+
* `ResolveScopedAnonymousRunUser` elevates AI-**run**-entity writes only (prompt runs, agent
|
|
2219
|
+
* runs, the relayed dispatch itself) — never this one.
|
|
2117
2220
|
*/
|
|
2118
2221
|
async persistDirectActionTurn(session, callId, toolName, argsJson, resultJson, durationMs, contextUser, provider) {
|
|
2119
2222
|
if (!session.ConversationID) {
|
|
2120
2223
|
return false;
|
|
2121
2224
|
}
|
|
2122
2225
|
try {
|
|
2123
|
-
const
|
|
2124
|
-
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, writeUser);
|
|
2226
|
+
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
|
|
2125
2227
|
detail.NewRecord();
|
|
2126
2228
|
detail.ConversationID = session.ConversationID;
|
|
2127
2229
|
detail.Role = 'AI';
|
|
@@ -2159,13 +2261,13 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2159
2261
|
detail.Message = JSON.stringify(factPayload);
|
|
2160
2262
|
const saved = await detail.Save();
|
|
2161
2263
|
if (!saved) {
|
|
2162
|
-
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn save failed
|
|
2264
|
+
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn save failed for session ${session.ID} (tool '${toolName}', call ${callId ?? 'n/a'}, write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`);
|
|
2163
2265
|
}
|
|
2164
2266
|
return saved;
|
|
2165
2267
|
}
|
|
2166
2268
|
catch (error) {
|
|
2167
2269
|
const message = error instanceof Error ? error.message : String(error);
|
|
2168
|
-
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error: ${message}`);
|
|
2270
|
+
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error for session ${session.ID}: ${message}`);
|
|
2169
2271
|
return false;
|
|
2170
2272
|
}
|
|
2171
2273
|
}
|