@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
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* 5. Guardian approval record update
|
|
13
13
|
* 6. Scoped grant minting on approve
|
|
14
14
|
*
|
|
15
|
+
* The canonical path (`applyCanonicalGuardianDecision`) adds:
|
|
16
|
+
* 7. Canonical request lookup and status validation
|
|
17
|
+
* 8. CAS resolution via `resolveCanonicalGuardianRequest`
|
|
18
|
+
* 9. Kind-specific resolver dispatch via the resolver registry
|
|
19
|
+
*
|
|
15
20
|
* Security invariants enforced here:
|
|
16
21
|
* - Decision application is identity-bound to expected guardian identity
|
|
17
22
|
* - Decisions are first-response-wins (CAS-like stale protection)
|
|
@@ -20,11 +25,18 @@
|
|
|
20
25
|
*/
|
|
21
26
|
|
|
22
27
|
import type { ChannelId } from '../channels/types.js';
|
|
28
|
+
import {
|
|
29
|
+
type CanonicalGuardianRequest,
|
|
30
|
+
type CanonicalRequestStatus,
|
|
31
|
+
getCanonicalGuardianRequest,
|
|
32
|
+
resolveCanonicalGuardianRequest,
|
|
33
|
+
} from '../memory/canonical-guardian-store.js';
|
|
23
34
|
import {
|
|
24
35
|
type GuardianApprovalRequest,
|
|
25
36
|
updateApprovalDecision,
|
|
26
37
|
} from '../memory/channel-guardian-store.js';
|
|
27
38
|
import type {
|
|
39
|
+
ApprovalAction,
|
|
28
40
|
ApprovalDecisionResult,
|
|
29
41
|
} from '../runtime/channel-approval-types.js';
|
|
30
42
|
import {
|
|
@@ -36,6 +48,11 @@ import type { ApplyGuardianDecisionResult } from '../runtime/guardian-decision-t
|
|
|
36
48
|
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
37
49
|
import { getLogger } from '../util/logger.js';
|
|
38
50
|
import { mintGrantFromDecision } from './approval-primitive.js';
|
|
51
|
+
import {
|
|
52
|
+
type ActorContext,
|
|
53
|
+
type ChannelDeliveryContext,
|
|
54
|
+
getResolver,
|
|
55
|
+
} from './guardian-request-resolvers.js';
|
|
39
56
|
|
|
40
57
|
const log = getLogger('guardian-decision-primitive');
|
|
41
58
|
|
|
@@ -189,3 +206,271 @@ export function applyGuardianDecision(params: ApplyGuardianDecisionParams): Appl
|
|
|
189
206
|
requestId: result.requestId,
|
|
190
207
|
};
|
|
191
208
|
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Consolidated canonical grant minting
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Mint a scoped approval grant from a canonical guardian request.
|
|
216
|
+
*
|
|
217
|
+
* Works for all request kinds that carry tool metadata (toolName + inputDigest).
|
|
218
|
+
* Requests without tool metadata are silently skipped — grant minting only
|
|
219
|
+
* applies to tool-approval flows.
|
|
220
|
+
*
|
|
221
|
+
* Fails silently on error — grant minting is best-effort and must never
|
|
222
|
+
* block the approval flow.
|
|
223
|
+
*/
|
|
224
|
+
export function mintCanonicalRequestGrant(params: {
|
|
225
|
+
request: CanonicalGuardianRequest;
|
|
226
|
+
actorChannel: string;
|
|
227
|
+
guardianExternalUserId?: string;
|
|
228
|
+
}): { minted: boolean } {
|
|
229
|
+
const { request, actorChannel, guardianExternalUserId } = params;
|
|
230
|
+
|
|
231
|
+
if (!request.toolName || !request.inputDigest) {
|
|
232
|
+
return { minted: false };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result = mintGrantFromDecision({
|
|
236
|
+
assistantId: 'self',
|
|
237
|
+
scopeMode: 'tool_signature',
|
|
238
|
+
toolName: request.toolName,
|
|
239
|
+
inputDigest: request.inputDigest,
|
|
240
|
+
requestChannel: request.sourceChannel ?? 'unknown',
|
|
241
|
+
decisionChannel: actorChannel,
|
|
242
|
+
executionChannel: null,
|
|
243
|
+
conversationId: request.conversationId ?? null,
|
|
244
|
+
callSessionId: request.callSessionId ?? null,
|
|
245
|
+
guardianExternalUserId: guardianExternalUserId ?? null,
|
|
246
|
+
requesterExternalUserId: request.requesterExternalUserId ?? null,
|
|
247
|
+
expiresAt: new Date(Date.now() + GRANT_TTL_MS).toISOString(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (result.ok) {
|
|
251
|
+
log.info(
|
|
252
|
+
{
|
|
253
|
+
event: 'canonical_grant_minted',
|
|
254
|
+
requestId: request.id,
|
|
255
|
+
toolName: request.toolName,
|
|
256
|
+
conversationId: request.conversationId,
|
|
257
|
+
},
|
|
258
|
+
'Minted scoped approval grant for canonical guardian request',
|
|
259
|
+
);
|
|
260
|
+
return { minted: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
log.error(
|
|
264
|
+
{
|
|
265
|
+
event: 'canonical_grant_mint_failed',
|
|
266
|
+
reason: result.reason,
|
|
267
|
+
requestId: request.id,
|
|
268
|
+
toolName: request.toolName,
|
|
269
|
+
},
|
|
270
|
+
'Failed to mint scoped approval grant for canonical request (non-fatal)',
|
|
271
|
+
);
|
|
272
|
+
return { minted: false };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Canonical guardian decision primitive
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/** Valid actions for canonical guardian decisions. */
|
|
280
|
+
const VALID_CANONICAL_ACTIONS: ReadonlySet<ApprovalAction> = new Set([
|
|
281
|
+
'approve_once',
|
|
282
|
+
'approve_always',
|
|
283
|
+
'reject',
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
export interface ApplyCanonicalGuardianDecisionParams {
|
|
287
|
+
/** The canonical request ID to resolve. */
|
|
288
|
+
requestId: string;
|
|
289
|
+
/** The decision action. */
|
|
290
|
+
action: ApprovalAction;
|
|
291
|
+
/** Actor context for the entity making the decision. */
|
|
292
|
+
actorContext: ActorContext;
|
|
293
|
+
/** Optional user-supplied text (e.g. answer text for pending questions). */
|
|
294
|
+
userText?: string;
|
|
295
|
+
/** Optional channel delivery context — present when the decision arrived via a channel message. */
|
|
296
|
+
channelDeliveryContext?: ChannelDeliveryContext;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export type CanonicalDecisionResult =
|
|
300
|
+
| { applied: true; requestId: string; grantMinted: boolean; resolverFailed?: boolean; resolverFailureReason?: string }
|
|
301
|
+
| { applied: false; reason: 'not_found' | 'already_resolved' | 'identity_mismatch' | 'invalid_action' | 'expired'; detail?: string };
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Apply a guardian decision through the canonical request primitive.
|
|
305
|
+
*
|
|
306
|
+
* This is the future single write path for all guardian decisions. It
|
|
307
|
+
* operates on the canonical_guardian_requests table and dispatches to
|
|
308
|
+
* kind-specific resolvers via the resolver registry.
|
|
309
|
+
*
|
|
310
|
+
* Steps:
|
|
311
|
+
* 1. Look up the canonical request by ID
|
|
312
|
+
* 2. Validate: exists, pending status, identity match, valid action
|
|
313
|
+
* 3. Downgrade approve_always to approve_once (guardian-on-behalf invariant)
|
|
314
|
+
* 4. CAS resolve the canonical request atomically
|
|
315
|
+
* 5. Dispatch to kind-specific resolver
|
|
316
|
+
* 6. Mint grant if applicable
|
|
317
|
+
*/
|
|
318
|
+
export async function applyCanonicalGuardianDecision(
|
|
319
|
+
params: ApplyCanonicalGuardianDecisionParams,
|
|
320
|
+
): Promise<CanonicalDecisionResult> {
|
|
321
|
+
const { requestId, action, actorContext, userText, channelDeliveryContext } = params;
|
|
322
|
+
|
|
323
|
+
// 1. Look up the canonical request
|
|
324
|
+
const request = getCanonicalGuardianRequest(requestId);
|
|
325
|
+
if (!request) {
|
|
326
|
+
log.warn(
|
|
327
|
+
{ event: 'canonical_decision_not_found', requestId },
|
|
328
|
+
'Canonical request not found',
|
|
329
|
+
);
|
|
330
|
+
return { applied: false, reason: 'not_found' };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 2a. Validate status is pending
|
|
334
|
+
if (request.status !== 'pending') {
|
|
335
|
+
log.info(
|
|
336
|
+
{ event: 'canonical_decision_already_resolved', requestId, currentStatus: request.status },
|
|
337
|
+
'Canonical request already resolved',
|
|
338
|
+
);
|
|
339
|
+
return { applied: false, reason: 'already_resolved' };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 2b. Validate action is valid
|
|
343
|
+
if (!VALID_CANONICAL_ACTIONS.has(action)) {
|
|
344
|
+
log.warn(
|
|
345
|
+
{ event: 'canonical_decision_invalid_action', requestId, action },
|
|
346
|
+
'Invalid action for canonical decision',
|
|
347
|
+
);
|
|
348
|
+
return { applied: false, reason: 'invalid_action', detail: `invalid action: ${action}` };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2c. Validate identity: actor must match guardian_external_user_id
|
|
352
|
+
// unless the actor is trusted (desktop) or the request has no guardian binding.
|
|
353
|
+
if (
|
|
354
|
+
request.guardianExternalUserId &&
|
|
355
|
+
!actorContext.isTrusted &&
|
|
356
|
+
actorContext.externalUserId !== request.guardianExternalUserId
|
|
357
|
+
) {
|
|
358
|
+
log.warn(
|
|
359
|
+
{
|
|
360
|
+
event: 'canonical_decision_identity_mismatch',
|
|
361
|
+
requestId,
|
|
362
|
+
expectedGuardian: request.guardianExternalUserId,
|
|
363
|
+
actualActor: actorContext.externalUserId,
|
|
364
|
+
},
|
|
365
|
+
'Actor identity does not match expected guardian',
|
|
366
|
+
);
|
|
367
|
+
return { applied: false, reason: 'identity_mismatch' };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 2d. Check expiry
|
|
371
|
+
if (request.expiresAt && new Date(request.expiresAt).getTime() < Date.now()) {
|
|
372
|
+
log.info(
|
|
373
|
+
{ event: 'canonical_decision_expired', requestId, expiresAt: request.expiresAt },
|
|
374
|
+
'Canonical request has expired',
|
|
375
|
+
);
|
|
376
|
+
return { applied: false, reason: 'expired' };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 3. Downgrade approve_always to approve_once for guardian-on-behalf requests.
|
|
380
|
+
// Guardians cannot permanently allowlist tools on behalf of requesters.
|
|
381
|
+
const effectiveAction: ApprovalAction = action === 'approve_always'
|
|
382
|
+
? 'approve_once'
|
|
383
|
+
: action;
|
|
384
|
+
|
|
385
|
+
// 4. CAS resolve: atomically transition from 'pending' to terminal status
|
|
386
|
+
const targetStatus: CanonicalRequestStatus = effectiveAction === 'reject'
|
|
387
|
+
? 'denied'
|
|
388
|
+
: 'approved';
|
|
389
|
+
|
|
390
|
+
const resolved = resolveCanonicalGuardianRequest(requestId, 'pending', {
|
|
391
|
+
status: targetStatus,
|
|
392
|
+
answerText: userText,
|
|
393
|
+
decidedByExternalUserId: actorContext.externalUserId,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (!resolved) {
|
|
397
|
+
// CAS failed — someone else resolved it first
|
|
398
|
+
log.info(
|
|
399
|
+
{ event: 'canonical_decision_cas_failed', requestId },
|
|
400
|
+
'CAS resolution failed (race condition — first writer wins)',
|
|
401
|
+
);
|
|
402
|
+
return { applied: false, reason: 'already_resolved' };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 5. Dispatch to kind-specific resolver
|
|
406
|
+
let resolverFailed = false;
|
|
407
|
+
let resolverFailureReason: string | undefined;
|
|
408
|
+
const resolver = getResolver(request.kind);
|
|
409
|
+
if (resolver) {
|
|
410
|
+
const resolverResult = await resolver.resolve({
|
|
411
|
+
request: resolved,
|
|
412
|
+
decision: { action: effectiveAction, userText },
|
|
413
|
+
actor: actorContext,
|
|
414
|
+
channelDeliveryContext,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (!resolverResult.ok) {
|
|
418
|
+
log.warn(
|
|
419
|
+
{
|
|
420
|
+
event: 'canonical_decision_resolver_failed',
|
|
421
|
+
requestId,
|
|
422
|
+
kind: request.kind,
|
|
423
|
+
reason: resolverResult.reason,
|
|
424
|
+
},
|
|
425
|
+
`Resolver for kind '${request.kind}' failed: ${resolverResult.reason}`,
|
|
426
|
+
);
|
|
427
|
+
// The canonical request is already resolved (CAS succeeded), so we don't
|
|
428
|
+
// roll back. Flag the failure and fall through to grant minting so that
|
|
429
|
+
// callers see applied: true (reflecting the committed DB state) while
|
|
430
|
+
// still being informed that the resolver had an issue.
|
|
431
|
+
resolverFailed = true;
|
|
432
|
+
resolverFailureReason = resolverResult.reason;
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
log.info(
|
|
436
|
+
{ event: 'canonical_decision_no_resolver', requestId, kind: request.kind },
|
|
437
|
+
`No resolver registered for kind '${request.kind}' — CAS resolution only`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 6. Mint grant if the decision is an approval with tool metadata.
|
|
442
|
+
// Skip when the resolver failed — minting a grant on a failed side effect
|
|
443
|
+
// would allow the tool to execute without the intended resolver action
|
|
444
|
+
// (e.g. answerCall) having succeeded.
|
|
445
|
+
let grantMinted = false;
|
|
446
|
+
if (effectiveAction !== 'reject' && !resolverFailed) {
|
|
447
|
+
const grantResult = mintCanonicalRequestGrant({
|
|
448
|
+
request: resolved,
|
|
449
|
+
actorChannel: actorContext.channel,
|
|
450
|
+
guardianExternalUserId: actorContext.externalUserId ?? resolved.guardianExternalUserId ?? undefined,
|
|
451
|
+
});
|
|
452
|
+
grantMinted = grantResult.minted;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
log.info(
|
|
456
|
+
{
|
|
457
|
+
event: 'canonical_decision_applied',
|
|
458
|
+
requestId,
|
|
459
|
+
kind: request.kind,
|
|
460
|
+
action: effectiveAction,
|
|
461
|
+
targetStatus,
|
|
462
|
+
grantMinted,
|
|
463
|
+
resolverFailed,
|
|
464
|
+
},
|
|
465
|
+
resolverFailed
|
|
466
|
+
? 'Canonical guardian decision applied (CAS committed) but resolver failed'
|
|
467
|
+
: 'Canonical guardian decision applied successfully',
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
applied: true,
|
|
472
|
+
requestId,
|
|
473
|
+
grantMinted,
|
|
474
|
+
...(resolverFailed ? { resolverFailed, resolverFailureReason } : {}),
|
|
475
|
+
};
|
|
476
|
+
}
|