@vellumai/assistant 0.10.4-dev.202607012234.a9be056 → 0.10.4-dev.202607020106.aa66e63

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 (53) hide show
  1. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  2. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +16 -3
  3. package/package.json +1 -1
  4. package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
  5. package/src/__tests__/config-loader-backfill.test.ts +5 -5
  6. package/src/__tests__/context-search-conversations-source.test.ts +193 -333
  7. package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
  8. package/src/__tests__/model-intents.test.ts +1 -1
  9. package/src/__tests__/non-member-access-request.test.ts +125 -2
  10. package/src/__tests__/persona-resolver.test.ts +56 -2
  11. package/src/__tests__/trust-context-guards.test.ts +21 -10
  12. package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
  13. package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
  14. package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
  15. package/src/channels/channel-verification-sessions.ts +6 -1
  16. package/src/config/seed-inference-profiles.ts +2 -2
  17. package/src/persistence/__tests__/conversation-queries-search.test.ts +83 -84
  18. package/src/persistence/conversation-crud.ts +0 -41
  19. package/src/persistence/conversation-queries.ts +60 -180
  20. package/src/persistence/conversation-row-batch-delete.ts +8 -11
  21. package/src/persistence/conversation-search-lexical.ts +5 -4
  22. package/src/persistence/job-handlers/cleanup.ts +3 -4
  23. package/src/persistence/migrations/116-messages-fts.ts +8 -1
  24. package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
  25. package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
  26. package/src/persistence/raw-query.ts +0 -3
  27. package/src/persistence/schema/contacts.ts +0 -28
  28. package/src/persistence/steps.ts +4 -0
  29. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +6 -58
  30. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +60 -236
  31. package/src/plugins/defaults/turn-context/unified-turn-context.ts +8 -0
  32. package/src/prompts/__tests__/system-prompt.test.ts +166 -1
  33. package/src/prompts/persona-resolver.ts +13 -9
  34. package/src/prompts/system-prompt.ts +24 -1
  35. package/src/prompts/templates/system-sections.ts +32 -0
  36. package/src/prompts/templates/users/default.md +19 -0
  37. package/src/providers/model-intents.ts +2 -2
  38. package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
  39. package/src/runtime/access-request-helper.ts +70 -2
  40. package/src/runtime/channel-verification-service.ts +9 -3
  41. package/src/runtime/local-actor-identity.test.ts +38 -0
  42. package/src/runtime/local-actor-identity.ts +14 -0
  43. package/src/runtime/routes/guardian-approval-interception.ts +51 -0
  44. package/src/runtime/routes/inbound-message-handler.ts +71 -38
  45. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +255 -0
  46. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +216 -73
  47. package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
  48. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
  49. package/src/runtime/tool-grant-request-helper.ts +17 -1
  50. package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
  51. package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
  52. package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
  53. package/src/workspace/migrations/registry.ts +6 -0
@@ -77,6 +77,7 @@ export type { AdmissionPolicy } from "./admission-policy-contract.js";
77
77
 
78
78
  // Trust verdict contract (gateway → daemon) — Zod schemas + derived types
