@vellumai/assistant 0.3.26 → 0.3.28
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/ARCHITECTURE.md +48 -1
- package/Dockerfile +2 -2
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +6 -2
- package/src/__tests__/agent-loop.test.ts +119 -0
- package/src/__tests__/bundled-asset.test.ts +107 -0
- package/src/__tests__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +19 -19
- package/src/__tests__/guardian-routing-invariants.test.ts +954 -0
- package/src/__tests__/mcp-cli.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +31 -29
- package/src/__tests__/notification-decision-fallback.test.ts +61 -3
- package/src/__tests__/notification-decision-strategy.test.ts +17 -0
- package/src/__tests__/notification-guardian-path.test.ts +13 -15
- package/src/__tests__/onboarding-template-contract.test.ts +116 -21
- package/src/__tests__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
- package/src/__tests__/session-runtime-assembly.test.ts +76 -47
- package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
- package/src/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +147 -2
- package/src/calls/types.ts +1 -1
- package/src/config/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +22 -17
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/lifecycle.ts +3 -16
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +32 -5
- package/src/daemon/session-process.ts +68 -307
- package/src/daemon/session-runtime-assembly.ts +112 -24
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +1 -0
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +524 -0
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/db-init.ts +16 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +52 -0
- package/src/notifications/copy-composer.ts +16 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +137 -0
- package/src/runtime/actor-trust-resolver.ts +225 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- package/src/runtime/guardian-context-resolver.ts +32 -7
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +687 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- package/src/runtime/routes/conversation-routes.ts +18 -0
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/inbound-message-handler.ts +170 -525
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +44 -1
- package/src/tools/types.ts +11 -0
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
|
@@ -93,6 +93,10 @@ import {
|
|
|
93
93
|
handleInstructionCall,
|
|
94
94
|
handleStartCall,
|
|
95
95
|
} from './routes/call-routes.js';
|
|
96
|
+
import {
|
|
97
|
+
startCanonicalGuardianExpirySweep,
|
|
98
|
+
stopCanonicalGuardianExpirySweep,
|
|
99
|
+
} from './routes/canonical-guardian-expiry-sweep.js';
|
|
96
100
|
import { canonicalChannelAssistantId } from './routes/channel-route-shared.js';
|
|
97
101
|
import {
|
|
98
102
|
handleChannelDeliveryAck,
|
|
@@ -341,6 +345,9 @@ export class RuntimeHttpServer {
|
|
|
341
345
|
startGuardianActionSweep(getGatewayInternalBaseUrl(), this.bearerToken, this.guardianActionCopyGenerator);
|
|
342
346
|
log.info('Guardian action expiry sweep started');
|
|
343
347
|
|
|
348
|
+
startCanonicalGuardianExpirySweep();
|
|
349
|
+
log.info('Canonical guardian request expiry sweep started');
|
|
350
|
+
|
|
344
351
|
log.info('Running in gateway-only ingress mode. Direct webhook routes disabled.');
|
|
345
352
|
if (!isLoopbackHost(this.hostname)) {
|
|
346
353
|
log.warn('RUNTIME_HTTP_HOST is not bound to loopback. This may expose the runtime to direct public access.');
|
|
@@ -361,6 +368,7 @@ export class RuntimeHttpServer {
|
|
|
361
368
|
this.pairingStore.stop();
|
|
362
369
|
stopGuardianExpirySweep();
|
|
363
370
|
stopGuardianActionSweep();
|
|
371
|
+
stopCanonicalGuardianExpirySweep();
|
|
364
372
|
if (this.retrySweepTimer) {
|
|
365
373
|
clearInterval(this.retrySweepTimer);
|
|
366
374
|
this.retrySweepTimer = null;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical guardian request expiry sweep.
|
|
3
|
+
*
|
|
4
|
+
* Periodically scans the `canonical_guardian_requests` table for pending
|
|
5
|
+
* requests whose `expiresAt` timestamp has passed and transitions them to
|
|
6
|
+
* the `expired` status. This ensures that stale requests are cleaned up
|
|
7
|
+
* even when no follow-up traffic arrives from either the guardian or the
|
|
8
|
+
* requester.
|
|
9
|
+
*
|
|
10
|
+
* Complements the existing sweeps:
|
|
11
|
+
* - `calls/guardian-action-sweep.ts` — voice call guardian action expiry
|
|
12
|
+
* - `runtime/routes/guardian-expiry-sweep.ts` — channel guardian approval expiry
|
|
13
|
+
*
|
|
14
|
+
* Unlike those sweeps, this one operates on the unified canonical domain
|
|
15
|
+
* (`canonical_guardian_requests`) and does not need to auto-deny pending
|
|
16
|
+
* interactions or deliver channel notices — the canonical request status
|
|
17
|
+
* transition is the single source of truth, and consumers (resolvers,
|
|
18
|
+
* clients polling prompts) observe the expired status directly.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
listCanonicalGuardianRequests,
|
|
23
|
+
resolveCanonicalGuardianRequest,
|
|
24
|
+
} from '../../memory/canonical-guardian-store.js';
|
|
25
|
+
import { getLogger } from '../../util/logger.js';
|
|
26
|
+
|
|
27
|
+
const log = getLogger('canonical-guardian-expiry-sweep');
|
|
28
|
+
|
|
29
|
+
/** Interval at which the expiry sweep runs (60 seconds). */
|
|
30
|
+
const SWEEP_INTERVAL_MS = 60_000;
|
|
31
|
+
|
|
32
|
+
/** Timer handle for the sweep so it can be stopped in tests and shutdown. */
|
|
33
|
+
let sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
34
|
+
|
|
35
|
+
/** Guard against overlapping sweeps. */
|
|
36
|
+
let sweepInProgress = false;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sweep all pending canonical guardian requests that have expired.
|
|
40
|
+
*
|
|
41
|
+
* Uses CAS resolution (`resolveCanonicalGuardianRequest`) so that a
|
|
42
|
+
* concurrent decision that wins the race is never overwritten by the
|
|
43
|
+
* sweep. Returns the count of requests transitioned to expired.
|
|
44
|
+
*/
|
|
45
|
+
export function sweepExpiredCanonicalGuardianRequests(): number {
|
|
46
|
+
const pending = listCanonicalGuardianRequests({ status: 'pending' });
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
let expiredCount = 0;
|
|
49
|
+
|
|
50
|
+
for (const request of pending) {
|
|
51
|
+
if (!request.expiresAt) continue;
|
|
52
|
+
|
|
53
|
+
const expiresAtMs = new Date(request.expiresAt).getTime();
|
|
54
|
+
if (expiresAtMs >= now) continue;
|
|
55
|
+
|
|
56
|
+
// CAS resolve: only transition from 'pending' to 'expired'.
|
|
57
|
+
// If someone resolved it between our read and this write, the CAS
|
|
58
|
+
// fails harmlessly (returns null) and we skip the request.
|
|
59
|
+
const resolved = resolveCanonicalGuardianRequest(request.id, 'pending', {
|
|
60
|
+
status: 'expired',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (resolved) {
|
|
64
|
+
expiredCount++;
|
|
65
|
+
log.info(
|
|
66
|
+
{
|
|
67
|
+
event: 'canonical_request_expired',
|
|
68
|
+
requestId: request.id,
|
|
69
|
+
kind: request.kind,
|
|
70
|
+
expiresAt: request.expiresAt,
|
|
71
|
+
},
|
|
72
|
+
'Expired canonical guardian request via sweep',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (expiredCount > 0) {
|
|
78
|
+
log.info(
|
|
79
|
+
{ event: 'canonical_expiry_sweep_complete', expiredCount },
|
|
80
|
+
`Canonical guardian expiry sweep: expired ${expiredCount} request(s)`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return expiredCount;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Start the periodic canonical guardian expiry sweep. Idempotent — calling
|
|
89
|
+
* it multiple times reuses the same timer.
|
|
90
|
+
*/
|
|
91
|
+
export function startCanonicalGuardianExpirySweep(): void {
|
|
92
|
+
if (sweepTimer) return;
|
|
93
|
+
sweepTimer = setInterval(() => {
|
|
94
|
+
if (sweepInProgress) return;
|
|
95
|
+
sweepInProgress = true;
|
|
96
|
+
try {
|
|
97
|
+
sweepExpiredCanonicalGuardianRequests();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
log.error({ err }, 'Canonical guardian expiry sweep failed');
|
|
100
|
+
} finally {
|
|
101
|
+
sweepInProgress = false;
|
|
102
|
+
}
|
|
103
|
+
}, SWEEP_INTERVAL_MS);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Stop the periodic canonical guardian expiry sweep. Used in tests and
|
|
108
|
+
* shutdown.
|
|
109
|
+
*/
|
|
110
|
+
export function stopCanonicalGuardianExpirySweep(): void {
|
|
111
|
+
if (sweepTimer) {
|
|
112
|
+
clearInterval(sweepTimer);
|
|
113
|
+
sweepTimer = null;
|
|
114
|
+
}
|
|
115
|
+
sweepInProgress = false;
|
|
116
|
+
}
|
|
@@ -8,6 +8,10 @@ import { CHANNEL_IDS, INTERFACE_IDS, parseChannelId, parseInterfaceId } from '..
|
|
|
8
8
|
import { mergeToolResults,renderHistoryContent } from '../../daemon/handlers.js';
|
|
9
9
|
import type { ServerMessage } from '../../daemon/ipc-protocol.js';
|
|
10
10
|
import * as attachmentsStore from '../../memory/attachments-store.js';
|
|
11
|
+
import {
|
|
12
|
+
createCanonicalGuardianRequest,
|
|
13
|
+
generateCanonicalRequestCode,
|
|
14
|
+
} from '../../memory/canonical-guardian-store.js';
|
|
11
15
|
import {
|
|
12
16
|
getConversationByKey,
|
|
13
17
|
getOrCreateConversation,
|
|
@@ -171,6 +175,20 @@ function makeHubPublisher(
|
|
|
171
175
|
persistentDecisionsAllowed: msg.persistentDecisionsAllowed,
|
|
172
176
|
},
|
|
173
177
|
});
|
|
178
|
+
|
|
179
|
+
// Create a canonical guardian request so IPC/HTTP handlers can find it
|
|
180
|
+
// via applyCanonicalGuardianDecision.
|
|
181
|
+
createCanonicalGuardianRequest({
|
|
182
|
+
id: msg.requestId,
|
|
183
|
+
kind: 'tool_approval',
|
|
184
|
+
sourceType: 'desktop',
|
|
185
|
+
sourceChannel: 'vellum',
|
|
186
|
+
conversationId,
|
|
187
|
+
toolName: msg.toolName,
|
|
188
|
+
status: 'pending',
|
|
189
|
+
requestCode: generateCanonicalRequestCode(),
|
|
190
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
|
|
191
|
+
});
|
|
174
192
|
} else if (msg.type === 'secret_request') {
|
|
175
193
|
pendingInteractions.register(msg.requestId, {
|
|
176
194
|
session,
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
* These endpoints let desktop clients fetch pending guardian prompts and
|
|
5
5
|
* submit button decisions without relying on text parsing.
|
|
6
6
|
*/
|
|
7
|
-
import { applyGuardianDecision } from '../../approvals/guardian-decision-primitive.js';
|
|
8
7
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
applyCanonicalGuardianDecision,
|
|
9
|
+
} from '../../approvals/guardian-decision-primitive.js';
|
|
10
|
+
import {
|
|
11
|
+
type CanonicalGuardianRequest,
|
|
12
|
+
getCanonicalGuardianRequest,
|
|
13
|
+
listCanonicalGuardianRequests,
|
|
14
|
+
} from '../../memory/canonical-guardian-store.js';
|
|
12
15
|
import type { ApprovalAction } from '../channel-approval-types.js';
|
|
13
|
-
import { handleChannelDecision } from '../channel-approvals.js';
|
|
14
16
|
import type { GuardianDecisionPrompt } from '../guardian-decision-types.js';
|
|
15
17
|
import { buildDecisionActions } from '../guardian-decision-types.js';
|
|
16
18
|
import { httpError } from '../http-errors.js';
|
|
17
|
-
import * as pendingInteractions from '../pending-interactions.js';
|
|
18
|
-
import { handleAccessRequestDecision } from './access-request-decision.js';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// GET /v1/guardian-actions/pending?conversationId=...
|
|
@@ -47,8 +47,9 @@ export function handleGuardianActionsPending(req: Request): Response {
|
|
|
47
47
|
/**
|
|
48
48
|
* Submit a guardian action decision.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* Routes all decisions through the unified canonical guardian decision
|
|
51
|
+
* primitive which handles CAS resolution, resolver dispatch, and grant
|
|
52
|
+
* minting.
|
|
52
53
|
*/
|
|
53
54
|
export async function handleGuardianActionDecision(req: Request): Promise<Response> {
|
|
54
55
|
const body = await req.json() as {
|
|
@@ -72,65 +73,53 @@ export async function handleGuardianActionDecision(req: Request): Promise<Respon
|
|
|
72
73
|
return httpError('BAD_REQUEST', `Invalid action: ${action}. Must be one of: approve_once, approve_always, reject`, 400);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
if (conversationId && conversationId !==
|
|
80
|
-
return httpError('
|
|
76
|
+
// Verify conversationId scoping before applying the canonical decision.
|
|
77
|
+
// A caller must not be able to cross-resolve requests from a different conversation.
|
|
78
|
+
if (conversationId) {
|
|
79
|
+
const canonicalRequest = getCanonicalGuardianRequest(requestId);
|
|
80
|
+
if (canonicalRequest && canonicalRequest.conversationId && canonicalRequest.conversationId !== conversationId) {
|
|
81
|
+
return httpError('NOT_FOUND', 'No pending guardian action found for this requestId', 404);
|
|
81
82
|
}
|
|
83
|
+
}
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
const canonicalResult = await applyCanonicalGuardianDecision({
|
|
86
|
+
requestId,
|
|
87
|
+
action: action as ApprovalAction,
|
|
88
|
+
actorContext: {
|
|
89
|
+
externalUserId: undefined,
|
|
90
|
+
channel: 'vellum',
|
|
91
|
+
isTrusted: true,
|
|
92
|
+
},
|
|
93
|
+
userText: undefined,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (canonicalResult.applied) {
|
|
97
|
+
// When the CAS committed but the resolver failed, the side effect
|
|
98
|
+
// (e.g. minting a verification session) did not happen. From the
|
|
99
|
+
// caller's perspective the decision was not truly applied.
|
|
100
|
+
if (canonicalResult.resolverFailed) {
|
|
96
101
|
return Response.json({
|
|
97
|
-
applied:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
applied: false,
|
|
103
|
+
reason: 'resolver_failed',
|
|
104
|
+
resolverFailureReason: canonicalResult.resolverFailureReason,
|
|
105
|
+
requestId: canonicalResult.requestId,
|
|
101
106
|
});
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// falsifying audit records with an unverified guardian identity.
|
|
108
|
-
const result = applyGuardianDecision({
|
|
109
|
-
approval,
|
|
110
|
-
decision: { action: action as 'approve_once' | 'approve_always' | 'reject', source: 'plain_text', requestId },
|
|
111
|
-
actorExternalUserId: undefined,
|
|
112
|
-
actorChannel: 'vellum',
|
|
109
|
+
return Response.json({
|
|
110
|
+
applied: true,
|
|
111
|
+
requestId: canonicalResult.requestId,
|
|
113
112
|
});
|
|
114
|
-
return Response.json({ ...result, requestId: result.requestId ?? requestId });
|
|
115
113
|
}
|
|
116
114
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const result = handleChannelDecision(
|
|
127
|
-
interaction.conversationId,
|
|
128
|
-
{ action: action as ApprovalAction, source: 'plain_text', requestId },
|
|
129
|
-
);
|
|
130
|
-
return Response.json({ ...result, requestId: result.requestId ?? requestId });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return httpError('NOT_FOUND', 'No pending guardian action found for this requestId', 404);
|
|
115
|
+
// Return the reason for failure (stale, expired, not_found, etc.)
|
|
116
|
+
return canonicalResult.reason === 'not_found'
|
|
117
|
+
? httpError('NOT_FOUND', 'No pending guardian action found for this requestId', 404)
|
|
118
|
+
: Response.json({
|
|
119
|
+
applied: false,
|
|
120
|
+
reason: canonicalResult.reason,
|
|
121
|
+
requestId,
|
|
122
|
+
});
|
|
134
123
|
}
|
|
135
124
|
|
|
136
125
|
// ---------------------------------------------------------------------------
|
|
@@ -140,10 +129,9 @@ export async function handleGuardianActionDecision(req: Request): Promise<Respon
|
|
|
140
129
|
/**
|
|
141
130
|
* Build a list of GuardianDecisionPrompt objects for the given conversation.
|
|
142
131
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* structured button UIs.
|
|
132
|
+
* Reads exclusively from the canonical guardian requests store. All request
|
|
133
|
+
* kinds (tool_approval, pending_question, access_request, etc.) that have
|
|
134
|
+
* been created as canonical requests will appear here.
|
|
147
135
|
*/
|
|
148
136
|
export function listGuardianDecisionPrompts(params: {
|
|
149
137
|
conversationId: string;
|
|
@@ -151,56 +139,59 @@ export function listGuardianDecisionPrompts(params: {
|
|
|
151
139
|
const { conversationId } = params;
|
|
152
140
|
const prompts: GuardianDecisionPrompt[] = [];
|
|
153
141
|
|
|
154
|
-
|
|
155
|
-
const approvalRequests = listPendingApprovalRequests({
|
|
142
|
+
const canonicalRequests = listCanonicalGuardianRequests({
|
|
156
143
|
conversationId,
|
|
157
144
|
status: 'pending',
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
for (const approval of approvalRequests) {
|
|
161
|
-
const reqId = approval.requestId!;
|
|
162
|
-
prompts.push({
|
|
163
|
-
requestId: reqId,
|
|
164
|
-
requestCode: reqId.slice(0, 6).toUpperCase(),
|
|
165
|
-
state: 'pending',
|
|
166
|
-
questionText: approval.reason ?? `Approve tool: ${approval.toolName ?? 'unknown'}`,
|
|
167
|
-
toolName: approval.toolName ?? null,
|
|
168
|
-
actions: buildDecisionActions({ forGuardianOnBehalf: true }),
|
|
169
|
-
expiresAt: approval.expiresAt,
|
|
170
|
-
conversationId: approval.conversationId,
|
|
171
|
-
callSessionId: null,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
145
|
+
});
|
|
174
146
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// 3. Pending confirmation interactions (direct tool approval prompts)
|
|
183
|
-
const interactions = pendingInteractions.getByConversation(conversationId);
|
|
184
|
-
for (const interaction of interactions) {
|
|
185
|
-
if (interaction.kind !== 'confirmation' || !interaction.confirmationDetails) continue;
|
|
186
|
-
// Skip if already covered by a channel guardian approval above
|
|
187
|
-
if (prompts.some(p => p.requestId === interaction.requestId)) continue;
|
|
188
|
-
|
|
189
|
-
const details = interaction.confirmationDetails;
|
|
190
|
-
prompts.push({
|
|
191
|
-
requestId: interaction.requestId,
|
|
192
|
-
requestCode: interaction.requestId.slice(0, 6).toUpperCase(),
|
|
193
|
-
state: 'pending',
|
|
194
|
-
questionText: `Approve tool: ${details.toolName}`,
|
|
195
|
-
toolName: details.toolName,
|
|
196
|
-
actions: buildDecisionActions({
|
|
197
|
-
persistentDecisionsAllowed: details.persistentDecisionsAllowed,
|
|
198
|
-
}),
|
|
199
|
-
expiresAt: Date.now() + 300_000,
|
|
200
|
-
conversationId,
|
|
201
|
-
callSessionId: null,
|
|
202
|
-
});
|
|
147
|
+
for (const req of canonicalRequests) {
|
|
148
|
+
// Skip expired canonical requests
|
|
149
|
+
if (req.expiresAt && new Date(req.expiresAt).getTime() < Date.now()) continue;
|
|
150
|
+
|
|
151
|
+
const prompt = mapCanonicalRequestToPrompt(req, conversationId);
|
|
152
|
+
prompts.push(prompt);
|
|
203
153
|
}
|
|
204
154
|
|
|
205
155
|
return prompts;
|
|
206
156
|
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Canonical request -> prompt mapping
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Map a canonical guardian request to the client-facing prompt format.
|
|
164
|
+
*
|
|
165
|
+
* Generates an appropriate questionText based on the request kind, and
|
|
166
|
+
* determines which actions are available. Pending questions surface as
|
|
167
|
+
* informational prompts since they may require text input rather than
|
|
168
|
+
* simple approve/reject buttons.
|
|
169
|
+
*/
|
|
170
|
+
function mapCanonicalRequestToPrompt(
|
|
171
|
+
req: CanonicalGuardianRequest,
|
|
172
|
+
conversationId: string,
|
|
173
|
+
): GuardianDecisionPrompt {
|
|
174
|
+
const questionText = req.questionText
|
|
175
|
+
?? (req.toolName ? `Approve tool: ${req.toolName}` : `Guardian request: ${req.kind}`);
|
|
176
|
+
|
|
177
|
+
// pending_question requests are typically voice-originated and need
|
|
178
|
+
// approve/reject only (no approve_always — guardian-on-behalf invariant).
|
|
179
|
+
const actions = buildDecisionActions({ forGuardianOnBehalf: true });
|
|
180
|
+
|
|
181
|
+
const expiresAt = req.expiresAt
|
|
182
|
+
? new Date(req.expiresAt).getTime()
|
|
183
|
+
: Date.now() + 300_000;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
requestId: req.id,
|
|
187
|
+
requestCode: req.requestCode ?? req.id.slice(0, 6).toUpperCase(),
|
|
188
|
+
state: 'pending',
|
|
189
|
+
questionText,
|
|
190
|
+
toolName: req.toolName ?? null,
|
|
191
|
+
actions,
|
|
192
|
+
expiresAt,
|
|
193
|
+
conversationId: req.conversationId ?? conversationId,
|
|
194
|
+
callSessionId: req.callSessionId ?? null,
|
|
195
|
+
kind: req.kind,
|
|
196
|
+
};
|
|
197
|
+
}
|