@vellumai/assistant 0.3.3 → 0.3.5
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/Dockerfile +2 -0
- package/README.md +45 -18
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +391 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +397 -135
- package/src/__tests__/channel-approvals.test.ts +99 -3
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +261 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +636 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +480 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +85 -22
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +24 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +40 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +58 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +819 -22
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +114 -4
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +111 -504
- package/src/daemon/session-agent-loop.ts +10 -15
- package/src/daemon/session-runtime-assembly.ts +115 -44
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +19 -2
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1163 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +11 -1
- package/src/memory/media-store.ts +759 -0
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +36 -2
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +99 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +26 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +29 -7
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +65 -28
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +237 -103
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +43 -3
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +10 -2
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +43 -1
- package/src/util/retry.ts +4 -4
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
buildApprovalUIMetadata,
|
|
42
42
|
handleChannelDecision,
|
|
43
43
|
buildReminderPrompt,
|
|
44
|
+
buildGuardianApprovalPrompt,
|
|
44
45
|
channelSupportsRichApprovalUI,
|
|
45
46
|
} from '../runtime/channel-approvals.js';
|
|
46
47
|
import type { ApprovalDecisionResult, ChannelApprovalPrompt } from '../runtime/channel-approval-types.js';
|
|
@@ -298,6 +299,53 @@ describe('handleChannelDecision', () => {
|
|
|
298
299
|
expect(orchestrator.submitDecision).toHaveBeenCalledWith(run.id, 'deny');
|
|
299
300
|
});
|
|
300
301
|
|
|
302
|
+
test('uses decision.runId to target the matching pending run', () => {
|
|
303
|
+
ensureConversation('conv-1');
|
|
304
|
+
const olderRun = createRun('conv-1');
|
|
305
|
+
setRunConfirmation(olderRun.id, {
|
|
306
|
+
...sampleConfirmation,
|
|
307
|
+
toolName: 'shell',
|
|
308
|
+
toolUseId: 'req-older',
|
|
309
|
+
});
|
|
310
|
+
const newerRun = createRun('conv-1');
|
|
311
|
+
setRunConfirmation(newerRun.id, {
|
|
312
|
+
...sampleConfirmation,
|
|
313
|
+
toolName: 'browser',
|
|
314
|
+
toolUseId: 'req-newer',
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const orchestrator = makeMockOrchestrator();
|
|
318
|
+
const decision: ApprovalDecisionResult = {
|
|
319
|
+
action: 'approve_once',
|
|
320
|
+
source: 'telegram_button',
|
|
321
|
+
runId: newerRun.id,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const result = handleChannelDecision('conv-1', decision, orchestrator);
|
|
325
|
+
expect(result.applied).toBe(true);
|
|
326
|
+
expect(result.runId).toBe(newerRun.id);
|
|
327
|
+
expect(orchestrator.submitDecision).toHaveBeenCalledWith(newerRun.id, 'allow');
|
|
328
|
+
expect(orchestrator.submitDecision).not.toHaveBeenCalledWith(olderRun.id, 'allow');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('returns applied: false when decision.runId does not match a pending run', () => {
|
|
332
|
+
ensureConversation('conv-1');
|
|
333
|
+
const run = createRun('conv-1');
|
|
334
|
+
setRunConfirmation(run.id, sampleConfirmation);
|
|
335
|
+
|
|
336
|
+
const orchestrator = makeMockOrchestrator();
|
|
337
|
+
const decision: ApprovalDecisionResult = {
|
|
338
|
+
action: 'approve_once',
|
|
339
|
+
source: 'telegram_button',
|
|
340
|
+
runId: 'run-missing',
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const result = handleChannelDecision('conv-1', decision, orchestrator);
|
|
344
|
+
expect(result.applied).toBe(false);
|
|
345
|
+
expect(result.runId).toBeUndefined();
|
|
346
|
+
expect(orchestrator.submitDecision).not.toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
|
|
301
349
|
test('approve_always adds a trust rule and submits "allow"', () => {
|
|
302
350
|
ensureConversation('conv-1');
|
|
303
351
|
const run = createRun('conv-1');
|
|
@@ -319,14 +367,16 @@ describe('handleChannelDecision', () => {
|
|
|
319
367
|
expect(result.applied).toBe(true);
|
|
320
368
|
expect(result.runId).toBe(run.id);
|
|
321
369
|
|
|
322
|
-
// Trust rule added with first allowlist and scope option
|
|
370
|
+
// Trust rule added with first allowlist and scope option.
|
|
371
|
+
// executionTarget is undefined for core tools like 'shell' — only
|
|
372
|
+
// skill-origin tools persist it (see channel-approvals.ts).
|
|
323
373
|
expect(addRuleSpy).toHaveBeenCalledWith(
|
|
324
374
|
'shell',
|
|
325
375
|
'rm -rf *',
|
|
326
376
|
'/tmp/project',
|
|
327
377
|
'allow',
|
|
328
378
|
100,
|
|
329
|
-
{ executionTarget:
|
|
379
|
+
{ executionTarget: undefined },
|
|
330
380
|
);
|
|
331
381
|
|
|
332
382
|
// The run is still approved with a simple "allow"
|
|
@@ -498,7 +548,53 @@ describe('buildReminderPrompt', () => {
|
|
|
498
548
|
});
|
|
499
549
|
|
|
500
550
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
501
|
-
// 5.
|
|
551
|
+
// 5. buildGuardianApprovalPrompt
|
|
552
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
553
|
+
|
|
554
|
+
describe('buildGuardianApprovalPrompt', () => {
|
|
555
|
+
test('prompt includes requester identifier and tool name', () => {
|
|
556
|
+
const runInfo: PendingRunInfo = {
|
|
557
|
+
runId: 'run-g1',
|
|
558
|
+
requestId: 'req-g1',
|
|
559
|
+
toolName: 'deploy',
|
|
560
|
+
input: {},
|
|
561
|
+
riskLevel: 'high',
|
|
562
|
+
};
|
|
563
|
+
const prompt = buildGuardianApprovalPrompt(runInfo, 'alice');
|
|
564
|
+
expect(prompt.promptText).toContain('alice');
|
|
565
|
+
expect(prompt.promptText).toContain('deploy');
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test('excludes approve_always action', () => {
|
|
569
|
+
const runInfo: PendingRunInfo = {
|
|
570
|
+
runId: 'run-g2',
|
|
571
|
+
requestId: 'req-g2',
|
|
572
|
+
toolName: 'shell',
|
|
573
|
+
input: {},
|
|
574
|
+
riskLevel: 'medium',
|
|
575
|
+
};
|
|
576
|
+
const prompt = buildGuardianApprovalPrompt(runInfo, 'bob');
|
|
577
|
+
expect(prompt.actions.map((a) => a.id)).not.toContain('approve_always');
|
|
578
|
+
expect(prompt.actions.map((a) => a.id)).toContain('approve_once');
|
|
579
|
+
expect(prompt.actions.map((a) => a.id)).toContain('reject');
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('plainTextFallback contains parser-compatible keywords', () => {
|
|
583
|
+
const runInfo: PendingRunInfo = {
|
|
584
|
+
runId: 'run-g3',
|
|
585
|
+
requestId: 'req-g3',
|
|
586
|
+
toolName: 'write_file',
|
|
587
|
+
input: {},
|
|
588
|
+
riskLevel: 'high',
|
|
589
|
+
};
|
|
590
|
+
const prompt = buildGuardianApprovalPrompt(runInfo, 'charlie');
|
|
591
|
+
expect(prompt.plainTextFallback).toContain('yes');
|
|
592
|
+
expect(prompt.plainTextFallback).toContain('no');
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
597
|
+
// 6. channelSupportsRichApprovalUI
|
|
502
598
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
503
599
|
|
|
504
600
|
describe('channelSupportsRichApprovalUI', () => {
|
|
@@ -24,7 +24,7 @@ mock.module('../util/logger.js', () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
27
|
-
import { channelInboundEvents, conversations, messages } from '../memory/schema.js';
|
|
27
|
+
import { channelInboundEvents, conversations, externalConversationBindings, messages } from '../memory/schema.js';
|
|
28
28
|
import {
|
|
29
29
|
recordInbound,
|
|
30
30
|
linkMessage,
|
|
@@ -470,7 +470,7 @@ describe('channel-delivery-store', () => {
|
|
|
470
470
|
|
|
471
471
|
// ── handleDeleteConversation assistantId parameter ───────────────
|
|
472
472
|
|
|
473
|
-
test('handleDeleteConversation
|
|
473
|
+
test('handleDeleteConversation with non-self assistant deletes only scoped key', async () => {
|
|
474
474
|
// Set up a scoped conversation key like the one created by recordInbound
|
|
475
475
|
// with a specific assistantId.
|
|
476
476
|
const convId = 'conv-delete-test';
|
|
@@ -488,6 +488,13 @@ describe('channel-delivery-store', () => {
|
|
|
488
488
|
}).run();
|
|
489
489
|
setConversationKey(scopedKey, convId);
|
|
490
490
|
setConversationKey(legacyKey, convId);
|
|
491
|
+
db.insert(externalConversationBindings).values({
|
|
492
|
+
conversationId: convId,
|
|
493
|
+
sourceChannel: 'telegram',
|
|
494
|
+
externalChatId: 'chat-del',
|
|
495
|
+
createdAt: now,
|
|
496
|
+
updatedAt: now,
|
|
497
|
+
}).run();
|
|
491
498
|
|
|
492
499
|
// Verify both keys exist
|
|
493
500
|
expect(getConversationByKey(scopedKey)).not.toBeNull();
|
|
@@ -510,9 +517,15 @@ describe('channel-delivery-store', () => {
|
|
|
510
517
|
const json = await res.json() as { ok: boolean };
|
|
511
518
|
expect(json.ok).toBe(true);
|
|
512
519
|
|
|
513
|
-
//
|
|
520
|
+
// Non-self delete should only remove the scoped key and preserve legacy.
|
|
514
521
|
expect(getConversationByKey(scopedKey)).toBeNull();
|
|
515
|
-
expect(getConversationByKey(legacyKey)).toBeNull();
|
|
522
|
+
expect(getConversationByKey(legacyKey)).not.toBeNull();
|
|
523
|
+
// Non-self delete should not mutate assistant-agnostic external bindings.
|
|
524
|
+
const remainingBinding = db.select()
|
|
525
|
+
.from(externalConversationBindings)
|
|
526
|
+
.where(eq(externalConversationBindings.conversationId, convId))
|
|
527
|
+
.get();
|
|
528
|
+
expect(remainingBinding).not.toBeNull();
|
|
516
529
|
});
|
|
517
530
|
|
|
518
531
|
test('handleDeleteConversation defaults to "self" when no assistantId provided', async () => {
|
|
@@ -530,6 +543,13 @@ describe('channel-delivery-store', () => {
|
|
|
530
543
|
}).run();
|
|
531
544
|
setConversationKey(scopedKey, convId);
|
|
532
545
|
setConversationKey(legacyKey, convId);
|
|
546
|
+
db.insert(externalConversationBindings).values({
|
|
547
|
+
conversationId: convId,
|
|
548
|
+
sourceChannel: 'telegram',
|
|
549
|
+
externalChatId: 'chat-def',
|
|
550
|
+
createdAt: now,
|
|
551
|
+
updatedAt: now,
|
|
552
|
+
}).run();
|
|
533
553
|
|
|
534
554
|
const req = new Request('http://localhost/channels/conversation', {
|
|
535
555
|
method: 'DELETE',
|
|
@@ -546,5 +566,11 @@ describe('channel-delivery-store', () => {
|
|
|
546
566
|
|
|
547
567
|
expect(getConversationByKey(scopedKey)).toBeNull();
|
|
548
568
|
expect(getConversationByKey(legacyKey)).toBeNull();
|
|
569
|
+
// Self delete should keep external bindings in sync for the canonical route.
|
|
570
|
+
const remainingBinding = db.select()
|
|
571
|
+
.from(externalConversationBindings)
|
|
572
|
+
.where(eq(externalConversationBindings.conversationId, convId))
|
|
573
|
+
.get();
|
|
574
|
+
expect(remainingBinding).toBeUndefined();
|
|
549
575
|
});
|
|
550
576
|
});
|
|
@@ -52,6 +52,10 @@ import {
|
|
|
52
52
|
isGuardian,
|
|
53
53
|
revokeBinding as serviceRevokeBinding,
|
|
54
54
|
} from '../runtime/channel-guardian-service.js';
|
|
55
|
+
import { handleGuardianVerification } from '../daemon/handlers/config.js';
|
|
56
|
+
import type { GuardianVerificationRequest, GuardianVerificationResponse } from '../daemon/ipc-contract.js';
|
|
57
|
+
import type { HandlerContext } from '../daemon/handlers/shared.js';
|
|
58
|
+
import type * as net from 'node:net';
|
|
55
59
|
|
|
56
60
|
initializeDb();
|
|
57
61
|
|
|
@@ -307,28 +311,32 @@ describe('guardian service challenge validation', () => {
|
|
|
307
311
|
resetTables();
|
|
308
312
|
});
|
|
309
313
|
|
|
310
|
-
test('createVerificationChallenge returns a secret and instruction', () => {
|
|
314
|
+
test('createVerificationChallenge returns a secret, verifyCommand, ttlSeconds, and instruction', () => {
|
|
311
315
|
const result = createVerificationChallenge('asst-1', 'telegram');
|
|
312
316
|
|
|
313
317
|
expect(result.challengeId).toBeDefined();
|
|
314
318
|
expect(result.secret).toBeDefined();
|
|
315
319
|
expect(result.secret.length).toBe(64); // 32 bytes hex-encoded
|
|
320
|
+
expect(result.verifyCommand).toBe(`/guardian_verify ${result.secret}`);
|
|
321
|
+
expect(result.ttlSeconds).toBe(600);
|
|
322
|
+
expect(result.instruction).toBeDefined();
|
|
323
|
+
expect(result.instruction.length).toBeGreaterThan(0);
|
|
316
324
|
expect(result.instruction).toContain('/guardian_verify');
|
|
317
|
-
expect(result.instruction).toContain(result.secret);
|
|
318
325
|
});
|
|
319
326
|
|
|
320
|
-
test('createVerificationChallenge
|
|
327
|
+
test('createVerificationChallenge produces a non-empty instruction for telegram channel', () => {
|
|
321
328
|
const result = createVerificationChallenge('asst-1', 'telegram');
|
|
322
|
-
expect(result.instruction).
|
|
323
|
-
expect(result.instruction).
|
|
329
|
+
expect(result.instruction).toBeDefined();
|
|
330
|
+
expect(result.instruction.length).toBeGreaterThan(0);
|
|
331
|
+
expect(result.instruction).toContain(result.verifyCommand);
|
|
324
332
|
});
|
|
325
333
|
|
|
326
|
-
test('createVerificationChallenge
|
|
334
|
+
test('createVerificationChallenge produces a non-empty instruction for sms channel', () => {
|
|
327
335
|
const result = createVerificationChallenge('asst-1', 'sms');
|
|
328
|
-
expect(result.instruction).
|
|
329
|
-
expect(result.instruction).
|
|
336
|
+
expect(result.instruction).toBeDefined();
|
|
337
|
+
expect(result.instruction.length).toBeGreaterThan(0);
|
|
330
338
|
expect(result.instruction).toContain('/guardian_verify');
|
|
331
|
-
expect(result.instruction).toContain(result.
|
|
339
|
+
expect(result.instruction).toContain(result.verifyCommand);
|
|
332
340
|
});
|
|
333
341
|
|
|
334
342
|
test('validateAndConsumeChallenge succeeds with correct secret', () => {
|
|
@@ -373,8 +381,10 @@ describe('guardian service challenge validation', () => {
|
|
|
373
381
|
|
|
374
382
|
expect(result.success).toBe(false);
|
|
375
383
|
if (!result.success) {
|
|
376
|
-
//
|
|
377
|
-
expect(result.reason).
|
|
384
|
+
// Composed failure message — check it is non-empty and contains "failed"
|
|
385
|
+
expect(result.reason).toBeDefined();
|
|
386
|
+
expect(result.reason.length).toBeGreaterThan(0);
|
|
387
|
+
expect(result.reason.toLowerCase()).toContain('failed');
|
|
378
388
|
}
|
|
379
389
|
});
|
|
380
390
|
|
|
@@ -400,8 +410,10 @@ describe('guardian service challenge validation', () => {
|
|
|
400
410
|
|
|
401
411
|
expect(result.success).toBe(false);
|
|
402
412
|
if (!result.success) {
|
|
403
|
-
//
|
|
404
|
-
expect(result.reason).
|
|
413
|
+
// Composed failure message — check it is non-empty and contains "failed"
|
|
414
|
+
expect(result.reason).toBeDefined();
|
|
415
|
+
expect(result.reason.length).toBeGreaterThan(0);
|
|
416
|
+
expect(result.reason.toLowerCase()).toContain('failed');
|
|
405
417
|
}
|
|
406
418
|
});
|
|
407
419
|
|
|
@@ -939,7 +951,9 @@ describe('guardian service rate limiting', () => {
|
|
|
939
951
|
'asst-1', 'telegram', 'another-wrong', 'user-42', 'chat-42',
|
|
940
952
|
);
|
|
941
953
|
expect(result.success).toBe(false);
|
|
942
|
-
expect((result as { reason: string }).reason).
|
|
954
|
+
expect((result as { reason: string }).reason).toBeDefined();
|
|
955
|
+
expect((result as { reason: string }).reason.length).toBeGreaterThan(0);
|
|
956
|
+
expect((result as { reason: string }).reason.toLowerCase()).toContain('failed');
|
|
943
957
|
|
|
944
958
|
// Verify the rate limit record
|
|
945
959
|
const rl = getRateLimit('asst-1', 'telegram', 'user-42', 'chat-42');
|
|
@@ -971,21 +985,38 @@ describe('guardian service rate limiting', () => {
|
|
|
971
985
|
test('rate-limit uses generic failure message (no oracle leakage)', () => {
|
|
972
986
|
createVerificationChallenge('asst-1', 'telegram');
|
|
973
987
|
|
|
974
|
-
//
|
|
975
|
-
|
|
988
|
+
// Capture a normal invalid-code failure response
|
|
989
|
+
const normalFailure = validateAndConsumeChallenge(
|
|
990
|
+
'asst-1', 'telegram', 'wrong-first', 'user-42', 'chat-42',
|
|
991
|
+
);
|
|
992
|
+
expect(normalFailure.success).toBe(false);
|
|
993
|
+
const normalReason = (normalFailure as { reason: string }).reason;
|
|
994
|
+
|
|
995
|
+
// Trigger rate limit (4 more attempts to reach 5 total)
|
|
996
|
+
for (let i = 0; i < 4; i++) {
|
|
976
997
|
validateAndConsumeChallenge(
|
|
977
998
|
'asst-1', 'telegram', `wrong-${i}`, 'user-42', 'chat-42',
|
|
978
999
|
);
|
|
979
1000
|
}
|
|
980
1001
|
|
|
981
|
-
|
|
1002
|
+
// Verify lockout is actually active before testing the rate-limited response
|
|
1003
|
+
const rl = getRateLimit('asst-1', 'telegram', 'user-42', 'chat-42');
|
|
1004
|
+
expect(rl).not.toBeNull();
|
|
1005
|
+
expect(rl!.lockedUntil).toBeGreaterThan(Date.now());
|
|
1006
|
+
|
|
1007
|
+
// The rate-limited response should be indistinguishable from normal failure
|
|
1008
|
+
const rateLimitedResult = validateAndConsumeChallenge(
|
|
982
1009
|
'asst-1', 'telegram', 'anything', 'user-42', 'chat-42',
|
|
983
1010
|
);
|
|
984
|
-
expect(
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
expect(
|
|
1011
|
+
expect(rateLimitedResult.success).toBe(false);
|
|
1012
|
+
const rateLimitedReason = (rateLimitedResult as { reason: string }).reason;
|
|
1013
|
+
|
|
1014
|
+
// Anti-oracle: both responses must be identical
|
|
1015
|
+
expect(rateLimitedReason).toBe(normalReason);
|
|
1016
|
+
|
|
1017
|
+
// Neither should reveal rate-limiting info
|
|
1018
|
+
expect(rateLimitedReason).not.toContain('rate limit');
|
|
1019
|
+
expect(normalReason).not.toContain('rate limit');
|
|
989
1020
|
});
|
|
990
1021
|
|
|
991
1022
|
test('rate limit does not affect different actors', () => {
|
|
@@ -1186,3 +1217,211 @@ describe('assistant-scoped approval request lookups', () => {
|
|
|
1186
1217
|
expect(foundB!.toolName).toBe('browser');
|
|
1187
1218
|
});
|
|
1188
1219
|
});
|
|
1220
|
+
|
|
1221
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1222
|
+
// 10. IPC handler — channel-aware guardian status response
|
|
1223
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Creates a minimal mock HandlerContext that captures the response sent via ctx.send().
|
|
1227
|
+
*/
|
|
1228
|
+
function createMockCtx(): { ctx: HandlerContext; lastResponse: () => GuardianVerificationResponse | null } {
|
|
1229
|
+
let captured: GuardianVerificationResponse | null = null;
|
|
1230
|
+
const ctx = {
|
|
1231
|
+
sessions: new Map(),
|
|
1232
|
+
socketToSession: new Map(),
|
|
1233
|
+
cuSessions: new Map(),
|
|
1234
|
+
socketToCuSession: new Map(),
|
|
1235
|
+
cuObservationParseSequence: new Map(),
|
|
1236
|
+
socketSandboxOverride: new Map(),
|
|
1237
|
+
sharedRequestTimestamps: [],
|
|
1238
|
+
debounceTimers: { schedule: () => {}, cancel: () => {} } as unknown as HandlerContext['debounceTimers'],
|
|
1239
|
+
suppressConfigReload: false,
|
|
1240
|
+
setSuppressConfigReload: () => {},
|
|
1241
|
+
updateConfigFingerprint: () => {},
|
|
1242
|
+
send: (_socket: net.Socket, msg: unknown) => { captured = msg as GuardianVerificationResponse; },
|
|
1243
|
+
broadcast: () => {},
|
|
1244
|
+
clearAllSessions: () => 0,
|
|
1245
|
+
getOrCreateSession: () => Promise.resolve({} as never),
|
|
1246
|
+
touchSession: () => {},
|
|
1247
|
+
} as unknown as HandlerContext;
|
|
1248
|
+
return { ctx, lastResponse: () => captured };
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const mockSocket = {} as net.Socket;
|
|
1252
|
+
|
|
1253
|
+
describe('IPC handler channel-aware guardian status', () => {
|
|
1254
|
+
beforeEach(() => {
|
|
1255
|
+
resetTables();
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
test('status action for telegram returns channel and assistantId fields', () => {
|
|
1259
|
+
const { ctx, lastResponse } = createMockCtx();
|
|
1260
|
+
const msg: GuardianVerificationRequest = {
|
|
1261
|
+
type: 'guardian_verification',
|
|
1262
|
+
action: 'status',
|
|
1263
|
+
channel: 'telegram',
|
|
1264
|
+
assistantId: 'self',
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
handleGuardianVerification(msg, mockSocket, ctx);
|
|
1268
|
+
|
|
1269
|
+
const resp = lastResponse();
|
|
1270
|
+
expect(resp).not.toBeNull();
|
|
1271
|
+
expect(resp!.success).toBe(true);
|
|
1272
|
+
expect(resp!.channel).toBe('telegram');
|
|
1273
|
+
expect(resp!.assistantId).toBe('self');
|
|
1274
|
+
expect(resp!.bound).toBe(false);
|
|
1275
|
+
expect(resp!.guardianDeliveryChatId).toBeUndefined();
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
test('status action for sms returns channel: sms and assistantId: self', () => {
|
|
1279
|
+
const { ctx, lastResponse } = createMockCtx();
|
|
1280
|
+
const msg: GuardianVerificationRequest = {
|
|
1281
|
+
type: 'guardian_verification',
|
|
1282
|
+
action: 'status',
|
|
1283
|
+
channel: 'sms',
|
|
1284
|
+
assistantId: 'self',
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
handleGuardianVerification(msg, mockSocket, ctx);
|
|
1288
|
+
|
|
1289
|
+
const resp = lastResponse();
|
|
1290
|
+
expect(resp).not.toBeNull();
|
|
1291
|
+
expect(resp!.success).toBe(true);
|
|
1292
|
+
expect(resp!.channel).toBe('sms');
|
|
1293
|
+
expect(resp!.assistantId).toBe('self');
|
|
1294
|
+
expect(resp!.bound).toBe(false);
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
test('status action returns guardianDeliveryChatId when bound', () => {
|
|
1298
|
+
createBinding({
|
|
1299
|
+
assistantId: 'self',
|
|
1300
|
+
channel: 'telegram',
|
|
1301
|
+
guardianExternalUserId: 'user-42',
|
|
1302
|
+
guardianDeliveryChatId: 'chat-42',
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
const { ctx, lastResponse } = createMockCtx();
|
|
1306
|
+
const msg: GuardianVerificationRequest = {
|
|
1307
|
+
type: 'guardian_verification',
|
|
1308
|
+
action: 'status',
|
|
1309
|
+
channel: 'telegram',
|
|
1310
|
+
assistantId: 'self',
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
handleGuardianVerification(msg, mockSocket, ctx);
|
|
1314
|
+
|
|
1315
|
+
const resp = lastResponse();
|
|
1316
|
+
expect(resp).not.toBeNull();
|
|
1317
|
+
expect(resp!.success).toBe(true);
|
|
1318
|
+
expect(resp!.bound).toBe(true);
|
|
1319
|
+
expect(resp!.guardianExternalUserId).toBe('user-42');
|
|
1320
|
+
expect(resp!.guardianDeliveryChatId).toBe('chat-42');
|
|
1321
|
+
expect(resp!.channel).toBe('telegram');
|
|
1322
|
+
expect(resp!.assistantId).toBe('self');
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
test('status action returns guardian username/displayName from binding metadata', () => {
|
|
1326
|
+
createBinding({
|
|
1327
|
+
assistantId: 'self',
|
|
1328
|
+
channel: 'telegram',
|
|
1329
|
+
guardianExternalUserId: 'user-43',
|
|
1330
|
+
guardianDeliveryChatId: 'chat-43',
|
|
1331
|
+
metadataJson: JSON.stringify({ username: 'guardian_handle', displayName: 'Guardian Name' }),
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
const { ctx, lastResponse } = createMockCtx();
|
|
1335
|
+
const msg: GuardianVerificationRequest = {
|
|
1336
|
+
type: 'guardian_verification',
|
|
1337
|
+
action: 'status',
|
|
1338
|
+
channel: 'telegram',
|
|
1339
|
+
assistantId: 'self',
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
handleGuardianVerification(msg, mockSocket, ctx);
|
|
1343
|
+
|
|
1344
|
+
const resp = lastResponse();
|
|
1345
|
+
expect(resp).not.toBeNull();
|
|
1346
|
+
expect(resp!.guardianUsername).toBe('guardian_handle');
|
|
1347
|
+
expect(resp!.guardianDisplayName).toBe('Guardian Name');
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test('status action defaults channel to telegram when omitted (backward compat)', () => {
|
|
1351
|
+
const { ctx, lastResponse } = createMockCtx();
|
|
1352
|
+
const msg: GuardianVerificationRequest = {
|
|
1353
|
+
type: 'guardian_verification',
|
|
1354
|
+
action: 'status',
|
|
1355
|
+
// channel omitted — should default to 'telegram'
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
handleGuardianVerification(msg, mockSocket, ctx);
|
|
1359
|
+
|
|
1360
|
+
const resp = lastResponse();
|
|
1361
|
+
expect(resp).not.toBeNull();
|
|
1362
|
+
expect(resp!.channel).toBe('telegram');
|
|
1363
|
+
expect(resp!.assistantId).toBe('self');
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
test('status action defaults assistantId to self when omitted (backward compat)', () => {
|
|
1367
|
+
const { ctx, lastResponse } = createMockCtx();
|
|
1368
|
+
const msg: GuardianVerificationRequest = {
|
|
1369
|
+
type: 'guardian_verification',
|
|
1370
|
+
action: 'status',
|
|
1371
|
+
channel: 'sms',
|
|
1372
|
+
// assistantId omitted — should default to 'self'
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
handleGuardianVerification(msg, mockSocket, ctx);
|
|
1376
|
+
|
|
1377
|
+
const resp = lastResponse();
|
|
1378
|
+
expect(resp).not.toBeNull();
|
|
1379
|
+
expect(resp!.assistantId).toBe('self');
|
|
1380
|
+
expect(resp!.channel).toBe('sms');
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
test('status action with custom assistantId returns correct value', () => {
|
|
1384
|
+
createBinding({
|
|
1385
|
+
assistantId: 'asst-custom',
|
|
1386
|
+
channel: 'telegram',
|
|
1387
|
+
guardianExternalUserId: 'user-77',
|
|
1388
|
+
guardianDeliveryChatId: 'chat-77',
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
const { ctx, lastResponse } = createMockCtx();
|
|
1392
|
+
const msg: GuardianVerificationRequest = {
|
|
1393
|
+
type: 'guardian_verification',
|
|
1394
|
+
action: 'status',
|
|
1395
|
+
channel: 'telegram',
|
|
1396
|
+
assistantId: 'asst-custom',
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
handleGuardianVerification(msg, mockSocket, ctx);
|
|
1400
|
+
|
|
1401
|
+
const resp = lastResponse();
|
|
1402
|
+
expect(resp).not.toBeNull();
|
|
1403
|
+
expect(resp!.success).toBe(true);
|
|
1404
|
+
expect(resp!.bound).toBe(true);
|
|
1405
|
+
expect(resp!.assistantId).toBe('asst-custom');
|
|
1406
|
+
expect(resp!.channel).toBe('telegram');
|
|
1407
|
+
expect(resp!.guardianExternalUserId).toBe('user-77');
|
|
1408
|
+
expect(resp!.guardianDeliveryChatId).toBe('chat-77');
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
test('status action for unbound sms does not return guardianDeliveryChatId', () => {
|
|
1412
|
+
const { ctx, lastResponse } = createMockCtx();
|
|
1413
|
+
const msg: GuardianVerificationRequest = {
|
|
1414
|
+
type: 'guardian_verification',
|
|
1415
|
+
action: 'status',
|
|
1416
|
+
channel: 'sms',
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
handleGuardianVerification(msg, mockSocket, ctx);
|
|
1420
|
+
|
|
1421
|
+
const resp = lastResponse();
|
|
1422
|
+
expect(resp).not.toBeNull();
|
|
1423
|
+
expect(resp!.bound).toBe(false);
|
|
1424
|
+
expect(resp!.guardianDeliveryChatId).toBeUndefined();
|
|
1425
|
+
expect(resp!.guardianExternalUserId).toBeUndefined();
|
|
1426
|
+
});
|
|
1427
|
+
});
|