79
79
  export {
80
+ isTrustClass,
80
81
  makeResolutionFailedVerdict,
81
82
  ResolveInboundTrustRequestSchema,
82
83
  TRUST_CLASS_VALUES,
@@ -32,6 +32,18 @@ export const TrustClassSchema = z.enum(TRUST_CLASS_VALUES);
32
32
 
33
33
  export type TrustClass = (typeof TRUST_CLASS_VALUES)[number];
34
34
 
35
+ const TRUST_CLASS_SET: ReadonlySet<string> = new Set(TRUST_CLASS_VALUES);
36
+
37
+ /**
38
+ * Type guard for wire-sourced trust classes. Consumers receive verdicts over
39
+ * IPC/HTTP, so a field statically typed {@link TrustClass} can still carry an
40
+ * out-of-contract value (version skew, malformed payload); this narrows it
41
+ * safely without casting.
42
+ */
43
+ export function isTrustClass(value: string): value is TrustClass {
44
+ return TRUST_CLASS_SET.has(value);
45
+ }
46
+
35
47
  /**
36
48
  * Per-actor trust verdict resolved by the gateway from its ACL DB. ACL +
37
49
  * identity keys + minimal labels only. Guardian binding and member
@@ -42,9 +54,10 @@ export const TrustVerdictSchema = z.object({
42
54
  trustClass: TrustClassSchema,
43
55
  canonicalSenderId: z.string().nullable(),
44
56
 
45
- // Present+true ⇒ gateway attempted resolution but failed (DB error);
46
- // consumer treats it as "could not vouch", distinct from a real `unknown`
47
- // stranger.
57
+ // Present+true ⇒ gateway attempted resolution but could not produce a
58
+ // usable verdict (DB error, or a sender who maps to the guardian contact
59
+ // whose principal is unresolved); consumer treats it as "could not vouch",
60
+ // distinct from a real `unknown` stranger.
48
61
  resolutionFailed: z.boolean().optional(),
49
62
 
50
63
  // Guardian binding — present only when a guardian binding matches.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.4-dev.202607012234.a9be056",
3
+ "version": "0.10.4-dev.202607020106.aa66e63",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -24,8 +24,14 @@ mock.module("../daemon/conversation-registry.js", () => ({
24
24
  findConversation: (id: string) => _conversationMocks.get(id),
25
25
  }));
26
26
 
27
+ // Anchored guardian principal, driven per-test to exercise the guardian
28
+ // principal gate (undefined = anchor unresolvable).
29
+ let _anchorPrincipalId: string | undefined;
30
+ mock.module("../runtime/local-actor-identity.js", () => ({
31
+ findLocalGuardianPrincipalId: async () => _anchorPrincipalId,
32
+ }));
33
+
27
34
  import type { Conversation } from "../daemon/conversation.js";
28
- import type { TrustContext } from "../daemon/trust-context.js";
29
35
  import { initializeDb } from "../persistence/db-init.js";
30
36
  import * as approvalMessageComposer from "../runtime/approval-message-composer.js";
31
37
  import * as gatewayClient from "../runtime/gateway-client.js";
@@ -76,6 +82,7 @@ describe("approval interception trust-class gates", () => {
76
82
 
77
83
  beforeEach(() => {
78
84
  pendingInteractions.clear();
85
+ _anchorPrincipalId = undefined;
79
86
  deliverSpy = spyOn(gatewayClient, "deliverChannelReply").mockResolvedValue({
80
87
  ok: true,
81
88
  });
@@ -105,7 +112,7 @@ describe("approval interception trust-class gates", () => {
105
112
  trustClass: "unknown",
106
113
  requesterExternalUserId: "intruder-user-1",
107
114
  guardianExternalUserId: "guardian-1",
108
- } as TrustContext,
115
+ },
109
116
  assistantId: ASSISTANT_ID,
110
117
  });
111
118
 
@@ -117,6 +124,144 @@ describe("approval interception trust-class gates", () => {
117
124
  composeSpy.mockRestore();
118
125
  });
119
126
 
127
+ test("guardian apr: callback with a principal matching the anchor applies the decision", async () => {
128
+ _anchorPrincipalId = "guardian-principal-1";
129
+ const sessionMock = registerPendingInteraction(
130
+ "req-guardian-apply-1",
131
+ CONVERSATION_ID,
132
+ TOOL_NAME,
133
+ TOOL_INPUT,
134
+ );
135
+
136
+ const result = await handleApprovalInterception({
137
+ conversationId: CONVERSATION_ID,
138
+ callbackData: "apr:req-guardian-apply-1:approve_once",
139
+ content: "",
140
+ conversationExternalId: REQUESTER_CHAT,
141
+ sourceChannel: "telegram",
142
+ actorExternalId: "guardian-user-1",
143
+ replyCallbackUrl: "https://gateway.test/deliver",
144
+ trustCtx: {
145
+ sourceChannel: "telegram",
146
+ trustClass: "guardian",
147
+ requesterExternalUserId: "guardian-user-1",
148
+ guardianExternalUserId: "guardian-user-1",
149
+ guardianPrincipalId: "guardian-principal-1",
150
+ },
151
+ assistantId: ASSISTANT_ID,
152
+ });
153
+
154
+ expect(result.handled).toBe(true);
155
+ expect(result.type).toBe("decision_applied");
156
+ expect(sessionMock).toHaveBeenCalled();
157
+ });
158
+
159
+ test("guardian apr: callback with a principal NOT matching the anchor is rejected before any decision", async () => {
160
+ _anchorPrincipalId = "the-real-guardian-principal";
161
+ const sessionMock = registerPendingInteraction(
162
+ "req-guardian-mismatch-1",
163
+ CONVERSATION_ID,
164
+ TOOL_NAME,
165
+ TOOL_INPUT,
166
+ );
167
+
168
+ const result = await handleApprovalInterception({
169
+ conversationId: CONVERSATION_ID,
170
+ callbackData: "apr:req-guardian-mismatch-1:approve_once",
171
+ content: "",
172
+ conversationExternalId: REQUESTER_CHAT,
173
+ sourceChannel: "telegram",
174
+ actorExternalId: "stale-guardian-user",
175
+ replyCallbackUrl: "https://gateway.test/deliver",
176
+ trustCtx: {
177
+ sourceChannel: "telegram",
178
+ trustClass: "guardian",
179
+ requesterExternalUserId: "stale-guardian-user",
180
+ guardianExternalUserId: "stale-guardian-user",
181
+ guardianPrincipalId: "some-other-principal",
182
+ },
183
+ assistantId: ASSISTANT_ID,
184
+ });
185
+
186
+ expect(result.handled).toBe(true);
187
+ expect(result.type).toBe("stale_ignored");
188
+ expect(sessionMock).not.toHaveBeenCalled();
189
+ // Generic failure copy — no oracle about pending requests or permission.
190
+ expect(deliverSpy).toHaveBeenCalledWith(
191
+ "https://gateway.test/deliver",
192
+ expect.objectContaining({
193
+ text: "Sorry, I couldn't process that. Please try again.",
194
+ }),
195
+ );
196
+ });
197
+
198
+ test("guardian without a resolved acting principal is rejected before any decision", async () => {
199
+ // Address-only guardian classification (e.g. a binding row with a null
200
+ // principal) must not authorize decisions.
201
+ _anchorPrincipalId = "the-real-guardian-principal";
202
+ const sessionMock = registerPendingInteraction(
203
+ "req-guardian-no-principal-1",
204
+ CONVERSATION_ID,
205
+ TOOL_NAME,
206
+ TOOL_INPUT,
207
+ );
208
+
209
+ const result = await handleApprovalInterception({
210
+ conversationId: CONVERSATION_ID,
211
+ callbackData: "apr:req-guardian-no-principal-1:approve_once",
212
+ content: "",
213
+ conversationExternalId: REQUESTER_CHAT,
214
+ sourceChannel: "telegram",
215
+ actorExternalId: "guardian-user-1",
216
+ replyCallbackUrl: "https://gateway.test/deliver",
217
+ trustCtx: {
218
+ sourceChannel: "telegram",
219
+ trustClass: "guardian",
220
+ requesterExternalUserId: "guardian-user-1",
221
+ guardianExternalUserId: "guardian-user-1",
222
+ },
223
+ assistantId: ASSISTANT_ID,
224
+ });
225
+
226
+ expect(result.handled).toBe(true);
227
+ expect(result.type).toBe("stale_ignored");
228
+ expect(sessionMock).not.toHaveBeenCalled();
229
+ });
230
+
231
+ test("guardian decision defers to the verdict when the anchor read is unresolvable", async () => {
232
+ // Transient gateway miss on the anchor read: the gateway-stamped verdict
233
+ // (which already classified the actor guardian by principal) wins.
234
+ _anchorPrincipalId = undefined;
235
+ const sessionMock = registerPendingInteraction(
236
+ "req-guardian-anchor-miss-1",
237
+ CONVERSATION_ID,
238
+ TOOL_NAME,
239
+ TOOL_INPUT,
240
+ );
241
+
242
+ const result = await handleApprovalInterception({
243
+ conversationId: CONVERSATION_ID,
244
+ callbackData: "apr:req-guardian-anchor-miss-1:approve_once",
245
+ content: "",
246
+ conversationExternalId: REQUESTER_CHAT,
247
+ sourceChannel: "telegram",
248
+ actorExternalId: "guardian-user-1",
249
+ replyCallbackUrl: "https://gateway.test/deliver",
250
+ trustCtx: {
251
+ sourceChannel: "telegram",
252
+ trustClass: "guardian",
253
+ requesterExternalUserId: "guardian-user-1",
254
+ guardianExternalUserId: "guardian-user-1",
255
+ guardianPrincipalId: "guardian-principal-1",
256
+ },
257
+ assistantId: ASSISTANT_ID,
258
+ });
259
+
260
+ expect(result.handled).toBe(true);
261
+ expect(result.type).toBe("decision_applied");
262
+ expect(sessionMock).toHaveBeenCalled();
263
+ });
264
+
120
265
  test("unverified sender (no identity) auto-denies pending approval", async () => {
121
266
  const sessionMock = registerPendingInteraction(
122
267
  "req-unknown-auto-deny-1",
@@ -135,7 +280,7 @@ describe("approval interception trust-class gates", () => {
135
280
  trustCtx: {
136
281
  sourceChannel: "telegram",
137
282
  trustClass: "unknown",
138
- } as TrustContext,
283
+ },
139
284
  assistantId: ASSISTANT_ID,
140
285
  });
141
286
 
@@ -856,9 +856,9 @@ describe("loadConfig startup behavior", () => {
856
856
  );
857
857
  expect(raw.llm.profiles.balanced.effort).toBe("high");
858
858
  expect(raw.llm.profiles.balanced.source).toBe("managed");
859
- // Quality serves Anthropic Opus, the most capable managed profile.
859
+ // Quality serves Anthropic Fable, the most capable managed profile.
860
860
  expect(raw.llm.profiles["quality-optimized"].provider).toBe("anthropic");
861
- expect(raw.llm.profiles["quality-optimized"].model).toBe("claude-opus-4-8");
861
+ expect(raw.llm.profiles["quality-optimized"].model).toBe("claude-fable-5");
862
862
  // Speed is served by DeepSeek V4 Flash on Fireworks.
863
863
  expect(raw.llm.profiles["cost-optimized"].provider).toBe("fireworks");
864
864
  expect(raw.llm.profiles["cost-optimized"].model).toBe(
@@ -899,7 +899,7 @@ describe("loadConfig startup behavior", () => {
899
899
  test("reseed updates a source-less legacy canonical managed profile", () => {
900
900
  // Migration 052 seeded canonical profiles without a `source`. Such a
901
901
  // source-less `quality-optimized` is legacy managed, not user-owned, so it
902
- // must still reseed to the latest template (Opus) and be tagged managed.
902
+ // must still reseed to the latest template (Fable) and be tagged managed.
903
903
  writeConfig({
904
904
  llm: {
905
905
  profiles: {
@@ -915,7 +915,7 @@ describe("loadConfig startup behavior", () => {
915
915
  mergeDefaultConfigAndSeedInferenceProfiles();
916
916
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
917
917
 
918
- expect(raw.llm.profiles["quality-optimized"].model).toBe("claude-opus-4-8");
918
+ expect(raw.llm.profiles["quality-optimized"].model).toBe("claude-fable-5");
919
919
  expect(raw.llm.profiles["quality-optimized"].source).toBe("managed");
920
920
  });
921
921
 
@@ -1574,7 +1574,7 @@ describe("seedInferenceProfiles BYOK-mode managed profile labels", () => {
1574
1574
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1575
1575
 
1576
1576
  expect(raw.llm.activeProfile).toBe("balanced");
1577
- // Balanced lives on `fireworks-managed`; `quality-optimized` (Opus on
1577
+ // Balanced lives on `fireworks-managed`; `quality-optimized` (Fable on
1578
1578
  // `anthropic-managed`) is on a different connection, so selecting balanced at
1579
1579
  // hatch disables it. The default advisor falls to the only active managed
1580
1580
  // profile, `balanced`.