@vellumai/assistant 0.9.0-dev.202606182147.d5eebd3 → 0.9.0-dev.202606182338.1cc9a2c
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/openapi.yaml +46 -0
- package/package.json +1 -1
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -13
- package/src/__tests__/config-schema.test.ts +34 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +150 -25
- package/src/__tests__/reaction-persistence.test.ts +110 -1
- package/src/api/events/tool-result.ts +6 -0
- package/src/calls/guardian-dispatch.ts +17 -41
- package/src/config/call-site-defaults.ts +3 -0
- package/src/config/llm-resolver.ts +3 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/llm.ts +23 -0
- package/src/config/schemas/services.ts +18 -0
- package/src/config/seed-inference-profiles.ts +20 -0
- package/src/daemon/message-types/web-activity.ts +7 -1
- package/src/messaging/providers/slack/send.test.ts +114 -2
- package/src/messaging/providers/slack/send.ts +30 -7
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/canonical-delivery-recorder.ts +124 -24
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +5 -3
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +6 -3
- package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
- package/src/plugins/defaults/advisor/config.ts +10 -11
- package/src/plugins/defaults/advisor/consult.ts +25 -6
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +7 -1
- package/src/plugins/defaults/advisor/tools/advisor.ts +11 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/fetch-provider-catalog.ts +85 -0
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/runtime/access-request-helper.ts +17 -44
- package/src/runtime/confirmation-request-guardian-bridge.ts +18 -13
- package/src/runtime/routes/conversation-query-routes.ts +18 -0
- package/src/runtime/routes/guardian-approval-prompt.ts +19 -47
- package/src/runtime/routes/inbound-message-handler.ts +33 -234
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
- package/src/runtime/tool-grant-request-helper.ts +17 -10
- package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
- package/src/security/secret-patterns.ts +3 -0
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
- package/src/tools/network/__tests__/web-search.test.ts +143 -0
- package/src/tools/network/web-fetch.ts +372 -1
- package/src/tools/network/web-search.ts +213 -10
package/openapi.yaml
CHANGED
|
@@ -16239,6 +16239,7 @@ paths:
|
|
|
16239
16239
|
- brave
|
|
16240
16240
|
- perplexity
|
|
16241
16241
|
- tavily
|
|
16242
|
+
- firecrawl
|
|
16242
16243
|
resultCount:
|
|
16243
16244
|
type: number
|
|
16244
16245
|
durationMs:
|
|
@@ -16286,6 +16287,11 @@ paths:
|
|
|
16286
16287
|
type: string
|
|
16287
16288
|
finalUrl:
|
|
16288
16289
|
type: string
|
|
16290
|
+
provider:
|
|
16291
|
+
type: string
|
|
16292
|
+
enum:
|
|
16293
|
+
- default
|
|
16294
|
+
- firecrawl
|
|
16289
16295
|
status:
|
|
16290
16296
|
type: number
|
|
16291
16297
|
contentType:
|
|
@@ -16621,6 +16627,7 @@ paths:
|
|
|
16621
16627
|
- brave
|
|
16622
16628
|
- perplexity
|
|
16623
16629
|
- tavily
|
|
16630
|
+
- firecrawl
|
|
16624
16631
|
resultCount:
|
|
16625
16632
|
type: number
|
|
16626
16633
|
durationMs:
|
|
@@ -16668,6 +16675,11 @@ paths:
|
|
|
16668
16675
|
type: string
|
|
16669
16676
|
finalUrl:
|
|
16670
16677
|
type: string
|
|
16678
|
+
provider:
|
|
16679
|
+
type: string
|
|
16680
|
+
enum:
|
|
16681
|
+
- default
|
|
16682
|
+
- firecrawl
|
|
16671
16683
|
status:
|
|
16672
16684
|
type: number
|
|
16673
16685
|
contentType:
|
|
@@ -28733,6 +28745,10 @@ components:
|
|
|
28733
28745
|
anyOf:
|
|
28734
28746
|
- type: string
|
|
28735
28747
|
- type: "null"
|
|
28748
|
+
advisorProfile:
|
|
28749
|
+
anyOf:
|
|
28750
|
+
- type: string
|
|
28751
|
+
- type: "null"
|
|
28736
28752
|
callSites:
|
|
28737
28753
|
type: object
|
|
28738
28754
|
propertyNames:
|
|
@@ -28768,6 +28784,16 @@ components:
|
|
|
28768
28784
|
type: string
|
|
28769
28785
|
additionalProperties: {}
|
|
28770
28786
|
- type: "null"
|
|
28787
|
+
web-fetch:
|
|
28788
|
+
anyOf:
|
|
28789
|
+
- type: object
|
|
28790
|
+
properties:
|
|
28791
|
+
mode:
|
|
28792
|
+
$ref: "#/components/schemas/ServiceMode"
|
|
28793
|
+
provider:
|
|
28794
|
+
type: string
|
|
28795
|
+
additionalProperties: {}
|
|
28796
|
+
- type: "null"
|
|
28771
28797
|
image-generation:
|
|
28772
28798
|
anyOf:
|
|
28773
28799
|
- type: object
|
|
@@ -28963,6 +28989,12 @@ components:
|
|
|
28963
28989
|
- $ref: "#/components/schemas/ProfileStatus"
|
|
28964
28990
|
- type: "null"
|
|
28965
28991
|
- type: "null"
|
|
28992
|
+
advisorEnabled:
|
|
28993
|
+
anyOf:
|
|
28994
|
+
- anyOf:
|
|
28995
|
+
- type: boolean
|
|
28996
|
+
- type: "null"
|
|
28997
|
+
- type: "null"
|
|
28966
28998
|
mix:
|
|
28967
28999
|
anyOf:
|
|
28968
29000
|
- minItems: 2
|
|
@@ -29354,6 +29386,8 @@ components:
|
|
|
29354
29386
|
type: string
|
|
29355
29387
|
activeProfile:
|
|
29356
29388
|
type: string
|
|
29389
|
+
advisorProfile:
|
|
29390
|
+
type: string
|
|
29357
29391
|
callSites:
|
|
29358
29392
|
type: object
|
|
29359
29393
|
propertyNames:
|
|
@@ -29504,6 +29538,14 @@ components:
|
|
|
29504
29538
|
provider:
|
|
29505
29539
|
type: string
|
|
29506
29540
|
additionalProperties: {}
|
|
29541
|
+
web-fetch:
|
|
29542
|
+
type: object
|
|
29543
|
+
properties:
|
|
29544
|
+
mode:
|
|
29545
|
+
$ref: "#/components/schemas/ServiceMode"
|
|
29546
|
+
provider:
|
|
29547
|
+
type: string
|
|
29548
|
+
additionalProperties: {}
|
|
29507
29549
|
image-generation:
|
|
29508
29550
|
type: object
|
|
29509
29551
|
properties:
|
|
@@ -29653,6 +29695,10 @@ components:
|
|
|
29653
29695
|
anyOf:
|
|
29654
29696
|
- $ref: "#/components/schemas/ProfileStatus"
|
|
29655
29697
|
- type: "null"
|
|
29698
|
+
advisorEnabled:
|
|
29699
|
+
anyOf:
|
|
29700
|
+
- type: boolean
|
|
29701
|
+
- type: "null"
|
|
29656
29702
|
mix:
|
|
29657
29703
|
minItems: 2
|
|
29658
29704
|
type: array
|
package/package.json
CHANGED
|
@@ -236,7 +236,7 @@ describe("channel inbound disk pressure gate", () => {
|
|
|
236
236
|
expect(db.select().from(messages).all()).toHaveLength(0);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
-
test("blocks non-guardian Slack reactions before persistence while locked", async () => {
|
|
239
|
+
test("blocks non-guardian Slack reactions silently (no reply) before persistence while locked", async () => {
|
|
240
240
|
upsertContact({
|
|
241
241
|
displayName: "Example Slack User",
|
|
242
242
|
channels: [
|
|
@@ -274,18 +274,10 @@ describe("channel inbound disk pressure gate", () => {
|
|
|
274
274
|
reason: "trusted-contact",
|
|
275
275
|
});
|
|
276
276
|
expect(processMessage).not.toHaveBeenCalled();
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
chatId: "slack-channel-1",
|
|
282
|
-
text: expectedRemoteBlockReply,
|
|
283
|
-
assistantId: "self",
|
|
284
|
-
ephemeral: true,
|
|
285
|
-
user: "slack-user-1",
|
|
286
|
-
},
|
|
287
|
-
],
|
|
288
|
-
]);
|
|
277
|
+
// Reactions are blocked silently during disk pressure: a passive signal
|
|
278
|
+
// has nothing to "try again", so no block reply is delivered (unlike the
|
|
279
|
+
// message path, which does reply).
|
|
280
|
+
expect(deliverChannelReplyMock).not.toHaveBeenCalled();
|
|
289
281
|
|
|
290
282
|
const db = getDb();
|
|
291
283
|
const event = db
|
|
@@ -145,6 +145,40 @@ describe("AssistantConfigSchema", () => {
|
|
|
145
145
|
expect(result.services["web-search"].mode).toBe("your-own");
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
test("accepts Firecrawl as a web search provider", () => {
|
|
149
|
+
const result = AssistantConfigSchema.parse({
|
|
150
|
+
services: {
|
|
151
|
+
"web-search": { mode: "your-own", provider: "firecrawl" },
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.services["web-search"].provider).toBe("firecrawl");
|
|
156
|
+
expect(result.services["web-search"].mode).toBe("your-own");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("defaults the web-fetch provider to the built-in fetcher", () => {
|
|
160
|
+
const result = AssistantConfigSchema.parse({});
|
|
161
|
+
expect(result.services["web-fetch"].provider).toBe("default");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("accepts Firecrawl as a web fetch provider", () => {
|
|
165
|
+
const result = AssistantConfigSchema.parse({
|
|
166
|
+
services: {
|
|
167
|
+
"web-fetch": { mode: "your-own", provider: "firecrawl" },
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result.services["web-fetch"].provider).toBe("firecrawl");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("rejects an unknown web-fetch provider", () => {
|
|
175
|
+
expect(() =>
|
|
176
|
+
AssistantConfigSchema.parse({
|
|
177
|
+
services: { "web-fetch": { provider: "nope" } },
|
|
178
|
+
}),
|
|
179
|
+
).toThrow();
|
|
180
|
+
});
|
|
181
|
+
|
|
148
182
|
test("accepts valid complete config", () => {
|
|
149
183
|
const input = {
|
|
150
184
|
llm: {
|
|
@@ -158,6 +158,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
158
158
|
"credential-execution/prompted-credential.ts", // shared prompt-action persistence (stores secret via setSecureKeyAsync)
|
|
159
159
|
"tools/credentials/broker.ts", // brokered credential access
|
|
160
160
|
"tools/network/web-search.ts", // web search API key lookup
|
|
161
|
+
"tools/network/web-fetch.ts", // web fetch provider (Firecrawl) API key lookup
|
|
161
162
|
"daemon/handlers/config-telegram.ts", // Telegram bot token management
|
|
162
163
|
"daemon/handlers/config-vercel.ts", // Vercel API token management
|
|
163
164
|
"runtime/routes/integrations/twilio.ts", // Twilio credential management (HTTP control-plane)
|
|
@@ -25,12 +25,15 @@ import {
|
|
|
25
25
|
type CanonicalGuardianRequest,
|
|
26
26
|
createCanonicalGuardianDelivery,
|
|
27
27
|
createCanonicalGuardianRequest,
|
|
28
|
+
getPendingCanonicalRequestByDestinationMessage,
|
|
28
29
|
listCanonicalGuardianDeliveries,
|
|
29
30
|
} from "../memory/canonical-guardian-store.js";
|
|
30
31
|
import { getDb } from "../memory/db-connection.js";
|
|
31
32
|
import { initializeDb } from "../memory/db-init.js";
|
|
32
|
-
import {
|
|
33
|
-
|
|
33
|
+
import {
|
|
34
|
+
recordApprovalCardDelivery,
|
|
35
|
+
recordGuardianRequestDeliveries,
|
|
36
|
+
} from "../notifications/canonical-delivery-recorder.js";
|
|
34
37
|
|
|
35
38
|
initializeDb();
|
|
36
39
|
|
|
@@ -232,28 +235,161 @@ describe("withdrawGuardianRequestCards", () => {
|
|
|
232
235
|
});
|
|
233
236
|
});
|
|
234
237
|
|
|
235
|
-
describe("
|
|
238
|
+
describe("recordApprovalCardDelivery", () => {
|
|
236
239
|
beforeEach(() => {
|
|
237
240
|
resetTables();
|
|
238
|
-
withdrawSlackApprovalCard.mockClear();
|
|
239
241
|
});
|
|
240
242
|
|
|
241
|
-
test("
|
|
243
|
+
test("records a channel card with its addressing and status", () => {
|
|
242
244
|
const req = makeRequest();
|
|
243
|
-
const
|
|
245
|
+
const delivery = recordApprovalCardDelivery({
|
|
246
|
+
requestId: req.id,
|
|
247
|
+
channel: "slack",
|
|
248
|
+
chatId: "C1",
|
|
249
|
+
messageId: "1700000000.0001",
|
|
250
|
+
status: "sent",
|
|
251
|
+
});
|
|
252
|
+
expect(delivery?.destinationChannel).toBe("slack");
|
|
253
|
+
expect(delivery?.destinationChatId).toBe("C1");
|
|
254
|
+
expect(delivery?.destinationMessageId).toBe("1700000000.0001");
|
|
255
|
+
expect(delivery?.status).toBe("sent");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("records a vellum card addressed by conversation id, defaulting to pending", () => {
|
|
259
|
+
const req = makeRequest();
|
|
260
|
+
const delivery = recordApprovalCardDelivery({
|
|
261
|
+
requestId: req.id,
|
|
262
|
+
channel: "vellum",
|
|
263
|
+
conversationId: "conv-x",
|
|
264
|
+
});
|
|
265
|
+
expect(delivery?.destinationConversationId).toBe("conv-x");
|
|
266
|
+
expect(delivery?.destinationChatId).toBeNull();
|
|
267
|
+
expect(delivery?.status).toBe("pending");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("lets a Slack reaction resolve back to its request (LUM-2502)", () => {
|
|
271
|
+
// A delivered Slack approval card must be addressable by (channel, chat, ts)
|
|
272
|
+
// so an emoji reaction on it resolves to the right request rather than
|
|
273
|
+
// silently falling through to transcript persistence.
|
|
274
|
+
const req = makeRequest();
|
|
275
|
+
recordApprovalCardDelivery({
|
|
276
|
+
requestId: req.id,
|
|
244
277
|
channel: "slack",
|
|
245
|
-
|
|
278
|
+
chatId: "C-guardian",
|
|
279
|
+
messageId: "1700000000.5678",
|
|
246
280
|
status: "sent",
|
|
247
|
-
|
|
248
|
-
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const resolved = getPendingCanonicalRequestByDestinationMessage(
|
|
284
|
+
"slack",
|
|
285
|
+
"C-guardian",
|
|
286
|
+
"1700000000.5678",
|
|
287
|
+
);
|
|
288
|
+
expect(resolved?.id).toBe(req.id);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("recordGuardianRequestDeliveries", () => {
|
|
293
|
+
beforeEach(() => {
|
|
294
|
+
resetTables();
|
|
295
|
+
withdrawSlackApprovalCard.mockClear();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("records each delivery with addressing + status and returns the vellum id", () => {
|
|
299
|
+
const req = makeRequest();
|
|
300
|
+
const vellumId = recordGuardianRequestDeliveries({
|
|
301
|
+
requestId: req.id,
|
|
302
|
+
deliveryResults: [
|
|
303
|
+
{
|
|
304
|
+
channel: "vellum",
|
|
305
|
+
destination: "",
|
|
306
|
+
status: "sent",
|
|
307
|
+
conversationId: "conv-1",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
channel: "slack",
|
|
311
|
+
destination: "C999",
|
|
312
|
+
status: "sent",
|
|
313
|
+
messageId: "1700000000.1234",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const deliveries = listCanonicalGuardianDeliveries(req.id);
|
|
319
|
+
expect(deliveries).toHaveLength(2);
|
|
320
|
+
const vellum = deliveries.find((d) => d.destinationChannel === "vellum");
|
|
321
|
+
const slack = deliveries.find((d) => d.destinationChannel === "slack");
|
|
322
|
+
expect(vellumId).toBe(vellum?.id);
|
|
323
|
+
expect(vellum?.destinationConversationId).toBe("conv-1");
|
|
324
|
+
expect(vellum?.status).toBe("sent");
|
|
325
|
+
expect(slack?.destinationChatId).toBe("C999");
|
|
326
|
+
expect(slack?.destinationMessageId).toBe("1700000000.1234");
|
|
327
|
+
expect(slack?.status).toBe("sent");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("reuses a pre-created vellum row instead of creating a second", () => {
|
|
331
|
+
const req = makeRequest();
|
|
332
|
+
const pre = recordApprovalCardDelivery({
|
|
333
|
+
requestId: req.id,
|
|
334
|
+
channel: "vellum",
|
|
335
|
+
conversationId: "conv-1",
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const vellumId = recordGuardianRequestDeliveries({
|
|
339
|
+
requestId: req.id,
|
|
340
|
+
deliveryResults: [
|
|
341
|
+
{
|
|
342
|
+
channel: "vellum",
|
|
343
|
+
destination: "",
|
|
344
|
+
status: "sent",
|
|
345
|
+
conversationId: "conv-1",
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
vellumDeliveryId: pre?.id,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(vellumId).toBe(pre?.id);
|
|
352
|
+
const deliveries = listCanonicalGuardianDeliveries(req.id);
|
|
353
|
+
expect(deliveries).toHaveLength(1);
|
|
354
|
+
expect(deliveries[0].status).toBe("sent");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("marks a non-sent delivery result as failed (status now tracked for all producers)", () => {
|
|
358
|
+
const req = makeRequest();
|
|
359
|
+
recordGuardianRequestDeliveries({
|
|
360
|
+
requestId: req.id,
|
|
361
|
+
deliveryResults: [
|
|
362
|
+
{ channel: "slack", destination: "C1", status: "failed" },
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
const [delivery] = listCanonicalGuardianDeliveries(req.id);
|
|
366
|
+
expect(delivery.status).toBe("failed");
|
|
367
|
+
});
|
|
249
368
|
|
|
250
|
-
|
|
369
|
+
test("omits chat id when the channel destination is empty", () => {
|
|
370
|
+
const req = makeRequest();
|
|
371
|
+
recordGuardianRequestDeliveries({
|
|
372
|
+
requestId: req.id,
|
|
373
|
+
deliveryResults: [{ channel: "slack", destination: "", status: "sent" }],
|
|
374
|
+
});
|
|
375
|
+
const [delivery] = listCanonicalGuardianDeliveries(req.id);
|
|
376
|
+
expect(delivery.destinationChatId).toBeNull();
|
|
377
|
+
});
|
|
251
378
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
379
|
+
test("records a Slack delivery the withdrawal path can then edit in place", async () => {
|
|
380
|
+
const req = makeRequest();
|
|
381
|
+
recordGuardianRequestDeliveries({
|
|
382
|
+
requestId: req.id,
|
|
383
|
+
deliveryResults: [
|
|
384
|
+
{
|
|
385
|
+
channel: "slack",
|
|
386
|
+
destination: "C999",
|
|
387
|
+
status: "sent",
|
|
388
|
+
messageId: "1700000000.1234",
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
});
|
|
255
392
|
|
|
256
|
-
// End-to-end: the recorded delivery is now editable by the withdrawal path.
|
|
257
393
|
await withdrawGuardianRequestCards({
|
|
258
394
|
request: req,
|
|
259
395
|
status: "approved",
|
|
@@ -264,15 +400,4 @@ describe("recordCanonicalChannelDelivery", () => {
|
|
|
264
400
|
"1700000000.1234",
|
|
265
401
|
);
|
|
266
402
|
});
|
|
267
|
-
|
|
268
|
-
test("omits chat id when the destination is empty", () => {
|
|
269
|
-
const req = makeRequest();
|
|
270
|
-
const delivery = recordCanonicalChannelDelivery(req.id, {
|
|
271
|
-
channel: "slack",
|
|
272
|
-
destination: "",
|
|
273
|
-
status: "sent",
|
|
274
|
-
});
|
|
275
|
-
expect(delivery.destinationChatId).toBeNull();
|
|
276
|
-
expect(listCanonicalGuardianDeliveries(req.id)).toHaveLength(1);
|
|
277
|
-
});
|
|
278
403
|
});
|
|
@@ -59,7 +59,7 @@ import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
|
59
59
|
import {
|
|
60
60
|
isSlackReactionEvent,
|
|
61
61
|
parseSlackReactionCallbackData,
|
|
62
|
-
} from "../runtime/routes/inbound-
|
|
62
|
+
} from "../runtime/routes/inbound-stages/reaction-intercept.js";
|
|
63
63
|
import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
|
|
64
64
|
import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
|
|
65
65
|
|
|
@@ -575,3 +575,112 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
|
|
|
575
575
|
expect(reactionRows.length).toBe(0);
|
|
576
576
|
});
|
|
577
577
|
});
|
|
578
|
+
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// Reaction access-control regression (LUM-2489)
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
//
|
|
583
|
+
// A reaction is a passive signal, not an access attempt. Because reactions are
|
|
584
|
+
// dispatched before the ingress pipeline, an unknown user's 👍 must never run
|
|
585
|
+
// ACL (no trusted-contact verification handshake), create a conversation, or
|
|
586
|
+
// write a binding — it is dropped as channel noise. A known contact's reaction
|
|
587
|
+
// is recorded; neither triggers a verification challenge.
|
|
588
|
+
|
|
589
|
+
describe("reaction access control (no verification handshake)", () => {
|
|
590
|
+
const STRANGER_USER_ID = "U_REACTION_STRANGER";
|
|
591
|
+
const CONTACT_USER_ID = "U_REACTION_CONTACT";
|
|
592
|
+
// Guardian's approval channel is a DM, distinct from the public channel the
|
|
593
|
+
// reaction lands in (mirroring production). Reusing the public channel id
|
|
594
|
+
// would let a reactor match the guardian's channel via findContactChannel's
|
|
595
|
+
// externalChatId fallback and mask the bug.
|
|
596
|
+
const GUARDIAN_DM_CHAT = "D_GUARDIAN_DM";
|
|
597
|
+
|
|
598
|
+
function tableCount(table: string): number {
|
|
599
|
+
return (
|
|
600
|
+
getDb().$client.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as {
|
|
601
|
+
n: number;
|
|
602
|
+
}
|
|
603
|
+
).n;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
beforeEach(() => {
|
|
607
|
+
resetState();
|
|
608
|
+
getDb().run("DELETE FROM channel_verification_sessions");
|
|
609
|
+
// The assistant has a guardian (as in production); the reactors below are
|
|
610
|
+
// different users.
|
|
611
|
+
createGuardianBinding({
|
|
612
|
+
channel: "slack",
|
|
613
|
+
guardianExternalUserId: GUARDIAN_USER_ID,
|
|
614
|
+
guardianDeliveryChatId: GUARDIAN_DM_CHAT,
|
|
615
|
+
guardianPrincipalId: GUARDIAN_USER_ID,
|
|
616
|
+
});
|
|
617
|
+
msgCounter = 0;
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("stranger's reaction is dropped — no challenge, session, conversation, or row", async () => {
|
|
621
|
+
let agentDispatched = false;
|
|
622
|
+
const processMessage = async (): Promise<{ messageId: string }> => {
|
|
623
|
+
agentDispatched = true;
|
|
624
|
+
return { messageId: "should-not-be-called" };
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const req = buildReactionRequest("reaction:thumbsup", {
|
|
628
|
+
actorExternalId: STRANGER_USER_ID,
|
|
629
|
+
actorDisplayName: "Outside Reactor",
|
|
630
|
+
actorUsername: "outsider",
|
|
631
|
+
});
|
|
632
|
+
const resp = await handleChannelInbound(
|
|
633
|
+
req,
|
|
634
|
+
processMessage,
|
|
635
|
+
TEST_BEARER_TOKEN,
|
|
636
|
+
);
|
|
637
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
638
|
+
|
|
639
|
+
// Accepted as a passive signal — never denied or turned into a challenge.
|
|
640
|
+
expect(json.accepted).toBe(true);
|
|
641
|
+
expect(json.denied).not.toBe(true);
|
|
642
|
+
expect(json.reason).not.toBe("verification_challenge_sent");
|
|
643
|
+
expect(json.verificationSessionId).toBeUndefined();
|
|
644
|
+
expect(agentDispatched).toBe(false);
|
|
645
|
+
|
|
646
|
+
// No verification handshake, and no side effects: a dropped reaction leaves
|
|
647
|
+
// no transcript row, no conversation, and no binding.
|
|
648
|
+
expect(tableCount("channel_verification_sessions")).toBe(0);
|
|
649
|
+
expect(readPersistedMessages().length).toBe(0);
|
|
650
|
+
expect(tableCount("conversations")).toBe(0);
|
|
651
|
+
expect(tableCount("external_conversation_bindings")).toBe(0);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("known contact's reaction is recorded — no challenge", async () => {
|
|
655
|
+
// A pending contact classifies as `unverified_contact` — a known tier, so
|
|
656
|
+
// its reactions are recorded. On a real message it would be re-challenged,
|
|
657
|
+
// but a reaction must not trigger that.
|
|
658
|
+
upsertContactChannel({
|
|
659
|
+
sourceChannel: "slack",
|
|
660
|
+
externalUserId: CONTACT_USER_ID,
|
|
661
|
+
externalChatId: SLACK_CHANNEL_ID,
|
|
662
|
+
status: "pending",
|
|
663
|
+
policy: "allow",
|
|
664
|
+
displayName: "Pending Contact",
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const req = buildReactionRequest("reaction:tada", {
|
|
668
|
+
actorExternalId: CONTACT_USER_ID,
|
|
669
|
+
actorDisplayName: "Pending Contact",
|
|
670
|
+
actorUsername: "pending_contact",
|
|
671
|
+
});
|
|
672
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
673
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
674
|
+
|
|
675
|
+
expect(json.denied).not.toBe(true);
|
|
676
|
+
expect(json.reason).not.toBe("verification_challenge_sent");
|
|
677
|
+
expect(tableCount("channel_verification_sessions")).toBe(0);
|
|
678
|
+
|
|
679
|
+
const rows = readPersistedMessages();
|
|
680
|
+
expect(rows.length).toBe(1);
|
|
681
|
+
const envelope = JSON.parse(rows[0].metadata!) as Record<string, unknown>;
|
|
682
|
+
const slackMeta = readSlackMetadata(envelope.slackMeta as string);
|
|
683
|
+
expect(slackMeta?.eventKind).toBe("reaction");
|
|
684
|
+
expect(slackMeta?.reaction?.emoji).toBe("tada");
|
|
685
|
+
});
|
|
686
|
+
});
|
|
@@ -47,10 +47,15 @@ export const WebSearchProviderIdSchema = z.enum([
|
|
|
47
47
|
"brave",
|
|
48
48
|
"perplexity",
|
|
49
49
|
"tavily",
|
|
50
|
+
"firecrawl",
|
|
50
51
|
]);
|
|
51
52
|
|
|
52
53
|
export type WebSearchProviderId = z.infer<typeof WebSearchProviderIdSchema>;
|
|
53
54
|
|
|
55
|
+
export const WebFetchProviderIdSchema = z.enum(["default", "firecrawl"]);
|
|
56
|
+
|
|
57
|
+
export type WebFetchProviderId = z.infer<typeof WebFetchProviderIdSchema>;
|
|
58
|
+
|
|
54
59
|
export const WebSearchResultItemSchema = z.object({
|
|
55
60
|
rank: z.number(),
|
|
56
61
|
title: z.string(),
|
|
@@ -78,6 +83,7 @@ export type WebSearchMetadata = z.infer<typeof WebSearchMetadataSchema>;
|
|
|
78
83
|
export const WebFetchMetadataSchema = z.object({
|
|
79
84
|
url: z.string(),
|
|
80
85
|
finalUrl: z.string(),
|
|
86
|
+
provider: WebFetchProviderIdSchema.optional(),
|
|
81
87
|
status: z.number(),
|
|
82
88
|
contentType: z.string().optional(),
|
|
83
89
|
byteCount: z.number(),
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
import { findGuardianForChannel } from "../contacts/contact-store.js";
|
|
11
11
|
import {
|
|
12
|
-
createCanonicalGuardianDelivery,
|
|
13
12
|
createCanonicalGuardianRequest,
|
|
14
13
|
listCanonicalGuardianDeliveries,
|
|
15
14
|
listCanonicalGuardianRequests,
|
|
16
|
-
updateCanonicalGuardianDelivery,
|
|
17
15
|
} from "../memory/canonical-guardian-store.js";
|
|
18
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
recordApprovalCardDelivery,
|
|
18
|
+
recordGuardianRequestDeliveries,
|
|
19
|
+
} from "../notifications/canonical-delivery-recorder.js";
|
|
19
20
|
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
20
|
-
import type { NotificationDeliveryResult } from "../notifications/types.js";
|
|
21
21
|
import { getLogger } from "../util/logger.js";
|
|
22
22
|
import { getUserConsultationTimeoutMs } from "./call-constants.js";
|
|
23
23
|
import type { CallPendingQuestion } from "./types.js";
|
|
@@ -40,17 +40,6 @@ export interface GuardianDispatchParams {
|
|
|
40
40
|
inputDigest?: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function applyDeliveryStatus(
|
|
44
|
-
deliveryId: string,
|
|
45
|
-
result: NotificationDeliveryResult,
|
|
46
|
-
): void {
|
|
47
|
-
if (result.status === "sent") {
|
|
48
|
-
updateCanonicalGuardianDelivery(deliveryId, { status: "sent" });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
updateCanonicalGuardianDelivery(deliveryId, { status: "failed" });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
43
|
/**
|
|
55
44
|
* Dispatch a guardian action request to all configured channels.
|
|
56
45
|
* Fire-and-forget: errors are logged but do not propagate.
|
|
@@ -180,7 +169,7 @@ async function dispatchGuardianQuestionInner(
|
|
|
180
169
|
|
|
181
170
|
// Route through the canonical notification pipeline. The paired vellum
|
|
182
171
|
// conversation from this pipeline is the canonical guardian conversation.
|
|
183
|
-
let vellumDeliveryId: string |
|
|
172
|
+
let vellumDeliveryId: string | undefined;
|
|
184
173
|
const requestCode =
|
|
185
174
|
request.requestCode ?? request.id.slice(0, 6).toUpperCase();
|
|
186
175
|
const signalResult = await emitNotificationSignal({
|
|
@@ -209,39 +198,26 @@ async function dispatchGuardianQuestionInner(
|
|
|
209
198
|
onConversationCreated: (info) => {
|
|
210
199
|
if (info.sourceEventName !== "guardian.question" || vellumDeliveryId)
|
|
211
200
|
return;
|
|
212
|
-
|
|
201
|
+
vellumDeliveryId = recordApprovalCardDelivery({
|
|
213
202
|
requestId: request.id,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
vellumDeliveryId = delivery.id;
|
|
203
|
+
channel: "vellum",
|
|
204
|
+
conversationId: info.conversationId,
|
|
205
|
+
})?.id;
|
|
218
206
|
},
|
|
219
207
|
});
|
|
220
208
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
destinationChannel: "vellum",
|
|
227
|
-
destinationConversationId: result.conversationId,
|
|
228
|
-
});
|
|
229
|
-
vellumDeliveryId = delivery.id;
|
|
230
|
-
}
|
|
231
|
-
applyDeliveryStatus(vellumDeliveryId, result);
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const delivery = recordCanonicalChannelDelivery(request.id, result);
|
|
236
|
-
applyDeliveryStatus(delivery.id, result);
|
|
237
|
-
}
|
|
209
|
+
vellumDeliveryId = recordGuardianRequestDeliveries({
|
|
210
|
+
requestId: request.id,
|
|
211
|
+
deliveryResults: signalResult.deliveryResults,
|
|
212
|
+
vellumDeliveryId,
|
|
213
|
+
});
|
|
238
214
|
|
|
239
215
|
if (!vellumDeliveryId) {
|
|
240
|
-
|
|
216
|
+
recordApprovalCardDelivery({
|
|
241
217
|
requestId: request.id,
|
|
242
|
-
|
|
218
|
+
channel: "vellum",
|
|
219
|
+
status: "failed",
|
|
243
220
|
});
|
|
244
|
-
updateCanonicalGuardianDelivery(fallback.id, { status: "failed" });
|
|
245
221
|
log.warn(
|
|
246
222
|
{ requestId: request.id, reason: signalResult.reason },
|
|
247
223
|
"Notification pipeline did not produce a vellum delivery result",
|
|
@@ -69,6 +69,9 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
|
|
|
69
69
|
meetConsentMonitor: { profile: "cost-optimized" },
|
|
70
70
|
meetChatOpportunity: { profile: "cost-optimized" },
|
|
71
71
|
inference: { profile: "cost-optimized" },
|
|
72
|
+
// The advisor consults the strongest managed profile by default; a workspace
|
|
73
|
+
// overrides this via `llm.advisorProfile` (which floats above this).
|
|
74
|
+
advisor: { profile: "quality-optimized" },
|
|
72
75
|
|
|
73
76
|
heartbeatAgent: {
|
|
74
77
|
profile: "cost-optimized",
|
|
@@ -366,6 +366,9 @@ function profileConfigFragment(profile: ProfileEntry): Mergeable {
|
|
|
366
366
|
// lower-precedence (e.g. active) profile into one that merely inherited it.
|
|
367
367
|
// `RetryProvider` resolves it from the applied profile, not the merge.
|
|
368
368
|
logitBias: _logitBias,
|
|
369
|
+
// Per-profile advisor toggle is profile identity, not inheritable model
|
|
370
|
+
// config — strip it so it can't leak into the merged `LLMConfigBase`.
|
|
371
|
+
advisorEnabled: _advisorEnabled,
|
|
369
372
|
...config
|
|
370
373
|
} = profile;
|
|
371
374
|
return config as Mergeable;
|