@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.
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +16 -3
- package/package.json +1 -1
- package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
- package/src/__tests__/config-loader-backfill.test.ts +5 -5
- package/src/__tests__/context-search-conversations-source.test.ts +193 -333
- package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +125 -2
- package/src/__tests__/persona-resolver.test.ts +56 -2
- package/src/__tests__/trust-context-guards.test.ts +21 -10
- package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
- package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
- package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
- package/src/channels/channel-verification-sessions.ts +6 -1
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/persistence/__tests__/conversation-queries-search.test.ts +83 -84
- package/src/persistence/conversation-crud.ts +0 -41
- package/src/persistence/conversation-queries.ts +60 -180
- package/src/persistence/conversation-row-batch-delete.ts +8 -11
- package/src/persistence/conversation-search-lexical.ts +5 -4
- package/src/persistence/job-handlers/cleanup.ts +3 -4
- package/src/persistence/migrations/116-messages-fts.ts +8 -1
- package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
- package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
- package/src/persistence/raw-query.ts +0 -3
- package/src/persistence/schema/contacts.ts +0 -28
- package/src/persistence/steps.ts +4 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +6 -58
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +60 -236
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +8 -0
- package/src/prompts/__tests__/system-prompt.test.ts +166 -1
- package/src/prompts/persona-resolver.ts +13 -9
- package/src/prompts/system-prompt.ts +24 -1
- package/src/prompts/templates/system-sections.ts +32 -0
- package/src/prompts/templates/users/default.md +19 -0
- package/src/providers/model-intents.ts +2 -2
- package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
- package/src/runtime/access-request-helper.ts +70 -2
- package/src/runtime/channel-verification-service.ts +9 -3
- package/src/runtime/local-actor-identity.test.ts +38 -0
- package/src/runtime/local-actor-identity.ts +14 -0
- package/src/runtime/routes/guardian-approval-interception.ts +51 -0
- package/src/runtime/routes/inbound-message-handler.ts +71 -38
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +255 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +216 -73
- package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
- package/src/runtime/tool-grant-request-helper.ts +17 -1
- package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
- package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
- package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
- 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
|
|
46
|
-
//
|
|
47
|
-
//
|
|
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
|
@@ -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
|
-
}
|
|
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
|
-
}
|
|
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
|
|
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-
|
|
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 (
|
|
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-
|
|
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` (
|
|
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`.
|