@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
|
@@ -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,12 +21,19 @@
|
|
|
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, direct actions never come from it, and its
|
|
26
|
+
* run ids are used only when {@link SessionRunIDVerifier} finds them to be records of the session.
|
|
27
|
+
* - **Scope-limited callers** (anonymous and resource-scoped magic-link sessions, including public
|
|
28
|
+
* web-widget guests): no direct actions. Their scope exists only as row-level filters, which an
|
|
29
|
+
* action can read around, and a scoped anonymous caller's relay runs as the system user. They
|
|
30
|
+
* reach the target agent only through `invoke-target-agent`.
|
|
24
31
|
*
|
|
25
32
|
* @module @memberjunction/server
|
|
26
33
|
*/
|
|
27
34
|
import { Resolver, Mutation, Arg, Ctx, Int, Float, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
|
|
28
35
|
import { AppContext, UserPayload } from '../types.js';
|
|
29
|
-
import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView } from '@memberjunction/core';
|
|
36
|
+
import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView, BaseEntity } from '@memberjunction/core';
|
|
30
37
|
import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
|
|
31
38
|
import {
|
|
32
39
|
MJAIAgentEntity,
|
|
@@ -48,6 +55,7 @@ import {
|
|
|
48
55
|
FindIgnoredRealtimeConfigKeys,
|
|
49
56
|
ParseRealtimeTypeConfiguration,
|
|
50
57
|
ResolveEffectiveRealtimeConfig,
|
|
58
|
+
INVOKE_TARGET_AGENT_TOOL_NAME,
|
|
51
59
|
RealtimeAllowedAgent,
|
|
52
60
|
RealtimeDirectActionsConfig,
|
|
53
61
|
REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION,
|
|
@@ -62,6 +70,8 @@ import { ResolverBase } from '../generic/ResolverBase.js';
|
|
|
62
70
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
63
71
|
import { GetReadWriteProvider } from '../util.js';
|
|
64
72
|
import { SessionManager } from '../agentSessions/index.js';
|
|
73
|
+
import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
|
|
74
|
+
import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
|
|
65
75
|
import { resolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
|
|
66
76
|
|
|
67
77
|
/**
|
|
@@ -137,9 +147,9 @@ const MAX_PRIOR_TRANSCRIPT_CHARS = 8_000;
|
|
|
137
147
|
const MAX_PRIOR_TRANSCRIPT_LEGS = 5;
|
|
138
148
|
|
|
139
149
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
150
|
+
* Shape persisted in `AIAgentSession.Config_` for a client-direct voice session. The server writes
|
|
151
|
+
* it, but the session owner can also edit it through the generic entity API, so relays re-check
|
|
152
|
+
* what they read from it (see the authorization model above).
|
|
143
153
|
*/
|
|
144
154
|
interface RealtimeSessionConfig {
|
|
145
155
|
/** The top-level target agent the co-agent voices on behalf of. */
|
|
@@ -375,6 +385,32 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
375
385
|
// state on — otherwise that cleanup silently no-ops on a freshly-constructed instance.
|
|
376
386
|
private readonly clientSessionService = new RealtimeClientSessionService();
|
|
377
387
|
private readonly sessionManager = new SessionManager(this.clientSessionService);
|
|
388
|
+
private readonly runIDVerifier = new SessionRunIDVerifier();
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
|
|
392
|
+
* `Message`, `Error` and every `Errors[]` entry (validation errors included, since `Save()`
|
|
393
|
+
* runs `Validate()` and records its errors there), so there is no separate `Validate()` call.
|
|
394
|
+
* When a subclass refused the write without recording anything, say so explicitly instead of
|
|
395
|
+
* "unknown error", which is what hid MJ#4791.
|
|
396
|
+
*
|
|
397
|
+
* Only a NULL `LatestResult` proves the refusal happened before the provider (nothing was
|
|
398
|
+
* registered at all). A SUCCESS `LatestResult` is an earlier save on the same instance — the
|
|
399
|
+
* refusal registered nothing — so its text must never be reported as this failure's reason.
|
|
400
|
+
*/
|
|
401
|
+
private describeSaveFailure(entity: BaseEntity): string {
|
|
402
|
+
const latest = entity.LatestResult;
|
|
403
|
+
if (!latest) {
|
|
404
|
+
return `no failure detail recorded (LatestResult null; ResultHistory length ${entity.ResultHistory.length}) ` +
|
|
405
|
+
'— the write was refused before reaching the provider';
|
|
406
|
+
}
|
|
407
|
+
if (latest.Success) {
|
|
408
|
+
return 'no failure detail recorded (latest result entry is a prior success; ' +
|
|
409
|
+
`ResultHistory length ${entity.ResultHistory.length})`;
|
|
410
|
+
}
|
|
411
|
+
const detail = latest.CompleteMessage?.trim();
|
|
412
|
+
return detail || `no failure detail recorded (LatestResult empty; ResultHistory length ${entity.ResultHistory.length})`;
|
|
413
|
+
}
|
|
378
414
|
|
|
379
415
|
/**
|
|
380
416
|
* Start a client-direct realtime voice session targeting `targetAgentId`.
|
|
@@ -386,7 +422,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
386
422
|
* agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
|
|
387
423
|
* see {@link RealtimeClientSessionResolver.resolveCoAgentID} for the full contract.
|
|
388
424
|
* 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
|
|
389
|
-
* config
|
|
425
|
+
* config — the target later relays use, re-authorized for the caller on each relay.
|
|
390
426
|
* 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
|
|
391
427
|
* On failure the just-created session is closed so no half-open session leaks.
|
|
392
428
|
*
|
|
@@ -501,8 +537,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
501
537
|
* Execute a single tool call the browser relayed from its provider socket and return the
|
|
502
538
|
* serialized result for the browser to relay back to the model.
|
|
503
539
|
*
|
|
504
|
-
* Ownership-gated
|
|
505
|
-
*
|
|
540
|
+
* Ownership-gated. The target agent id is read from the session config and `CanRun` on it is
|
|
541
|
+
* checked for the caller on every call, because the owner can edit the config. A direct-action
|
|
542
|
+
* call from a scope-limited caller is refused before any elevation. Heartbeats the session on
|
|
543
|
+
* success.
|
|
506
544
|
*
|
|
507
545
|
* @returns The serialized tool result JSON.
|
|
508
546
|
*/
|
|
@@ -518,13 +556,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
518
556
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
519
557
|
const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
|
|
520
558
|
const config = this.readSessionConfig(session);
|
|
559
|
+
await this.assertCanRunTarget(config.targetAgentID, contextUser, provider);
|
|
560
|
+
if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME && IsScopeLimitedPrincipal(contextUser)) {
|
|
561
|
+
return this.refuseDirectAction(agentSessionId, toolName);
|
|
562
|
+
}
|
|
521
563
|
|
|
522
564
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
|
|
523
565
|
// run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
|
|
524
566
|
// (the caller's role deliberately holds no grants on the run entities). The lead
|
|
525
|
-
// targetAgentID
|
|
526
|
-
//
|
|
567
|
+
// targetAgentID is CanRun-gated against the CALLER just above, and the colleague union
|
|
568
|
+
// just below, so elevation never widens agent authority.
|
|
527
569
|
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
570
|
+
const runIDs = await this.runIDVerifier.Verify(
|
|
571
|
+
session.ID,
|
|
572
|
+
{ CoAgentRunID: config.coAgentRunID, PendingFeedbackRunID: config.pendingFeedbackRunID },
|
|
573
|
+
runUser,
|
|
574
|
+
provider,
|
|
575
|
+
);
|
|
528
576
|
if (runUser !== contextUser) {
|
|
529
577
|
LogStatus(
|
|
530
578
|
`ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
|
|
@@ -542,16 +590,17 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
542
590
|
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
543
591
|
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
544
592
|
AllowedAgents: await this.filterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
|
|
545
|
-
DirectActions:
|
|
593
|
+
// No DirectActions: the service takes them from the session's start-time projection
|
|
594
|
+
// or the target agent's metadata, never from the owner-editable config.
|
|
546
595
|
// Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
|
|
547
596
|
// row and its context-memory scope must stay the person's, not the system user's.
|
|
548
597
|
AttributionUserID: contextUser.ID,
|
|
549
598
|
// Nest the delegated target-agent run under the co-agent observability run (when present).
|
|
550
|
-
ParentRunID:
|
|
599
|
+
ParentRunID: runIDs.CoAgentRunID,
|
|
551
600
|
Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
|
|
552
601
|
OnProgress: this.buildDelegationProgressCallback(pubSub, userPayload, agentSessionId, callId),
|
|
553
602
|
// Resume a previously-paused delegated run (if any) with the user's answer.
|
|
554
|
-
ResumeRunID:
|
|
603
|
+
ResumeRunID: runIDs.PendingFeedbackRunID,
|
|
555
604
|
},
|
|
556
605
|
runUser,
|
|
557
606
|
provider,
|
|
@@ -561,7 +610,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
561
610
|
`ExecuteRealtimeSessionTool: completed relayed tool '${toolName}' (callId: ${callId}) in ${durationMs}ms`,
|
|
562
611
|
);
|
|
563
612
|
|
|
564
|
-
if (toolName !==
|
|
613
|
+
if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME) {
|
|
565
614
|
await this.persistDirectActionTurn(
|
|
566
615
|
session,
|
|
567
616
|
callId,
|
|
@@ -581,7 +630,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
581
630
|
// Junction-link any artifacts the delegated run produced into the session's conversation
|
|
582
631
|
// history (best-effort) — so chat, session review, and resume carryover can all see them.
|
|
583
632
|
// Runs as `runUser`: the junction entity is not among an anonymous caller's relay grants.
|
|
584
|
-
|
|
633
|
+
// The hidden anchor detail it may need to create, though, is written by the CALLER (#4791)
|
|
634
|
+
// — same reason `persistDirectActionTurn` is: the System user is not the conversation owner.
|
|
635
|
+
await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, contextUser, provider);
|
|
585
636
|
|
|
586
637
|
await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
|
|
587
638
|
return ResultJson;
|
|
@@ -616,6 +667,19 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
616
667
|
};
|
|
617
668
|
}
|
|
618
669
|
|
|
670
|
+
/**
|
|
671
|
+
* The tool result for a direct-action call from a scope-limited caller (see the authorization
|
|
672
|
+
* model above). It has the broker's result shape, so the model hears that the tool is not
|
|
673
|
+
* available and can ask the target agent through `invoke-target-agent` instead.
|
|
674
|
+
*/
|
|
675
|
+
private refuseDirectAction(agentSessionId: string, toolName: string): string {
|
|
676
|
+
LogStatus(`ExecuteRealtimeSessionTool: refused direct action '${toolName}' for a scope-limited caller on session ${agentSessionId}.`);
|
|
677
|
+
return JSON.stringify({
|
|
678
|
+
success: false,
|
|
679
|
+
output: `The tool '${toolName}' is not available in this session. Ask the agent to do it with ${INVOKE_TARGET_AGENT_TOOL_NAME}.`,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
619
683
|
/**
|
|
620
684
|
* Rolls the session's `pendingFeedbackRunID` forward after a relayed tool call. The id we just
|
|
621
685
|
* consumed (resumed or none) is dropped; a new paused run id is stored only when the run paused
|
|
@@ -645,7 +709,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
645
709
|
session.Config_ = JSON.stringify(next);
|
|
646
710
|
if (!(await session.Save())) {
|
|
647
711
|
LogError(
|
|
648
|
-
`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed
|
|
712
|
+
`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
649
713
|
);
|
|
650
714
|
}
|
|
651
715
|
}
|
|
@@ -686,14 +750,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
686
750
|
// conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
|
|
687
751
|
// The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
|
|
688
752
|
// Conversation Detail above deliberately stays on the caller.
|
|
689
|
-
const
|
|
753
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
754
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
690
755
|
if (promptRunID) {
|
|
691
756
|
await this.clientSessionService.AppendPromptRunMessage(
|
|
692
757
|
promptRunID,
|
|
693
758
|
this.mapTranscriptRoleToChatRole(role),
|
|
694
759
|
text,
|
|
695
760
|
replacesPrevious ?? false,
|
|
696
|
-
|
|
761
|
+
runUser,
|
|
697
762
|
provider,
|
|
698
763
|
);
|
|
699
764
|
}
|
|
@@ -891,7 +956,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
891
956
|
provider,
|
|
892
957
|
);
|
|
893
958
|
|
|
894
|
-
const
|
|
959
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
960
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
895
961
|
if (!promptRunID) {
|
|
896
962
|
return false;
|
|
897
963
|
}
|
|
@@ -900,7 +966,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
900
966
|
'assistant',
|
|
901
967
|
this.formatToolTurn(toolName, argsJson, resultJson),
|
|
902
968
|
false,
|
|
903
|
-
|
|
969
|
+
runUser,
|
|
904
970
|
provider,
|
|
905
971
|
);
|
|
906
972
|
}
|
|
@@ -992,16 +1058,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
992
1058
|
return true; // nothing to add — a no-op flush is a success, not a failure
|
|
993
1059
|
}
|
|
994
1060
|
|
|
995
|
-
|
|
1061
|
+
// Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
|
|
1062
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
1063
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
996
1064
|
if (!promptRunID) {
|
|
997
1065
|
return false; // no observability prompt run for this session — logged in the helper
|
|
998
1066
|
}
|
|
999
1067
|
// Delegate to the service so usage writes share the per-run serialization with transcript-message
|
|
1000
1068
|
// appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
|
|
1001
|
-
|
|
1002
|
-
return this.clientSessionService.AccumulatePromptRunUsage(
|
|
1003
|
-
promptRunID, inputDelta, outputDelta, ResolveScopedAnonymousRunUser(contextUser), provider,
|
|
1004
|
-
);
|
|
1069
|
+
return this.clientSessionService.AccumulatePromptRunUsage(promptRunID, inputDelta, outputDelta, runUser, provider);
|
|
1005
1070
|
}
|
|
1006
1071
|
|
|
1007
1072
|
/** Clamps a relayed token delta: negative / non-finite values become 0. */
|
|
@@ -1028,24 +1093,40 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1028
1093
|
|
|
1029
1094
|
/**
|
|
1030
1095
|
* Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
|
|
1031
|
-
* throw-on-missing-target semantics
|
|
1032
|
-
*
|
|
1096
|
+
* throw-on-missing-target semantics, and keeps it only when it is the prompt run of the
|
|
1097
|
+
* session's co-agent run (the owner can edit the config). Usage relay is best-effort, so a
|
|
1098
|
+
* missing/malformed config, an absent id, or an id of another session just logs and returns `null`.
|
|
1099
|
+
*
|
|
1100
|
+
* @param runUser The user the prompt-run write runs as; the check runs as the same user.
|
|
1033
1101
|
*/
|
|
1034
|
-
private readPromptRunID(
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1102
|
+
private async readPromptRunID(
|
|
1103
|
+
session: MJAIAgentSessionEntity,
|
|
1104
|
+
runUser: UserInfo,
|
|
1105
|
+
provider: IMetadataProvider,
|
|
1106
|
+
): Promise<string | null> {
|
|
1107
|
+
const config = this.tryReadSessionConfig(session);
|
|
1108
|
+
if (!config) {
|
|
1109
|
+
LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
if (!config.promptRunID) {
|
|
1040
1113
|
LogStatus(
|
|
1041
1114
|
`RelayRealtimeUsage: session ${session.ID} has no co-agent promptRunID (observability run ` +
|
|
1042
1115
|
'creation was skipped or failed) — usage delta dropped.',
|
|
1043
1116
|
);
|
|
1044
1117
|
return null;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1118
|
+
}
|
|
1119
|
+
const verified = await this.runIDVerifier.Verify(
|
|
1120
|
+
session.ID,
|
|
1121
|
+
{ CoAgentRunID: config.coAgentRunID, PromptRunID: config.promptRunID },
|
|
1122
|
+
runUser,
|
|
1123
|
+
provider,
|
|
1124
|
+
);
|
|
1125
|
+
if (!verified.PromptRunID) {
|
|
1126
|
+
LogError(`RelayRealtimeUsage: session ${session.ID} names a promptRunID that is not its co-agent prompt run — ignored.`);
|
|
1047
1127
|
return null;
|
|
1048
1128
|
}
|
|
1129
|
+
return verified.PromptRunID;
|
|
1049
1130
|
}
|
|
1050
1131
|
|
|
1051
1132
|
/**
|
|
@@ -1195,8 +1276,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1195
1276
|
}
|
|
1196
1277
|
|
|
1197
1278
|
/**
|
|
1198
|
-
* Authorization gate for
|
|
1199
|
-
*
|
|
1279
|
+
* Authorization gate for the **target** agent: the caller must be able to run it. Denial throws.
|
|
1280
|
+
* {@link RealtimeClientSessionResolver.StartRealtimeClientSession} runs it before any session is
|
|
1281
|
+
* created, and {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} runs it on every
|
|
1282
|
+
* relayed call, because the owner can edit the target in the session config.
|
|
1200
1283
|
*/
|
|
1201
1284
|
private async assertCanRunTarget(
|
|
1202
1285
|
targetAgentId: string,
|
|
@@ -1725,6 +1808,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1725
1808
|
// (allowed-agent union); the snapshot is injected into the system prompt at mint.
|
|
1726
1809
|
ApplicationID: applicationId,
|
|
1727
1810
|
AppContext: appContext,
|
|
1811
|
+
// Scope-limited callers get no direct actions (see the authorization model above), so
|
|
1812
|
+
// the model is not given tools that the relay refuses.
|
|
1813
|
+
ExcludeDirectActions: IsScopeLimitedPrincipal(contextUser),
|
|
1728
1814
|
},
|
|
1729
1815
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): the prepare creates the co-agent
|
|
1730
1816
|
// observability AIAgentRun/AIPromptRun/run-step, which a scoped anonymous caller's role
|
|
@@ -1766,9 +1852,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1766
1852
|
|
|
1767
1853
|
/**
|
|
1768
1854
|
* Writes the co-agent observability run ids into the session's `Config_` alongside the
|
|
1769
|
-
*
|
|
1770
|
-
* finalize the runs (and on relay to nest delegated runs).
|
|
1771
|
-
* not thrown — the voice session still proceeds,
|
|
1855
|
+
* `targetAgentID`, then saves the session. These ids are read back on close to
|
|
1856
|
+
* finalize the runs (and on relay to nest delegated runs). Relays never read the direct actions
|
|
1857
|
+
* from it. Best-effort: a save failure is logged, not thrown — the voice session still proceeds,
|
|
1858
|
+
* it just won't carry the run ids.
|
|
1772
1859
|
*/
|
|
1773
1860
|
private async persistObservabilityRunIDs(
|
|
1774
1861
|
session: MJAIAgentSessionEntity,
|
|
@@ -1797,7 +1884,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1797
1884
|
const saved = await session.Save();
|
|
1798
1885
|
if (!saved) {
|
|
1799
1886
|
LogError(
|
|
1800
|
-
`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed
|
|
1887
|
+
`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
1801
1888
|
);
|
|
1802
1889
|
}
|
|
1803
1890
|
}
|
|
@@ -1915,7 +2002,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1915
2002
|
if (!saved) {
|
|
1916
2003
|
LogError(
|
|
1917
2004
|
`SaveSessionChannelState: save failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
1918
|
-
`${row
|
|
2005
|
+
`${this.describeSaveFailure(row)}`,
|
|
1919
2006
|
);
|
|
1920
2007
|
}
|
|
1921
2008
|
return saved;
|
|
@@ -2200,7 +2287,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2200
2287
|
artifact.UserID = contextUser.ID;
|
|
2201
2288
|
artifact.Visibility = 'Always';
|
|
2202
2289
|
if (!(await artifact.Save())) {
|
|
2203
|
-
const message = `SaveSessionChannelArtifact: artifact save failed: ${artifact
|
|
2290
|
+
const message = `SaveSessionChannelArtifact: artifact save failed for session ${session.ID}: ${this.describeSaveFailure(artifact)}`;
|
|
2204
2291
|
LogError(message);
|
|
2205
2292
|
return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
|
|
2206
2293
|
}
|
|
@@ -2212,7 +2299,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2212
2299
|
version.Content = contentJson;
|
|
2213
2300
|
version.UserID = contextUser.ID;
|
|
2214
2301
|
if (!(await version.Save())) {
|
|
2215
|
-
const message = `SaveSessionChannelArtifact: artifact version save failed
|
|
2302
|
+
const message = `SaveSessionChannelArtifact: artifact version save failed for session ${session.ID} (artifact ${artifact.ID}): ${this.describeSaveFailure(version)}`;
|
|
2216
2303
|
LogError(message);
|
|
2217
2304
|
return { Success: false, ErrorMessage: message, ArtifactID: artifact.ID, ConversationDetailLinked: false };
|
|
2218
2305
|
}
|
|
@@ -2270,13 +2357,22 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2270
2357
|
* queries (which scan junctions across ALL details) still surface the artifact. This is the
|
|
2271
2358
|
* least-invasive correct anchor — no fake visible message, no orphaned artifact.
|
|
2272
2359
|
*
|
|
2360
|
+
* TWO PRINCIPALS, deliberately: the lookup (`findLatestSessionDetailID`) and the junction row
|
|
2361
|
+
* itself (`saveDetailArtifactJunction`, `MJ: Conversation Detail Artifacts`) run as `runUser`
|
|
2362
|
+
* (elevated for a scoped-anonymous caller) — that junction entity is outside an anonymous
|
|
2363
|
+
* caller's relay grants. The hidden ANCHOR `Conversation Detail` this may need to create runs as
|
|
2364
|
+
* `callerUser` instead: like every other `MJ: Conversation Details` write, the System user is
|
|
2365
|
+
* refused because it is not the conversation's owner (see {@link persistDirectActionTurn},
|
|
2366
|
+
* issue #4791) — only the actual caller can create it.
|
|
2367
|
+
*
|
|
2273
2368
|
* Strictly best-effort: every failure path logs and returns; a relayed tool call NEVER fails
|
|
2274
2369
|
* because history linking did.
|
|
2275
2370
|
*/
|
|
2276
2371
|
private async linkDelegatedArtifactsToConversation(
|
|
2277
2372
|
session: MJAIAgentSessionEntity,
|
|
2278
2373
|
artifacts: DelegatedRunArtifact[] | undefined,
|
|
2279
|
-
|
|
2374
|
+
runUser: UserInfo,
|
|
2375
|
+
callerUser: UserInfo,
|
|
2280
2376
|
provider: IMetadataProvider,
|
|
2281
2377
|
): Promise<void> {
|
|
2282
2378
|
if (!artifacts || artifacts.length === 0 || !session.ConversationID) {
|
|
@@ -2284,13 +2380,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2284
2380
|
}
|
|
2285
2381
|
try {
|
|
2286
2382
|
const detailID =
|
|
2287
|
-
(await this.findLatestSessionDetailID(session,
|
|
2288
|
-
(await this.createHiddenSessionAnchorDetail(session,
|
|
2383
|
+
(await this.findLatestSessionDetailID(session, runUser, provider)) ??
|
|
2384
|
+
(await this.createHiddenSessionAnchorDetail(session, callerUser, provider));
|
|
2289
2385
|
if (!detailID) {
|
|
2290
2386
|
return; // anchor unavailable — logged in the helpers
|
|
2291
2387
|
}
|
|
2292
2388
|
for (const artifact of artifacts) {
|
|
2293
|
-
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID,
|
|
2389
|
+
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, runUser, provider);
|
|
2294
2390
|
}
|
|
2295
2391
|
} catch (error) {
|
|
2296
2392
|
LogError(`ExecuteRealtimeSessionTool: delegated-artifact history link failed: ${(error as Error).message}`);
|
|
@@ -2331,25 +2427,25 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2331
2427
|
*/
|
|
2332
2428
|
private async createHiddenSessionAnchorDetail(
|
|
2333
2429
|
session: MJAIAgentSessionEntity,
|
|
2334
|
-
|
|
2430
|
+
callerUser: UserInfo,
|
|
2335
2431
|
provider: IMetadataProvider,
|
|
2336
2432
|
): Promise<string | null> {
|
|
2337
|
-
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY,
|
|
2433
|
+
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY, callerUser);
|
|
2338
2434
|
detail.NewRecord();
|
|
2339
2435
|
detail.ConversationID = session.ConversationID;
|
|
2340
2436
|
detail.Role = 'AI';
|
|
2341
2437
|
detail.HiddenToUser = true;
|
|
2342
2438
|
detail.Message = 'Artifacts produced during a realtime session (system anchor).';
|
|
2343
2439
|
detail.AgentSessionID = session.ID;
|
|
2344
|
-
// Attribute
|
|
2345
|
-
//
|
|
2440
|
+
// Attribute to the SESSION owner — the caller writing this row, whose ownership
|
|
2441
|
+
// `loadOwnedActiveSession` already proved.
|
|
2346
2442
|
detail.UserID = session.UserID;
|
|
2347
2443
|
if (await detail.Save()) {
|
|
2348
2444
|
return detail.ID;
|
|
2349
2445
|
}
|
|
2350
2446
|
LogError(
|
|
2351
2447
|
`ExecuteRealtimeSessionTool: hidden anchor detail save failed for session ${session.ID}: ` +
|
|
2352
|
-
`${detail
|
|
2448
|
+
`${this.describeSaveFailure(detail)}`,
|
|
2353
2449
|
);
|
|
2354
2450
|
return null;
|
|
2355
2451
|
}
|
|
@@ -2376,7 +2472,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2376
2472
|
if (!saved) {
|
|
2377
2473
|
LogError(
|
|
2378
2474
|
`RealtimeClientSessionResolver: artifact junction save failed for detail ${conversationDetailID} / version ${artifactVersionID}: ` +
|
|
2379
|
-
`${junction
|
|
2475
|
+
`${this.describeSaveFailure(junction)}`,
|
|
2380
2476
|
);
|
|
2381
2477
|
}
|
|
2382
2478
|
return saved;
|
|
@@ -2416,7 +2512,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2416
2512
|
if (!(await row.Save())) {
|
|
2417
2513
|
LogError(
|
|
2418
2514
|
`SaveSessionChannelArtifact: LastActiveAt stamp failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
2419
|
-
`${row
|
|
2515
|
+
`${this.describeSaveFailure(row)}`,
|
|
2420
2516
|
);
|
|
2421
2517
|
}
|
|
2422
2518
|
} catch (error) {
|
|
@@ -2501,9 +2597,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2501
2597
|
}
|
|
2502
2598
|
|
|
2503
2599
|
/**
|
|
2504
|
-
* Parses the session's persisted config, returning
|
|
2505
|
-
*
|
|
2506
|
-
*
|
|
2600
|
+
* Parses the session's persisted config, returning its `targetAgentID` plus any observability run
|
|
2601
|
+
* ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed or carries no
|
|
2602
|
+
* target — a relay cannot proceed without a target. The owner can edit the config, so callers
|
|
2603
|
+
* re-check what they act on.
|
|
2507
2604
|
*/
|
|
2508
2605
|
private readSessionConfig(session: MJAIAgentSessionEntity): RealtimeSessionConfig {
|
|
2509
2606
|
const raw = session.Config_;
|
|
@@ -2560,12 +2657,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2560
2657
|
return minutes && minutes > 0 ? minutes * 60 : undefined;
|
|
2561
2658
|
}
|
|
2562
2659
|
|
|
2563
|
-
/**
|
|
2564
|
-
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
2565
|
-
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
2566
|
-
*
|
|
2567
|
-
* @returns The boolean save result (logs `CompleteMessage` on failure).
|
|
2568
|
-
*/
|
|
2569
2660
|
/**
|
|
2570
2661
|
* Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
|
|
2571
2662
|
* just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
|
|
@@ -2592,11 +2683,18 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2592
2683
|
session.RecordingMedia = 'Audio';
|
|
2593
2684
|
if (!(await session.Save())) {
|
|
2594
2685
|
LogError(
|
|
2595
|
-
`RealtimeClientSessionResolver.stampRecordingStart save failed
|
|
2686
|
+
`RealtimeClientSessionResolver.stampRecordingStart save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
2596
2687
|
);
|
|
2597
2688
|
}
|
|
2598
2689
|
}
|
|
2599
2690
|
|
|
2691
|
+
/**
|
|
2692
|
+
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
2693
|
+
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
2694
|
+
*
|
|
2695
|
+
* @returns The boolean save result. On failure it logs the session, the write user and the
|
|
2696
|
+
* reason from {@link describeSaveFailure}.
|
|
2697
|
+
*/
|
|
2600
2698
|
private async persistTranscriptTurn(
|
|
2601
2699
|
session: MJAIAgentSessionEntity,
|
|
2602
2700
|
role: string,
|
|
@@ -2626,7 +2724,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2626
2724
|
const saved = await detail.Save();
|
|
2627
2725
|
if (!saved) {
|
|
2628
2726
|
LogError(
|
|
2629
|
-
`RealtimeClientSessionResolver.persistTranscriptTurn save failed
|
|
2727
|
+
`RealtimeClientSessionResolver.persistTranscriptTurn save failed for session ${session.ID} (write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`,
|
|
2630
2728
|
);
|
|
2631
2729
|
}
|
|
2632
2730
|
return saved;
|
|
@@ -2667,8 +2765,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2667
2765
|
// ⚠️ ELEVATING INSTEAD WAS TRIED AND IS A TRAP. `ResolveScopedAnonymousRunUser` returns
|
|
2668
2766
|
// `UserCache.GetSystemUser()`, which on this deployment resolves to the unconfigured
|
|
2669
2767
|
// placeholder `not.set@nowhere.com` ("Configured provisioning user not found; falling back
|
|
2670
|
-
// to an Owner").
|
|
2671
|
-
//
|
|
2768
|
+
// to an Owner"). That user EXISTS — it is not a missing/invalid principal — and even carries
|
|
2769
|
+
// full CRUD on `MJ: Conversation Details`. It is refused anyway:
|
|
2770
|
+
// `MJConversationDetailEntityExtended`'s owner gate accepts only the conversation's OWNER or
|
|
2771
|
+
// an Edit/Owner Resource Permission grantee, and the System user is neither. Measured on
|
|
2772
|
+
// MJ#4791 (before the core-entities fix that now records the denial): `Save()` → `false`,
|
|
2773
|
+
// `LatestResult` null — indistinguishable from a permission denial, and just as silent. The
|
|
2774
|
+
// denial itself still happens; that fix only made it stop being silent.
|
|
2672
2775
|
const writeUser = contextUser;
|
|
2673
2776
|
const rv = RunView.FromMetadataProvider(provider);
|
|
2674
2777
|
const result = await rv.RunView<MJConversationDetailEntity>(
|
|
@@ -2689,8 +2792,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2689
2792
|
//
|
|
2690
2793
|
// `ResultType: 'entity_object'` hands back hydrated entities, but they do not carry the
|
|
2691
2794
|
// context user the way `GetEntityObject(entity, user)` does — so `Save()` ran with no
|
|
2692
|
-
// principal and failed with an EMPTY message ("unknown error"), which is what
|
|
2693
|
-
//
|
|
2795
|
+
// principal and failed with an EMPTY message ("unknown error"), which is what an empty-message
|
|
2796
|
+
// failure here looked like before the re-load. The insert path beside this one has
|
|
2694
2797
|
// always used `GetEntityObject`; this now matches it, which is also why they now succeed
|
|
2695
2798
|
// and fail for the same reasons.
|
|
2696
2799
|
const previous = await provider.GetEntityObject<MJConversationDetailEntity>(
|
|
@@ -2722,7 +2825,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2722
2825
|
`RealtimeClientSessionResolver.replacePreviousTranscriptTurn save failed for session `
|
|
2723
2826
|
+ `${session.ID} — the ${mappedRole} turn keeps its PREVIOUS, shorter text and the `
|
|
2724
2827
|
+ `transcript now understates what was said: `
|
|
2725
|
-
+ `${
|
|
2828
|
+
+ `${this.describeSaveFailure(previous)}`,
|
|
2726
2829
|
);
|
|
2727
2830
|
}
|
|
2728
2831
|
return saved;
|
|
@@ -2742,6 +2845,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2742
2845
|
* Stored as stable facts (Role: 'AI', HiddenToUser: true, ExternalID: callId, CompletionTime: durationMs,
|
|
2743
2846
|
* Message: JSON-serialized tool facts) so that session review can reconstruct the action card dynamically
|
|
2744
2847
|
* on load (Option 1) without polluting user-facing speech chat bubbles.
|
|
2848
|
+
*
|
|
2849
|
+
* Writes as the CALLER, never the elevated run user (issue #4791). Owning the session
|
|
2850
|
+
* (`loadOwnedActiveSession`'s gate has already run on every path here) does NOT imply owning
|
|
2851
|
+
* the conversation: the conversation id was client-supplied at session start. What authorizes
|
|
2852
|
+
* this write is the save itself, evaluated for the caller — the conversation owner gate in
|
|
2853
|
+
* `MJConversationDetailEntityExtended`, plus the caller's own entity permission and Create RLS
|
|
2854
|
+
* on `MJ: Conversation Details` (for the magic-link role that RLS scopes by the invite's
|
|
2855
|
+
* `ScopeResourceID`, and the role belongs to a downstream app, not MJ core). That is why
|
|
2856
|
+
* writing as the caller is safe even though the conversation id originated client-side.
|
|
2857
|
+
* Elevating to the System user would bypass RLS entirely, and the owner gate refuses it anyway
|
|
2858
|
+
* because the System user is not the conversation owner. Measured on MJ#4791 (before the
|
|
2859
|
+
* core-entities fix that now records the denial): `Save()` returned `false` with a NULL `LatestResult` and an empty
|
|
2860
|
+
* `ResultHistory` — indistinguishable from any other silent failure. Elevating this write is
|
|
2861
|
+
* what silently dropped both hidden tool-execution turns of a real magic-link session; the
|
|
2862
|
+
* denial itself still happens today, only its silence was fixed separately.
|
|
2863
|
+
* `ResolveScopedAnonymousRunUser` elevates AI-**run**-entity writes only (prompt runs, agent
|
|
2864
|
+
* runs, the relayed dispatch itself) — never this one.
|
|
2745
2865
|
*/
|
|
2746
2866
|
private async persistDirectActionTurn(
|
|
2747
2867
|
session: MJAIAgentSessionEntity,
|
|
@@ -2757,10 +2877,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2757
2877
|
return false;
|
|
2758
2878
|
}
|
|
2759
2879
|
try {
|
|
2760
|
-
const writeUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
2761
2880
|
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(
|
|
2762
2881
|
CONVERSATION_DETAIL_ENTITY,
|
|
2763
|
-
|
|
2882
|
+
contextUser,
|
|
2764
2883
|
);
|
|
2765
2884
|
detail.NewRecord();
|
|
2766
2885
|
detail.ConversationID = session.ConversationID;
|
|
@@ -2802,13 +2921,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2802
2921
|
const saved = await detail.Save();
|
|
2803
2922
|
if (!saved) {
|
|
2804
2923
|
LogError(
|
|
2805
|
-
`RealtimeClientSessionResolver.persistDirectActionTurn save failed
|
|
2924
|
+
`RealtimeClientSessionResolver.persistDirectActionTurn save failed for session ${session.ID} (tool '${toolName}', call ${callId ?? 'n/a'}, write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`,
|
|
2806
2925
|
);
|
|
2807
2926
|
}
|
|
2808
2927
|
return saved;
|
|
2809
2928
|
} catch (error) {
|
|
2810
2929
|
const message = error instanceof Error ? error.message : String(error);
|
|
2811
|
-
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error: ${message}`);
|
|
2930
|
+
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error for session ${session.ID}: ${message}`);
|
|
2812
2931
|
return false;
|
|
2813
2932
|
}
|
|
2814
2933
|
}
|