@vellumai/assistant 0.3.2 → 0.3.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.
Files changed (109) hide show
  1. package/README.md +82 -21
  2. package/package.json +1 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
  4. package/src/__tests__/app-git-history.test.ts +22 -27
  5. package/src/__tests__/app-git-service.test.ts +44 -78
  6. package/src/__tests__/call-orchestrator.test.ts +321 -0
  7. package/src/__tests__/channel-approval-routes.test.ts +1267 -93
  8. package/src/__tests__/channel-approval.test.ts +2 -0
  9. package/src/__tests__/channel-approvals.test.ts +51 -2
  10. package/src/__tests__/channel-delivery-store.test.ts +130 -1
  11. package/src/__tests__/channel-guardian.test.ts +371 -1
  12. package/src/__tests__/config-schema.test.ts +1 -1
  13. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  14. package/src/__tests__/daemon-lifecycle.test.ts +635 -0
  15. package/src/__tests__/daemon-server-session-init.test.ts +5 -0
  16. package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
  17. package/src/__tests__/handlers-telegram-config.test.ts +82 -0
  18. package/src/__tests__/handlers-twilio-config.test.ts +738 -5
  19. package/src/__tests__/ingress-url-consistency.test.ts +64 -0
  20. package/src/__tests__/ipc-snapshot.test.ts +10 -0
  21. package/src/__tests__/run-orchestrator.test.ts +1 -1
  22. package/src/__tests__/secret-scanner.test.ts +223 -0
  23. package/src/__tests__/session-process-bridge.test.ts +2 -0
  24. package/src/__tests__/shell-parser-property.test.ts +357 -2
  25. package/src/__tests__/system-prompt.test.ts +25 -1
  26. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  27. package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
  28. package/src/__tests__/user-reference.test.ts +68 -0
  29. package/src/calls/call-orchestrator.ts +63 -11
  30. package/src/calls/twilio-config.ts +10 -1
  31. package/src/calls/twilio-rest.ts +70 -0
  32. package/src/cli/map.ts +6 -0
  33. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  34. package/src/commands/cc-command-registry.ts +14 -1
  35. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  36. package/src/config/bundled-skills/email-setup/SKILL.md +56 -0
  37. package/src/config/bundled-skills/messaging/SKILL.md +4 -0
  38. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  39. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  40. package/src/config/defaults.ts +1 -1
  41. package/src/config/schema.ts +6 -3
  42. package/src/config/skills.ts +5 -32
  43. package/src/config/system-prompt.ts +16 -0
  44. package/src/config/user-reference.ts +29 -0
  45. package/src/config/vellum-skills/catalog.json +52 -0
  46. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  47. package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
  48. package/src/daemon/auth-manager.ts +103 -0
  49. package/src/daemon/computer-use-session.ts +8 -1
  50. package/src/daemon/config-watcher.ts +253 -0
  51. package/src/daemon/handlers/config.ts +193 -17
  52. package/src/daemon/handlers/sessions.ts +5 -3
  53. package/src/daemon/handlers/skills.ts +60 -17
  54. package/src/daemon/ipc-contract-inventory.json +4 -0
  55. package/src/daemon/ipc-contract.ts +16 -0
  56. package/src/daemon/ipc-handler.ts +87 -0
  57. package/src/daemon/lifecycle.ts +16 -4
  58. package/src/daemon/ride-shotgun-handler.ts +11 -1
  59. package/src/daemon/server.ts +105 -502
  60. package/src/daemon/session-agent-loop.ts +9 -14
  61. package/src/daemon/session-process.ts +20 -3
  62. package/src/daemon/session-runtime-assembly.ts +60 -44
  63. package/src/daemon/session-slash.ts +50 -2
  64. package/src/daemon/session-surfaces.ts +17 -1
  65. package/src/daemon/session.ts +8 -1
  66. package/src/inbound/public-ingress-urls.ts +20 -3
  67. package/src/index.ts +1 -23
  68. package/src/memory/app-git-service.ts +24 -0
  69. package/src/memory/app-store.ts +0 -21
  70. package/src/memory/channel-delivery-store.ts +74 -3
  71. package/src/memory/channel-guardian-store.ts +54 -26
  72. package/src/memory/conversation-key-store.ts +20 -0
  73. package/src/memory/conversation-store.ts +14 -2
  74. package/src/memory/db-connection.ts +28 -0
  75. package/src/memory/db-init.ts +1019 -0
  76. package/src/memory/db.ts +2 -1995
  77. package/src/memory/embedding-backend.ts +79 -11
  78. package/src/memory/indexer.ts +2 -0
  79. package/src/memory/job-utils.ts +64 -4
  80. package/src/memory/jobs-worker.ts +7 -1
  81. package/src/memory/recall-cache.ts +107 -0
  82. package/src/memory/retriever.ts +30 -1
  83. package/src/memory/schema-migration.ts +984 -0
  84. package/src/memory/schema.ts +6 -0
  85. package/src/memory/search/types.ts +2 -0
  86. package/src/permissions/prompter.ts +14 -3
  87. package/src/permissions/trust-store.ts +7 -0
  88. package/src/runtime/channel-approvals.ts +17 -3
  89. package/src/runtime/gateway-client.ts +2 -1
  90. package/src/runtime/http-server.ts +28 -9
  91. package/src/runtime/routes/channel-routes.ts +279 -100
  92. package/src/runtime/routes/run-routes.ts +7 -1
  93. package/src/runtime/run-orchestrator.ts +8 -1
  94. package/src/security/secret-scanner.ts +218 -0
  95. package/src/skills/clawhub.ts +6 -2
  96. package/src/skills/frontmatter.ts +63 -0
  97. package/src/skills/slash-commands.ts +23 -0
  98. package/src/skills/vellum-catalog-remote.ts +107 -0
  99. package/src/subagent/manager.ts +4 -1
  100. package/src/subagent/types.ts +2 -0
  101. package/src/tools/browser/auto-navigate.ts +132 -24
  102. package/src/tools/browser/browser-manager.ts +67 -61
  103. package/src/tools/claude-code/claude-code.ts +55 -3
  104. package/src/tools/executor.ts +10 -2
  105. package/src/tools/skills/vellum-catalog.ts +75 -127
  106. package/src/tools/subagent/spawn.ts +2 -0
  107. package/src/tools/terminal/parser.ts +21 -5
  108. package/src/util/platform.ts +8 -1
  109. package/src/util/retry.ts +4 -4
