@vellumai/assistant 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/ARCHITECTURE.md +40 -3
- package/README.md +43 -35
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -87
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +4 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -4
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +131 -8
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +62 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +841 -39
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +10 -2
- package/src/approvals/guardian-request-resolvers.ts +128 -9
- package/src/calls/call-constants.ts +21 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-domain.ts +28 -7
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +424 -12
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
- package/src/config/calls-schema.ts +24 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +5 -55
- package/src/daemon/handlers/config-inbox.ts +9 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/sessions.ts +48 -3
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/integrations.ts +1 -99
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +14 -1
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +22 -11
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +3 -0
- package/src/daemon/session-surfaces.ts +3 -2
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +16 -0
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +39 -1
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +16 -8
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +71 -1
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +0 -3
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +65 -12
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/invite-redemption-service.ts +8 -0
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/conversation-routes.ts +140 -52
- package/src/runtime/routes/events-routes.ts +20 -5
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-message-handler.ts +143 -2
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/tool-approval-handler.ts +242 -18
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -7,6 +7,7 @@ import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.
|
|
|
7
7
|
import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
|
|
8
8
|
import { getLogger } from '../util/logger.js';
|
|
9
9
|
import { deliverReplyViaCallback } from './channel-reply-delivery.js';
|
|
10
|
+
import { resolveRoutingStateFromRuntime } from './guardian-context-resolver.js';
|
|
10
11
|
import type { MessageProcessor } from './http-types.js';
|
|
11
12
|
|
|
12
13
|
const log = getLogger('runtime-http');
|
|
@@ -129,7 +130,9 @@ export async function sweepFailedEvents(
|
|
|
129
130
|
},
|
|
130
131
|
assistantId,
|
|
131
132
|
guardianContext,
|
|
132
|
-
isInteractive: guardianContext
|
|
133
|
+
isInteractive: guardianContext
|
|
134
|
+
? resolveRoutingStateFromRuntime(guardianContext).promptWaitingAllowed
|
|
135
|
+
: false,
|
|
133
136
|
},
|
|
134
137
|
sourceChannel,
|
|
135
138
|
sourceInterface,
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge trusted-contact confirmation_request events to guardian.question notifications.
|
|
3
|
+
*
|
|
4
|
+
* When a trusted-contact channel session creates a confirmation_request (tool approval),
|
|
5
|
+
* this helper emits a guardian.question notification signal and persists canonical
|
|
6
|
+
* delivery rows to guardian destinations (Telegram/SMS/Vellum), enabling the guardian
|
|
7
|
+
* to approve via callback/request-code path.
|
|
8
|
+
*
|
|
9
|
+
* Modeled after the tool-grant-request-helper pattern. Designed to be called from
|
|
10
|
+
* both the daemon event registrar (server.ts) and the HTTP hub publisher
|
|
11
|
+
* (conversation-routes.ts) — the two paths that create confirmation_request
|
|
12
|
+
* canonical records.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
16
|
+
import {
|
|
17
|
+
type CanonicalGuardianRequest,
|
|
18
|
+
createCanonicalGuardianDelivery,
|
|
19
|
+
} from '../memory/canonical-guardian-store.js';
|
|
20
|
+
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
21
|
+
import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
|
|
22
|
+
import { getLogger } from '../util/logger.js';
|
|
23
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
24
|
+
import { getGuardianBinding } from './channel-guardian-service.js';
|
|
25
|
+
|
|
26
|
+
const log = getLogger('confirmation-request-guardian-bridge');
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Types
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export interface BridgeConfirmationRequestParams {
|
|
33
|
+
/** The canonical guardian request already persisted for this confirmation_request. */
|
|
34
|
+
canonicalRequest: CanonicalGuardianRequest;
|
|
35
|
+
/** Guardian runtime context from the session. */
|
|
36
|
+
guardianContext: GuardianRuntimeContext;
|
|
37
|
+
/** Conversation ID where the confirmation_request was emitted. */
|
|
38
|
+
conversationId: string;
|
|
39
|
+
/** Tool name from the confirmation_request. */
|
|
40
|
+
toolName: string;
|
|
41
|
+
/** Logical assistant ID (defaults to 'self'). */
|
|
42
|
+
assistantId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BridgeConfirmationRequestResult =
|
|
46
|
+
| { bridged: true; signalId: string }
|
|
47
|
+
| { skipped: true; reason: 'not_trusted_contact' | 'no_guardian_binding' | 'missing_guardian_identity' | 'binding_identity_mismatch' };
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Helper
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Bridge a trusted-contact confirmation_request to a guardian.question notification.
|
|
55
|
+
*
|
|
56
|
+
* Only emits when the session belongs to a trusted-contact actor with a
|
|
57
|
+
* resolvable guardian binding. Guardian and unknown actors are skipped — guardians
|
|
58
|
+
* self-approve, and unknown actors are already fail-closed by the routing layer.
|
|
59
|
+
*
|
|
60
|
+
* Fire-and-forget safe: notification emission errors are logged but not propagated.
|
|
61
|
+
*/
|
|
62
|
+
export function bridgeConfirmationRequestToGuardian(
|
|
63
|
+
params: BridgeConfirmationRequestParams,
|
|
64
|
+
): BridgeConfirmationRequestResult {
|
|
65
|
+
const {
|
|
66
|
+
canonicalRequest,
|
|
67
|
+
guardianContext,
|
|
68
|
+
conversationId,
|
|
69
|
+
toolName,
|
|
70
|
+
assistantId = DAEMON_INTERNAL_ASSISTANT_ID,
|
|
71
|
+
} = params;
|
|
72
|
+
|
|
73
|
+
// Only bridge for trusted-contact sessions. Guardians self-approve and
|
|
74
|
+
// unknown actors are fail-closed by the routing layer.
|
|
75
|
+
if (guardianContext.trustClass !== 'trusted_contact') {
|
|
76
|
+
return { skipped: true, reason: 'not_trusted_contact' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!guardianContext.guardianExternalUserId) {
|
|
80
|
+
log.debug(
|
|
81
|
+
{ conversationId, sourceChannel: guardianContext.sourceChannel },
|
|
82
|
+
'Skipping guardian bridge: no guardian identity on trusted-contact context',
|
|
83
|
+
);
|
|
84
|
+
return { skipped: true, reason: 'missing_guardian_identity' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sourceChannel = guardianContext.sourceChannel;
|
|
88
|
+
const binding = getGuardianBinding(assistantId, sourceChannel);
|
|
89
|
+
if (!binding) {
|
|
90
|
+
log.debug(
|
|
91
|
+
{ sourceChannel, assistantId },
|
|
92
|
+
'No guardian binding for confirmation request bridge',
|
|
93
|
+
);
|
|
94
|
+
return { skipped: true, reason: 'no_guardian_binding' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate that the binding's guardian identity matches the canonical request's
|
|
98
|
+
// guardian identity. A mismatch can occur if a guardian rebind happens between
|
|
99
|
+
// message ingress and confirmation emission — sending the notification to the
|
|
100
|
+
// new binding would leak requester/tool metadata to the wrong recipient.
|
|
101
|
+
//
|
|
102
|
+
// Both sides are canonicalized before comparison because the canonical request
|
|
103
|
+
// value was normalized by resolveGuardianContext() while the binding stores the
|
|
104
|
+
// raw identity. On phone channels the same guardian can have format variance
|
|
105
|
+
// (e.g. "+1 555-123-4567" vs "+15551234567") that would cause a false mismatch.
|
|
106
|
+
const canonicalBindingId = canonicalizeInboundIdentity(sourceChannel, binding.guardianExternalUserId);
|
|
107
|
+
const canonicalRequestId = canonicalRequest.guardianExternalUserId
|
|
108
|
+
? canonicalizeInboundIdentity(sourceChannel, canonicalRequest.guardianExternalUserId)
|
|
109
|
+
: null;
|
|
110
|
+
if (
|
|
111
|
+
canonicalRequestId &&
|
|
112
|
+
canonicalBindingId !== canonicalRequestId
|
|
113
|
+
) {
|
|
114
|
+
log.warn(
|
|
115
|
+
{
|
|
116
|
+
sourceChannel,
|
|
117
|
+
assistantId,
|
|
118
|
+
bindingGuardianId: binding.guardianExternalUserId,
|
|
119
|
+
expectedGuardianId: canonicalRequest.guardianExternalUserId,
|
|
120
|
+
requestId: canonicalRequest.id,
|
|
121
|
+
},
|
|
122
|
+
'Guardian binding identity does not match canonical request guardian — skipping notification to prevent misrouting',
|
|
123
|
+
);
|
|
124
|
+
return { skipped: true, reason: 'binding_identity_mismatch' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const senderLabel = guardianContext.requesterIdentifier
|
|
128
|
+
|| guardianContext.requesterExternalUserId
|
|
129
|
+
|| 'unknown';
|
|
130
|
+
|
|
131
|
+
const questionText = `Tool approval request: ${toolName}`;
|
|
132
|
+
|
|
133
|
+
// Emit guardian.question notification so the guardian is alerted.
|
|
134
|
+
const signalPromise = emitNotificationSignal({
|
|
135
|
+
sourceEventName: 'guardian.question',
|
|
136
|
+
sourceChannel,
|
|
137
|
+
sourceSessionId: conversationId,
|
|
138
|
+
assistantId,
|
|
139
|
+
attentionHints: {
|
|
140
|
+
requiresAction: true,
|
|
141
|
+
urgency: 'high',
|
|
142
|
+
isAsyncBackground: false,
|
|
143
|
+
visibleInSourceNow: false,
|
|
144
|
+
},
|
|
145
|
+
contextPayload: {
|
|
146
|
+
requestKind: 'tool_approval' as const,
|
|
147
|
+
requestId: canonicalRequest.id,
|
|
148
|
+
requestCode: canonicalRequest.requestCode ?? canonicalRequest.id.slice(0, 6).toUpperCase(),
|
|
149
|
+
sourceChannel,
|
|
150
|
+
requesterExternalUserId: guardianContext.requesterExternalUserId,
|
|
151
|
+
requesterChatId: guardianContext.requesterChatId ?? null,
|
|
152
|
+
requesterIdentifier: senderLabel,
|
|
153
|
+
toolName,
|
|
154
|
+
questionText,
|
|
155
|
+
},
|
|
156
|
+
dedupeKey: `tc-confirmation-request:${canonicalRequest.id}`,
|
|
157
|
+
onThreadCreated: (info) => {
|
|
158
|
+
createCanonicalGuardianDelivery({
|
|
159
|
+
requestId: canonicalRequest.id,
|
|
160
|
+
destinationChannel: 'vellum',
|
|
161
|
+
destinationConversationId: info.conversationId,
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Record channel deliveries from the notification pipeline (fire-and-forget).
|
|
167
|
+
void signalPromise.then((signalResult) => {
|
|
168
|
+
for (const result of signalResult.deliveryResults) {
|
|
169
|
+
if (result.channel === 'vellum') continue; // handled in onThreadCreated
|
|
170
|
+
if (result.channel !== 'telegram' && result.channel !== 'sms') continue;
|
|
171
|
+
createCanonicalGuardianDelivery({
|
|
172
|
+
requestId: canonicalRequest.id,
|
|
173
|
+
destinationChannel: result.channel,
|
|
174
|
+
destinationChatId: result.destination.length > 0 ? result.destination : undefined,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}).catch((err) => {
|
|
178
|
+
log.warn({ err, requestId: canonicalRequest.id }, 'Failed to record channel deliveries for guardian bridge');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
log.info(
|
|
182
|
+
{
|
|
183
|
+
sourceChannel,
|
|
184
|
+
requesterExternalUserId: guardianContext.requesterExternalUserId,
|
|
185
|
+
toolName,
|
|
186
|
+
requestId: canonicalRequest.id,
|
|
187
|
+
requestCode: canonicalRequest.requestCode,
|
|
188
|
+
},
|
|
189
|
+
'Guardian notified of trusted-contact confirmation request',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Return the signal ID synchronously from the promise-producing call.
|
|
193
|
+
// The actual signal ID is not available until the promise resolves, but
|
|
194
|
+
// callers only need to know it was bridged — the ID is for diagnostics.
|
|
195
|
+
// We use the canonical request ID as a stable correlation key.
|
|
196
|
+
return { bridged: true, signalId: canonicalRequest.id };
|
|
197
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guardian action follow-up executor.
|
|
3
3
|
*
|
|
4
|
-
* After the conversation engine
|
|
4
|
+
* After the conversation engine classifies the guardian's reply as
|
|
5
5
|
* `call_back` or `message_back` and transitions the follow-up state to
|
|
6
6
|
* `dispatching`, this module executes the actual action:
|
|
7
7
|
*
|
|
@@ -62,6 +62,88 @@ export function resolveGuardianContext(input: ResolveGuardianContextInput): Guar
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Routing-state helper
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Routing state for a channel actor turn.
|
|
71
|
+
*
|
|
72
|
+
* Determines whether a turn should be treated as interactive (the caller
|
|
73
|
+
* can be kept waiting for a guardian to respond to an approval prompt) by
|
|
74
|
+
* combining trust class with guardian route resolvability.
|
|
75
|
+
*
|
|
76
|
+
* A guardian route is "resolvable" when a verified guardian binding exists
|
|
77
|
+
* for the channel — meaning there is a concrete destination to deliver
|
|
78
|
+
* approval notifications to. Without a resolvable guardian route, entering
|
|
79
|
+
* an interactive wait (up to 300s) is a dead-end: no guardian will ever
|
|
80
|
+
* see the prompt.
|
|
81
|
+
*/
|
|
82
|
+
export interface RoutingState {
|
|
83
|
+
/** Whether the actor's trust class alone permits interactive waits. */
|
|
84
|
+
canBeInteractive: boolean;
|
|
85
|
+
/** Whether a verified guardian destination exists for this channel. */
|
|
86
|
+
guardianRouteResolvable: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Whether the turn should actually enter an interactive prompt wait.
|
|
89
|
+
* True only when the actor can be interactive AND a guardian route is
|
|
90
|
+
* resolvable. This is the canonical decision used by processMessage.
|
|
91
|
+
*/
|
|
92
|
+
promptWaitingAllowed: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compute the routing state for a channel actor turn.
|
|
97
|
+
*
|
|
98
|
+
* Guardian actors are always interactive (they self-approve).
|
|
99
|
+
* Trusted contacts are only interactive when a guardian binding exists
|
|
100
|
+
* to receive approval notifications. Unknown actors are never interactive.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveRoutingState(ctx: GuardianContext): RoutingState {
|
|
103
|
+
const isGuardian = ctx.trustClass === 'guardian';
|
|
104
|
+
const isTrustedContact = ctx.trustClass === 'trusted_contact';
|
|
105
|
+
|
|
106
|
+
// Guardians self-approve — they are always interactive and route-resolvable.
|
|
107
|
+
if (isGuardian) {
|
|
108
|
+
return {
|
|
109
|
+
canBeInteractive: true,
|
|
110
|
+
guardianRouteResolvable: true,
|
|
111
|
+
promptWaitingAllowed: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Trusted contacts can be interactive only if a guardian destination
|
|
116
|
+
// exists. The guardian binding populates guardianExternalUserId during
|
|
117
|
+
// trust resolution; its presence means there is a verified guardian
|
|
118
|
+
// to route approval notifications to.
|
|
119
|
+
const guardianRouteResolvable = !!ctx.guardianExternalUserId;
|
|
120
|
+
if (isTrustedContact) {
|
|
121
|
+
return {
|
|
122
|
+
canBeInteractive: true,
|
|
123
|
+
guardianRouteResolvable,
|
|
124
|
+
promptWaitingAllowed: guardianRouteResolvable,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Unknown actors are never interactive.
|
|
129
|
+
return {
|
|
130
|
+
canBeInteractive: false,
|
|
131
|
+
guardianRouteResolvable: !!ctx.guardianExternalUserId,
|
|
132
|
+
promptWaitingAllowed: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convenience: compute routing state from a GuardianRuntimeContext
|
|
138
|
+
* (the shape persisted in stored payloads and used by the retry sweep).
|
|
139
|
+
*/
|
|
140
|
+
export function resolveRoutingStateFromRuntime(ctx: GuardianRuntimeContext): RoutingState {
|
|
141
|
+
return resolveRoutingState({
|
|
142
|
+
trustClass: ctx.trustClass,
|
|
143
|
+
guardianExternalUserId: ctx.guardianExternalUserId,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
65
147
|
export function toGuardianRuntimeContext(sourceChannel: ChannelId, ctx: GuardianContext): GuardianRuntimeContext {
|
|
66
148
|
return {
|
|
67
149
|
sourceChannel,
|
|
@@ -94,7 +94,6 @@ function getTelegramBotUsername(): string | undefined {
|
|
|
94
94
|
|
|
95
95
|
export interface StartOutboundParams {
|
|
96
96
|
channel: ChannelId;
|
|
97
|
-
assistantId?: string;
|
|
98
97
|
destination?: string;
|
|
99
98
|
rebind?: boolean;
|
|
100
99
|
/** Origin conversation ID so completion/failure pointers can route back. */
|
|
@@ -103,14 +102,12 @@ export interface StartOutboundParams {
|
|
|
103
102
|
|
|
104
103
|
export interface ResendOutboundParams {
|
|
105
104
|
channel: ChannelId;
|
|
106
|
-
assistantId?: string;
|
|
107
105
|
/** Origin conversation ID so completion/failure pointers can route back on resend. */
|
|
108
106
|
originConversationId?: string;
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
export interface CancelOutboundParams {
|
|
112
110
|
channel: ChannelId;
|
|
113
|
-
assistantId?: string;
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
/**
|
|
@@ -21,13 +21,20 @@ import {
|
|
|
21
21
|
applyCanonicalGuardianDecision,
|
|
22
22
|
type CanonicalDecisionResult,
|
|
23
23
|
} from '../approvals/guardian-decision-primitive.js';
|
|
24
|
-
import type { ActorContext, ChannelDeliveryContext } from '../approvals/guardian-request-resolvers.js';
|
|
24
|
+
import type { ActorContext, ChannelDeliveryContext, ResolverEmissionContext } from '../approvals/guardian-request-resolvers.js';
|
|
25
25
|
import {
|
|
26
26
|
type CanonicalGuardianRequest,
|
|
27
27
|
getCanonicalGuardianRequest,
|
|
28
28
|
getCanonicalGuardianRequestByCode,
|
|
29
29
|
listCanonicalGuardianRequests,
|
|
30
30
|
} from '../memory/canonical-guardian-store.js';
|
|
31
|
+
import {
|
|
32
|
+
buildGuardianCodeOnlyClarification,
|
|
33
|
+
buildGuardianDisambiguationExample,
|
|
34
|
+
buildGuardianDisambiguationLabel,
|
|
35
|
+
buildGuardianInvalidActionReply,
|
|
36
|
+
resolveGuardianInstructionModeForRequest,
|
|
37
|
+
} from '../notifications/guardian-question-mode.js';
|
|
31
38
|
import { getLogger } from '../util/logger.js';
|
|
32
39
|
import { runApprovalConversationTurn } from './approval-conversation-turn.js';
|
|
33
40
|
import type { ApprovalAction } from './channel-approval-types.js';
|
|
@@ -60,6 +67,8 @@ export interface GuardianReplyContext {
|
|
|
60
67
|
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
61
68
|
/** Optional channel delivery context for resolver-driven side effects. */
|
|
62
69
|
channelDeliveryContext?: ChannelDeliveryContext;
|
|
70
|
+
/** Optional emission context threaded to handleConfirmationResponse for correct source attribution. */
|
|
71
|
+
emissionContext?: ResolverEmissionContext;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
export type GuardianReplyResultType =
|
|
@@ -235,9 +244,11 @@ function notConsumed(): GuardianReplyResult {
|
|
|
235
244
|
export async function routeGuardianReply(
|
|
236
245
|
ctx: GuardianReplyContext,
|
|
237
246
|
): Promise<GuardianReplyResult> {
|
|
238
|
-
const { messageText, actor, conversationId, callbackData, approvalConversationGenerator, channelDeliveryContext } = ctx;
|
|
247
|
+
const { messageText, actor, conversationId, callbackData, approvalConversationGenerator, channelDeliveryContext, emissionContext } = ctx;
|
|
239
248
|
const pendingRequests = findPendingCanonicalRequests(actor, ctx.pendingRequestIds, conversationId);
|
|
240
|
-
const scopedPendingRequestIds = ctx.pendingRequestIds
|
|
249
|
+
const scopedPendingRequestIds = ctx.pendingRequestIds && ctx.pendingRequestIds.length > 0
|
|
250
|
+
? new Set(ctx.pendingRequestIds)
|
|
251
|
+
: null;
|
|
241
252
|
|
|
242
253
|
// ── 1. Deterministic callback parsing (button presses) ──
|
|
243
254
|
// No conversationId scoping here — the guardian's reply comes from a
|
|
@@ -247,7 +258,7 @@ export async function routeGuardianReply(
|
|
|
247
258
|
if (callbackData) {
|
|
248
259
|
const parsed = parseCallbackAction(callbackData);
|
|
249
260
|
if (parsed) {
|
|
250
|
-
return applyDecision(parsed.requestId, parsed.action, actor, undefined, channelDeliveryContext);
|
|
261
|
+
return applyDecision(parsed.requestId, parsed.action, actor, undefined, channelDeliveryContext, emissionContext);
|
|
251
262
|
}
|
|
252
263
|
}
|
|
253
264
|
|
|
@@ -280,7 +291,7 @@ export async function routeGuardianReply(
|
|
|
280
291
|
consumed: true,
|
|
281
292
|
type: 'canonical_decision_stale',
|
|
282
293
|
requestId: request.id,
|
|
283
|
-
replyText: failureReplyText('already_resolved', request.requestCode),
|
|
294
|
+
replyText: failureReplyText('already_resolved', request.requestCode, request),
|
|
284
295
|
};
|
|
285
296
|
}
|
|
286
297
|
|
|
@@ -333,7 +344,7 @@ export async function routeGuardianReply(
|
|
|
333
344
|
// If the text indicates rejection, use reject; otherwise approve_once.
|
|
334
345
|
const action = inferActionFromText(codeResult.remainingText);
|
|
335
346
|
|
|
336
|
-
return applyDecision(request.id, action, actor, codeResult.remainingText, channelDeliveryContext);
|
|
347
|
+
return applyDecision(request.id, action, actor, codeResult.remainingText, channelDeliveryContext, emissionContext);
|
|
337
348
|
}
|
|
338
349
|
}
|
|
339
350
|
|
|
@@ -375,6 +386,7 @@ export async function routeGuardianReply(
|
|
|
375
386
|
actor,
|
|
376
387
|
messageText,
|
|
377
388
|
channelDeliveryContext,
|
|
389
|
+
emissionContext,
|
|
378
390
|
);
|
|
379
391
|
}
|
|
380
392
|
|
|
@@ -475,12 +487,13 @@ export async function routeGuardianReply(
|
|
|
475
487
|
};
|
|
476
488
|
}
|
|
477
489
|
|
|
478
|
-
const result = await applyDecision(targetId, decisionAction, actor, messageText, channelDeliveryContext);
|
|
490
|
+
const result = await applyDecision(targetId, decisionAction, actor, messageText, channelDeliveryContext, emissionContext);
|
|
479
491
|
|
|
480
492
|
// Attach the engine's reply text for stale/expired/identity-mismatch cases,
|
|
481
|
-
// but preserve
|
|
482
|
-
//
|
|
483
|
-
|
|
493
|
+
// but preserve resolver-authored replies (for example verification codes)
|
|
494
|
+
// and explicit resolver-failure text.
|
|
495
|
+
const hasResolverReplyText = Boolean(result.canonicalResult?.applied && result.canonicalResult.resolverReplyText);
|
|
496
|
+
if (engineResult.replyText && result.type !== 'canonical_resolver_failed' && !hasResolverReplyText) {
|
|
484
497
|
result.replyText = engineResult.replyText;
|
|
485
498
|
}
|
|
486
499
|
|
|
@@ -504,6 +517,7 @@ async function applyDecision(
|
|
|
504
517
|
actor: ActorContext,
|
|
505
518
|
userText?: string,
|
|
506
519
|
channelDeliveryContext?: ChannelDeliveryContext,
|
|
520
|
+
emissionContext?: ResolverEmissionContext,
|
|
507
521
|
): Promise<GuardianReplyResult> {
|
|
508
522
|
const canonicalResult = await applyCanonicalGuardianDecision({
|
|
509
523
|
requestId,
|
|
@@ -511,6 +525,7 @@ async function applyDecision(
|
|
|
511
525
|
actorContext: actor,
|
|
512
526
|
userText,
|
|
513
527
|
channelDeliveryContext,
|
|
528
|
+
emissionContext,
|
|
514
529
|
});
|
|
515
530
|
|
|
516
531
|
if (canonicalResult.applied) {
|
|
@@ -549,6 +564,7 @@ async function applyDecision(
|
|
|
549
564
|
decisionApplied: true,
|
|
550
565
|
consumed: true,
|
|
551
566
|
type: 'canonical_decision_applied',
|
|
567
|
+
...(canonicalResult.resolverReplyText ? { replyText: canonicalResult.resolverReplyText } : {}),
|
|
552
568
|
requestId,
|
|
553
569
|
canonicalResult,
|
|
554
570
|
};
|
|
@@ -570,13 +586,15 @@ async function applyDecision(
|
|
|
570
586
|
return notConsumed();
|
|
571
587
|
}
|
|
572
588
|
|
|
589
|
+
const request = getCanonicalGuardianRequest(requestId);
|
|
590
|
+
|
|
573
591
|
return {
|
|
574
592
|
decisionApplied: false,
|
|
575
593
|
consumed: true,
|
|
576
594
|
type: 'canonical_decision_stale',
|
|
577
595
|
requestId,
|
|
578
596
|
canonicalResult,
|
|
579
|
-
replyText: failureReplyText(canonicalResult.reason),
|
|
597
|
+
replyText: failureReplyText(canonicalResult.reason, request?.requestCode, request ?? undefined),
|
|
580
598
|
};
|
|
581
599
|
}
|
|
582
600
|
|
|
@@ -643,6 +661,12 @@ function inferActionFromText(text: string): ApprovalAction {
|
|
|
643
661
|
return 'approve_once';
|
|
644
662
|
}
|
|
645
663
|
|
|
664
|
+
function resolveRequestInstructionMode(
|
|
665
|
+
request?: Pick<CanonicalGuardianRequest, 'kind' | 'toolName'> | null,
|
|
666
|
+
): 'approval' | 'answer' {
|
|
667
|
+
return resolveGuardianInstructionModeForRequest(request);
|
|
668
|
+
}
|
|
669
|
+
|
|
646
670
|
// ---------------------------------------------------------------------------
|
|
647
671
|
// Failure reason reply text
|
|
648
672
|
// ---------------------------------------------------------------------------
|
|
@@ -653,7 +677,11 @@ type CanonicalFailureReason = 'already_resolved' | 'identity_mismatch' | 'invali
|
|
|
653
677
|
* Map a canonical decision failure reason to a distinct, actionable reply
|
|
654
678
|
* so the guardian understands exactly what happened and what to do next.
|
|
655
679
|
*/
|
|
656
|
-
function failureReplyText(
|
|
680
|
+
function failureReplyText(
|
|
681
|
+
reason: CanonicalFailureReason,
|
|
682
|
+
requestCode?: string | null,
|
|
683
|
+
request?: CanonicalGuardianRequest,
|
|
684
|
+
): string {
|
|
657
685
|
switch (reason) {
|
|
658
686
|
case 'already_resolved':
|
|
659
687
|
return 'This request has already been resolved.';
|
|
@@ -662,9 +690,7 @@ function failureReplyText(reason: CanonicalFailureReason, requestCode?: string |
|
|
|
662
690
|
case 'identity_mismatch':
|
|
663
691
|
return "You don't have permission to decide on this request.";
|
|
664
692
|
case 'invalid_action':
|
|
665
|
-
return requestCode
|
|
666
|
-
? `I found request ${requestCode}, but I need to know your decision. Reply "${requestCode} approve" or "${requestCode} reject".`
|
|
667
|
-
: "I couldn't determine your intended action. Reply with the request code followed by 'approve' or 'reject' (e.g., \"ABC123 approve\").";
|
|
693
|
+
return buildGuardianInvalidActionReply(resolveRequestInstructionMode(request), requestCode ?? undefined);
|
|
668
694
|
default:
|
|
669
695
|
return "I couldn't process that request. Please try again.";
|
|
670
696
|
}
|
|
@@ -681,15 +707,12 @@ function failureReplyText(reason: CanonicalFailureReason, requestCode?: string |
|
|
|
681
707
|
*/
|
|
682
708
|
function composeCodeOnlyClarification(request: CanonicalGuardianRequest): string {
|
|
683
709
|
const code = request.requestCode ?? 'unknown';
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
lines.push(`Reply "${code} approve" to approve or "${code} reject" to reject.`);
|
|
692
|
-
return lines.join('\n');
|
|
710
|
+
const mode = resolveRequestInstructionMode(request);
|
|
711
|
+
return buildGuardianCodeOnlyClarification(mode, {
|
|
712
|
+
requestCode: code,
|
|
713
|
+
questionText: request.questionText,
|
|
714
|
+
toolName: request.toolName,
|
|
715
|
+
});
|
|
693
716
|
}
|
|
694
717
|
|
|
695
718
|
// ---------------------------------------------------------------------------
|
|
@@ -706,6 +729,10 @@ function composeDisambiguationReply(
|
|
|
706
729
|
engineReplyText?: string,
|
|
707
730
|
): string {
|
|
708
731
|
const lines: string[] = [];
|
|
732
|
+
const requestsWithMode = pendingRequests.map((request) => ({
|
|
733
|
+
request,
|
|
734
|
+
mode: resolveRequestInstructionMode(request),
|
|
735
|
+
}));
|
|
709
736
|
|
|
710
737
|
if (engineReplyText) {
|
|
711
738
|
lines.push(engineReplyText);
|
|
@@ -714,16 +741,26 @@ function composeDisambiguationReply(
|
|
|
714
741
|
|
|
715
742
|
lines.push(`You have ${pendingRequests.length} pending requests. Please specify which one:`);
|
|
716
743
|
|
|
717
|
-
for (const
|
|
718
|
-
const toolLabel =
|
|
719
|
-
|
|
744
|
+
for (const { request, mode } of requestsWithMode) {
|
|
745
|
+
const toolLabel = buildGuardianDisambiguationLabel(mode, {
|
|
746
|
+
questionText: request.questionText,
|
|
747
|
+
toolName: request.toolName,
|
|
748
|
+
});
|
|
749
|
+
const code = request.requestCode ?? request.id.slice(0, 6).toUpperCase();
|
|
720
750
|
lines.push(` - ${code}: ${toolLabel}`);
|
|
721
751
|
}
|
|
722
752
|
|
|
723
|
-
|
|
724
|
-
const
|
|
753
|
+
const questionRequest = requestsWithMode.find(({ mode }) => mode === 'answer');
|
|
754
|
+
const decisionRequest = requestsWithMode.find(({ mode }) => mode === 'approval');
|
|
725
755
|
lines.push('');
|
|
726
|
-
|
|
756
|
+
if (questionRequest) {
|
|
757
|
+
const exampleCode = questionRequest.request.requestCode ?? questionRequest.request.id.slice(0, 6).toUpperCase();
|
|
758
|
+
lines.push(buildGuardianDisambiguationExample(questionRequest.mode, exampleCode));
|
|
759
|
+
}
|
|
760
|
+
if (decisionRequest) {
|
|
761
|
+
const exampleCode = decisionRequest.request.requestCode ?? decisionRequest.request.id.slice(0, 6).toUpperCase();
|
|
762
|
+
lines.push(buildGuardianDisambiguationExample(decisionRequest.mode, exampleCode));
|
|
763
|
+
}
|
|
727
764
|
|
|
728
765
|
return lines.join('\n');
|
|
729
766
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup migration: backfill channel='vellum' guardian binding.
|
|
3
|
+
*
|
|
4
|
+
* On runtime start, ensures that a guardian binding exists for the
|
|
5
|
+
* 'vellum' channel with a guardianPrincipalId. This is required for
|
|
6
|
+
* the identity-bound hatch bootstrap flow.
|
|
7
|
+
*
|
|
8
|
+
* - If a vellum binding already exists, no-op.
|
|
9
|
+
* - If no vellum binding exists, creates one with a fresh principal.
|
|
10
|
+
* - Preserves existing guardian bindings for other channels unchanged.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { v4 as uuid } from 'uuid';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createBinding,
|
|
17
|
+
getActiveBinding,
|
|
18
|
+
} from '../memory/guardian-bindings.js';
|
|
19
|
+
import { getLogger } from '../util/logger.js';
|
|
20
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
21
|
+
|
|
22
|
+
const log = getLogger('guardian-vellum-migration');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure a vellum guardian binding exists for the given assistant.
|
|
26
|
+
* Called during daemon startup to backfill existing installations.
|
|
27
|
+
*
|
|
28
|
+
* Returns the guardianPrincipalId (existing or newly created).
|
|
29
|
+
*/
|
|
30
|
+
export function ensureVellumGuardianBinding(assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): string {
|
|
31
|
+
const existing = getActiveBinding(assistantId, 'vellum');
|
|
32
|
+
if (existing) {
|
|
33
|
+
log.debug(
|
|
34
|
+
{ assistantId, guardianPrincipalId: existing.guardianExternalUserId },
|
|
35
|
+
'Vellum guardian binding already exists',
|
|
36
|
+
);
|
|
37
|
+
return existing.guardianExternalUserId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const guardianPrincipalId = `vellum-principal-${uuid()}`;
|
|
41
|
+
|
|
42
|
+
createBinding({
|
|
43
|
+
assistantId,
|
|
44
|
+
channel: 'vellum',
|
|
45
|
+
guardianExternalUserId: guardianPrincipalId,
|
|
46
|
+
guardianDeliveryChatId: 'local',
|
|
47
|
+
verifiedVia: 'startup-migration',
|
|
48
|
+
metadataJson: JSON.stringify({ migratedAt: Date.now() }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
log.info(
|
|
52
|
+
{ assistantId, guardianPrincipalId },
|
|
53
|
+
'Backfilled vellum guardian binding on startup',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return guardianPrincipalId;
|
|
57
|
+
}
|