@@ -129,6 +129,7 @@ export const memoryEmbeddings = sqliteTable('memory_embeddings', {
129
129
  model: text('model').notNull(),
130
130
  dimensions: integer('dimensions').notNull(),
131
131
  vectorJson: text('vector_json').notNull(),
132
+ contentHash: text('content_hash'),
132
133
  createdAt: integer('created_at').notNull(),
133
134
  updatedAt: integer('updated_at').notNull(),
134
135
  });
@@ -646,6 +647,7 @@ export const channelGuardianApprovalRequests = sqliteTable('channel_guardian_app
646
647
  id: text('id').primaryKey(),
647
648
  runId: text('run_id').notNull(),
648
649
  conversationId: text('conversation_id').notNull(),
650
+ assistantId: text('assistant_id').notNull().default('self'),
649
651
  channel: text('channel').notNull(),
650
652
  requesterExternalUserId: text('requester_external_user_id').notNull(),
651
653
  requesterChatId: text('requester_chat_id').notNull(),
@@ -669,6 +671,10 @@ export const channelGuardianRateLimits = sqliteTable('channel_guardian_rate_limi
669
671
  channel: text('channel').notNull(),
670
672
  actorExternalUserId: text('actor_external_user_id').notNull(),
671
673
  actorChatId: text('actor_chat_id').notNull(),
674
+ // Legacy columns kept with defaults for backward compatibility with upgraded databases
675
+ // that still have the old NOT NULL columns without DEFAULT. Not read by app logic.
676
+ invalidAttempts: integer('invalid_attempts').notNull().default(0),
677
+ windowStartedAt: integer('window_started_at').notNull().default(0),
672
678
  attemptTimestampsJson: text('attempt_timestamps_json').notNull().default('[]'),
673
679
  lockedUntil: integer('locked_until'),
674
680
  createdAt: integer('created_at').notNull(),
@@ -94,6 +94,8 @@ export interface CollectedCandidates {
94
94
  relationNeighborEntityCount: number;
95
95
  relationExpandedItemCount: number;
96
96
  earlyTerminated: boolean;
97
+ /** True when semantic search was attempted but threw an error. */
98
+ semanticSearchFailed: boolean;
97
99
  merged: Candidate[];
98
100
  }
99
101
 
@@ -10,7 +10,12 @@ import { redactSensitiveFields } from '../security/redaction.js';
10
10
  const log = getLogger('permission-prompter');
11
11
 
12
12
  interface PendingPrompt {
13
- resolve: (value: { decision: UserDecision; selectedPattern?: string; selectedScope?: string }) => void;
13
+ resolve: (value: {
14
+ decision: UserDecision;
15
+ selectedPattern?: string;
16
+ selectedScope?: string;
17
+ decisionContext?: string;
18
+ }) => void;
14
19
  reject: (reason: Error) => void;
15
20
  timer: ReturnType<typeof setTimeout>;
16
21
  }
@@ -38,7 +43,12 @@ export class PermissionPrompter {
38
43
  sessionId?: string,
39
44
  executionTarget?: ExecutionTarget,
40
45
  persistentDecisionsAllowed?: boolean,
41
- ): Promise<{ decision: UserDecision; selectedPattern?: string; selectedScope?: string }> {
46
+ ): Promise<{
47
+ decision: UserDecision;
48
+ selectedPattern?: string;
49
+ selectedScope?: string;
50
+ decisionContext?: string;
51
+ }> {
42
52
  const requestId = uuid();
43
53
 
44
54
  return new Promise((resolve, reject) => {
@@ -77,6 +87,7 @@ export class PermissionPrompter {
77
87
  decision: UserDecision,
78
88
  selectedPattern?: string,
79
89
  selectedScope?: string,
90
+ decisionContext?: string,
80
91
  ): void {
81
92
  const pending = this.pending.get(requestId);
82
93
  if (!pending) {
@@ -85,7 +96,7 @@ export class PermissionPrompter {
85
96
  }
86
97
  clearTimeout(pending.timer);
87
98
  this.pending.delete(requestId);
88
- pending.resolve({ decision, selectedPattern, selectedScope });
99
+ pending.resolve({ decision, selectedPattern, selectedScope, decisionContext });
89
100
  }
90
101
 
91
102
  dispose(): void {
@@ -27,13 +27,17 @@ let cachedStarterBundleAccepted: boolean | null = null;
27
27
  * on every tool-call permission check.
28
28
  */
29
29
  const compiledPatterns = new Map<string, Minimatch>();
30
+ /** Patterns that failed compilation — cached to avoid repeated attempts and log spam. */
31
+ const invalidPatterns = new Set<string>();
30
32
 
31
33
  /** Get or compile a Minimatch object for the given pattern. Returns null if the pattern is invalid. */
32
34
  function getCompiledPattern(pattern: string): Minimatch | null {
35
+ if (invalidPatterns.has(pattern)) return null;
33
36
  let compiled = compiledPatterns.get(pattern);
34
37
  if (!compiled) {
35
38
  if (typeof pattern !== 'string') {
36
39
  log.warn({ pattern }, 'Cannot compile non-string pattern');
40
+ invalidPatterns.add(pattern as string);
37
41
  return null;
38
42
  }
39
43
  try {
@@ -41,6 +45,7 @@ function getCompiledPattern(pattern: string): Minimatch | null {
41
45
  compiledPatterns.set(pattern, compiled);
42
46
  } catch (err) {
43
47
  log.warn({ pattern, err }, 'Failed to compile pattern');
48
+ invalidPatterns.add(pattern);
44
49
  return null;
45
50
  }
46
51
  }
@@ -50,6 +55,7 @@ function getCompiledPattern(pattern: string): Minimatch | null {
50
55
  /** Rebuild the compiled pattern cache from the current rule set. */
51
56
  function rebuildPatternCache(rules: TrustRule[]): void {
52
57
  compiledPatterns.clear();
58
+ invalidPatterns.clear();
53
59
  for (const rule of rules) {
54
60
  if (typeof rule.pattern !== 'string') {
55
61
  log.warn({ ruleId: rule.id, pattern: rule.pattern }, 'Skipping rule with non-string pattern during cache rebuild');
@@ -509,6 +515,7 @@ export function clearCache(): void {
509
515
  cachedRules = null;
510
516
  cachedStarterBundleAccepted = null;
511
517
  compiledPatterns.clear();
518
+ invalidPatterns.clear();
512
519
  }
513
520
 
514
521
  // ─── Starter approval bundle ────────────────────────────────────────────────
@@ -13,6 +13,7 @@
13
13
  import { getPendingConfirmationsByConversation, getRun } from '../memory/runs-store.js';
14
14
  import type { PendingRunInfo } from '../memory/runs-store.js';
15
15
  import { addRule } from '../permissions/trust-store.js';
16
+ import { getTool } from '../tools/registry.js';
16
17
  import type { RunOrchestrator } from './run-orchestrator.js';
17
18
  import { DEFAULT_APPROVAL_ACTIONS } from './channel-approval-types.js';
18
19
  import type {
@@ -101,11 +102,17 @@ export function handleChannelDecision(
101
102
  conversationId: string,
102
103
  decision: ApprovalDecisionResult,
103
104
  orchestrator: RunOrchestrator,
105
+ decisionContext?: string,
104
106
  ): HandleDecisionResult {
105
107
  const pending = getPendingConfirmationsByConversation(conversationId);
106
108
  if (pending.length === 0) return { applied: false };
107
109
 
108
- const info = pending[0];
110
+ // Callback-based decisions include a run ID and must resolve to that exact
111
+ // pending confirmation. Plain-text decisions still apply to the first prompt.
112
+ const info = decision.runId
113
+ ? pending.find((candidate) => candidate.runId === decision.runId)
114
+ : pending[0];
115
+ if (!info) return { applied: false };
109
116
 
110
117
  if (decision.action === 'approve_always') {
111
118
  // Only persist a trust rule when the confirmation explicitly allows persistence
@@ -121,8 +128,13 @@ export function handleChannelDecision(
121
128
  ) {
122
129
  const pattern = confirmation.allowlistOptions[0].pattern;
123
130
  const scope = confirmation.scopeOptions[0].scope;
131
+ // Only persist executionTarget for skill-origin tools — core tools don't
132
+ // set it in their PolicyContext, so a persisted value would prevent the
133
+ // rule from ever matching on subsequent permission checks.
134
+ const tool = getTool(confirmation.toolName);
135
+ const executionTarget = tool?.origin === 'skill' ? confirmation.executionTarget : undefined;
124
136
  addRule(confirmation.toolName, pattern, scope, 'allow', 100, {
125
- executionTarget: confirmation.executionTarget,
137
+ executionTarget,
126
138
  });
127
139
  }
128
140
  // When persistence is not allowed or options are missing, the decision
@@ -131,7 +143,9 @@ export function handleChannelDecision(
131
143
 
132
144
  // Map channel-level action to the permission system's UserDecision type.
133
145
  const userDecision = decision.action === 'reject' ? 'deny' as const : 'allow' as const;
134
- const result = orchestrator.submitDecision(info.runId, userDecision);
146
+ const result = decisionContext === undefined
147
+ ? orchestrator.submitDecision(info.runId, userDecision)
148
+ : orchestrator.submitDecision(info.runId, userDecision, decisionContext);
135
149
 
136
150
  return {
137
151
  applied: result === 'applied',
@@ -52,7 +52,8 @@ export async function deliverApprovalPrompt(
52
52
  chatId: string,
53
53
  text: string,
54
54
  approval: ApprovalUIMetadata,
55
+ assistantId?: string,
55
56
  bearerToken?: string,
56
57
  ): Promise<void> {
57
- await deliverChannelReply(callbackUrl, { chatId, text, approval }, bearerToken);
58
+ await deliverChannelReply(callbackUrl, { chatId, text, approval, assistantId }, bearerToken);
58
59
  }
@@ -41,7 +41,6 @@ import {
41
41
  handleChannelDeliveryAck,
42
42
  handleListDeadLetters,
43
43
  handleReplayDeadLetters,
44
- isChannelApprovalsEnabled,
45
44
  startGuardianExpirySweep,
46
45
  stopGuardianExpirySweep,
47
46
  } from './routes/channel-routes.js';
@@ -154,13 +153,16 @@ const GATEWAY_SUBPATH_MAP: Record<string, string> = {
154
153
  voice: 'voice-webhook',
155
154
  status: 'status',
156
155
  'connect-action': 'connect-action',
156
+ sms: 'sms',
157
157
  };
158
158
 
159
159
  /**
160
160
  * Direct Twilio webhook subpaths that are blocked in gateway_only mode.
161
+ * Includes all public-facing webhook paths (voice, status, connect-action, SMS)
162
+ * because the runtime must never serve as a direct ingress for external webhooks.
161
163
  * Internal forwarding endpoints (gateway→runtime) are unaffected.
162
164
  */
163
- const GATEWAY_ONLY_BLOCKED_SUBPATHS = new Set(['voice-webhook', 'status', 'connect-action']);
165
+ const GATEWAY_ONLY_BLOCKED_SUBPATHS = new Set(['voice-webhook', 'status', 'connect-action', 'sms']);
164
166
 
165
167
  /**
166
168
  * Check if a request origin is from a private/internal network address.
@@ -411,8 +413,10 @@ export class RuntimeHttpServer {
411
413
  }, 30_000);
412
414
  }
413
415
 
414
- // Start proactive guardian approval expiry sweep when approvals are enabled
415
- if (isChannelApprovalsEnabled() && this.runOrchestrator) {
416
+ // Start proactive guardian approval expiry sweep whenever orchestrator
417
+ // support is available. Guardian approvals can be created even when the
418
+ // generic channel-approval UX flag is disabled.
419
+ if (this.runOrchestrator) {
416
420
  startGuardianExpirySweep(this.runOrchestrator, getGatewayBaseUrl(), this.bearerToken);
417
421
  log.info('Guardian approval expiry sweep started');
418
422
  }
@@ -616,7 +620,7 @@ export class RuntimeHttpServer {
616
620
  const assistantId = match[1];
617
621
  const endpoint = match[2];
618
622
  log.warn({ endpoint, assistantId }, '[deprecated] /v1/assistants/:assistantId/... route used; migrate to /v1/...');
619
- return this.dispatchEndpoint(endpoint, req, url);
623
+ return this.dispatchEndpoint(endpoint, req, url, assistantId);
620
624
  }
621
625
 
622
626
  /**
@@ -628,6 +632,7 @@ export class RuntimeHttpServer {
628
632
  endpoint: string,
629
633
  req: Request,
630
634
  url: URL,
635
+ assistantId: string = 'self',
631
636
  ): Promise<Response> {
632
637
  try {
633
638
  if (endpoint === 'health' && req.method === 'GET') {
@@ -636,7 +641,9 @@ export class RuntimeHttpServer {
636
641
 
637
642
  if (endpoint === 'conversations' && req.method === 'GET') {
638
643
  const limit = Number(url.searchParams.get('limit') ?? 50);
639
- const conversations = conversationStore.listConversations(limit);
644
+ const offset = Number(url.searchParams.get('offset') ?? 0);
645
+ const conversations = conversationStore.listConversations(limit, false, offset);
646
+ const totalCount = conversationStore.countConversations();
640
647
  const bindings = externalConversationStore.getBindingsForConversations(
641
648
  conversations.map((c) => c.id),
642
649
  );
@@ -659,6 +666,7 @@ export class RuntimeHttpServer {
659
666
  } : {}),
660
667
  };
661
668
  }),
669
+ hasMore: offset + conversations.length < totalCount,
662
670
  });
663
671
  }
664
672
 
@@ -732,11 +740,12 @@ export class RuntimeHttpServer {
732
740
  }
733
741
 
734
742
  if (endpoint === 'channels/conversation' && req.method === 'DELETE') {
735
- return await handleDeleteConversation(req);
743
+ return await handleDeleteConversation(req, assistantId);
736
744
  }
737
745
 
738
746
  if (endpoint === 'channels/inbound' && req.method === 'POST') {
739
- return await handleChannelInbound(req, this.processMessage, this.bearerToken, this.runOrchestrator);
747
+ const gatewayOriginSecret = process.env.RUNTIME_GATEWAY_ORIGIN_SECRET || undefined;
748
+ return await handleChannelInbound(req, this.processMessage, this.bearerToken, this.runOrchestrator, assistantId, gatewayOriginSecret);
740
749
  }
741
750
 
742
751
  if (endpoint === 'channels/delivery-ack' && req.method === 'POST') {
@@ -931,8 +940,16 @@ export class RuntimeHttpServer {
931
940
  const externalChatId = typeof payload.externalChatId === 'string'
932
941
  ? payload.externalChatId
933
942
  : undefined;
943
+ const assistantId = typeof payload.assistantId === 'string'
944
+ ? payload.assistantId
945
+ : undefined;
934
946
  if (externalChatId) {
935
- await this.deliverReplyViaCallback(event.conversationId, externalChatId, replyCallbackUrl);
947
+ await this.deliverReplyViaCallback(
948
+ event.conversationId,
949
+ externalChatId,
950
+ replyCallbackUrl,
951
+ assistantId,
952
+ );
936
953
  }
937
954
  }
938
955
  } catch (err) {
@@ -946,6 +963,7 @@ export class RuntimeHttpServer {
946
963
  conversationId: string,
947
964
  externalChatId: string,
948
965
  callbackUrl: string,
966
+ assistantId?: string,
949
967
  ): Promise<void> {
950
968
  const msgs = conversationStore.getMessages(conversationId);
951
969
  for (let i = msgs.length - 1; i >= 0; i--) {
@@ -968,6 +986,7 @@ export class RuntimeHttpServer {
968
986
  chatId: externalChatId,
969
987
  text: rendered.text || undefined,
970
988
  attachments: replyAttachments.length > 0 ? replyAttachments : undefined,
989
+ assistantId,
971
990
  }, this.bearerToken);
972
991
  }
973
992
  break;