@vellumai/assistant 0.4.32 → 0.4.33
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/docs/architecture/memory.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +83 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +0 -1
- package/src/__tests__/channel-invite-transport.test.ts +52 -40
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/handlers-telegram-config.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -4
- package/src/__tests__/ingress-reconcile.test.ts +3 -36
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
- package/src/__tests__/relay-server.test.ts +145 -2
- package/src/__tests__/sandbox-host-parity.test.ts +5 -2
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +0 -1
- package/src/__tests__/sms-messaging-provider.test.ts +0 -4
- package/src/__tests__/terminal-tools.test.ts +5 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +66 -74
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-routes.test.ts +0 -1
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/user-reference.test.ts +47 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-git-service.test.ts +2 -2
- package/src/calls/relay-server.ts +5 -55
- package/src/channels/config.ts +41 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
- package/src/config/env.ts +0 -4
- package/src/config/feature-flag-registry.json +4 -4
- package/src/config/user-reference.ts +47 -9
- package/src/daemon/handlers/config-channels.ts +11 -10
- package/src/daemon/handlers/contacts.ts +5 -1
- package/src/daemon/lifecycle.ts +18 -26
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/delivery-crud.ts +13 -0
- package/src/memory/invite-store.ts +71 -1
- package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/contacts.ts +2 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
- package/src/runtime/auth/token-service.ts +50 -0
- package/src/runtime/channel-guardian-service.ts +1 -3
- package/src/runtime/channel-invite-transport.ts +121 -34
- package/src/runtime/channel-invite-transports/email.ts +50 -0
- package/src/runtime/channel-invite-transports/slack.ts +81 -0
- package/src/runtime/channel-invite-transports/sms.ts +70 -0
- package/src/runtime/channel-invite-transports/telegram.ts +29 -11
- package/src/runtime/channel-invite-transports/voice.ts +12 -12
- package/src/runtime/invite-redemption-service.ts +193 -0
- package/src/runtime/invite-redemption-templates.ts +6 -6
- package/src/runtime/invite-service.ts +81 -11
- package/src/runtime/routes/access-request-decision.ts +52 -6
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -0
- package/src/runtime/routes/contact-routes.ts +33 -6
- package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -6
- package/src/runtime/routes/inbound-message-handler.ts +1 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +289 -4
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +9 -42
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
- package/src/runtime/routes/invite-routes.ts +1 -0
- package/src/tools/browser/browser-manager.ts +10 -1
- package/src/tools/browser/runtime-check.ts +3 -1
- package/src/tools/shared/shell-output.ts +7 -2
- package/src/util/platform.ts +0 -4
- package/src/workspace/git-service.ts +10 -4
|
@@ -26,7 +26,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
26
26
|
getPidPath: () => join(testDir, "test.pid"),
|
|
27
27
|
getDbPath: () => join(testDir, "test.db"),
|
|
28
28
|
getLogPath: () => join(testDir, "test.log"),
|
|
29
|
-
readHttpToken: () => "test-token",
|
|
30
29
|
ensureDataDir: () => {},
|
|
31
30
|
migrateToDataLayout: () => {},
|
|
32
31
|
migrateToWorkspaceLayout: () => {},
|
|
@@ -80,10 +79,15 @@ mock.module("../runtime/gateway-client.js", () => ({
|
|
|
80
79
|
}));
|
|
81
80
|
|
|
82
81
|
// ── Guardian binding mock ──
|
|
83
|
-
|
|
82
|
+
// mockGuardianContact controls what findGuardianForChannel returns.
|
|
83
|
+
// When non-null, it should look like { contact: { displayName: "..." }, channel: { ... } }.
|
|
84
|
+
let mockGuardianContact: {
|
|
85
|
+
contact: { displayName: string };
|
|
86
|
+
channel: Record<string, unknown>;
|
|
87
|
+
} | null = null;
|
|
84
88
|
|
|
85
89
|
mock.module("../runtime/channel-guardian-service.js", () => ({
|
|
86
|
-
getGuardianBinding: () =>
|
|
90
|
+
getGuardianBinding: () => null,
|
|
87
91
|
// Re-export stubs for other functions to prevent import errors
|
|
88
92
|
bindSessionIdentity: () => {},
|
|
89
93
|
createOutboundSession: () => ({}),
|
|
@@ -100,6 +104,11 @@ mock.module("../runtime/channel-guardian-service.js", () => ({
|
|
|
100
104
|
}),
|
|
101
105
|
}));
|
|
102
106
|
|
|
107
|
+
// ── Contact store mock ──
|
|
108
|
+
mock.module("../contacts/contact-store.js", () => ({
|
|
109
|
+
findGuardianForChannel: () => mockGuardianContact,
|
|
110
|
+
}));
|
|
111
|
+
|
|
103
112
|
// ── Pending interactions mock ──
|
|
104
113
|
let mockPendingApprovals: Array<{
|
|
105
114
|
requestId: string;
|
|
@@ -123,11 +132,22 @@ mock.module("../config/env.js", () => ({
|
|
|
123
132
|
// ── User reference mock ──
|
|
124
133
|
mock.module("../config/user-reference.js", () => ({
|
|
125
134
|
resolveUserReference: () => "my human",
|
|
135
|
+
DEFAULT_USER_REFERENCE: "my human",
|
|
136
|
+
resolveGuardianName: (guardianDisplayName?: string | null): string => {
|
|
137
|
+
// Mirror the real implementation: USER.md name > guardianDisplayName > default
|
|
138
|
+
const userRef = "my human"; // In tests, resolveUserReference() returns this
|
|
139
|
+
if (userRef !== "my human") return userRef;
|
|
140
|
+
if (guardianDisplayName && guardianDisplayName.trim().length > 0) {
|
|
141
|
+
return guardianDisplayName.trim();
|
|
142
|
+
}
|
|
143
|
+
return "my human";
|
|
144
|
+
},
|
|
126
145
|
}));
|
|
127
146
|
|
|
128
147
|
// Import module under test AFTER mocks are set up
|
|
129
148
|
import type { ChannelId } from "../channels/types.js";
|
|
130
|
-
import {
|
|
149
|
+
import { resolveGuardianName } from "../config/user-reference.js";
|
|
150
|
+
import { findGuardianForChannel } from "../contacts/contact-store.js";
|
|
131
151
|
import type { TrustContext } from "../daemon/session-runtime-assembly.js";
|
|
132
152
|
|
|
133
153
|
// We need to test the private functions by importing the module.
|
|
@@ -178,8 +198,6 @@ async function simulateNotifierPoll(params: {
|
|
|
178
198
|
const { getApprovalInfoByConversation } =
|
|
179
199
|
await import("../runtime/channel-approvals.js");
|
|
180
200
|
const { deliverChannelReply } = await import("../runtime/gateway-client.js");
|
|
181
|
-
const { getGuardianBinding } =
|
|
182
|
-
await import("../runtime/channel-guardian-service.js");
|
|
183
201
|
|
|
184
202
|
const pending = getApprovalInfoByConversation(params.conversationId);
|
|
185
203
|
const info = pending[0];
|
|
@@ -198,37 +216,14 @@ async function simulateNotifierPoll(params: {
|
|
|
198
216
|
|
|
199
217
|
notifiedRequestIds.set(info.requestId, conversationId);
|
|
200
218
|
|
|
201
|
-
// Resolve guardian name
|
|
202
|
-
|
|
203
|
-
const binding = getGuardianBinding(
|
|
204
|
-
params.assistantId ?? "self",
|
|
219
|
+
// Resolve guardian name via the contacts-based approach
|
|
220
|
+
const guardian = findGuardianForChannel(
|
|
205
221
|
params.sourceChannel,
|
|
222
|
+
params.assistantId ?? "self",
|
|
206
223
|
);
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const parsed = JSON.parse(binding.metadataJson as string) as Record<
|
|
210
|
-
string,
|
|
211
|
-
unknown
|
|
212
|
-
>;
|
|
213
|
-
if (
|
|
214
|
-
typeof parsed.displayName === "string" &&
|
|
215
|
-
parsed.displayName.trim().length > 0
|
|
216
|
-
) {
|
|
217
|
-
guardianName = parsed.displayName.trim();
|
|
218
|
-
} else if (
|
|
219
|
-
typeof parsed.username === "string" &&
|
|
220
|
-
parsed.username.trim().length > 0
|
|
221
|
-
) {
|
|
222
|
-
guardianName = `@${parsed.username.trim()}`;
|
|
223
|
-
}
|
|
224
|
-
} catch {
|
|
225
|
-
// ignore
|
|
226
|
-
}
|
|
227
|
-
}
|
|
224
|
+
const guardianName = resolveGuardianName(guardian?.contact.displayName);
|
|
228
225
|
|
|
229
|
-
const waitingText = `Waiting for ${
|
|
230
|
-
guardianName ?? resolveUserReference()
|
|
231
|
-
}'s approval...`;
|
|
226
|
+
const waitingText = `Waiting for ${guardianName}'s approval...`;
|
|
232
227
|
|
|
233
228
|
try {
|
|
234
229
|
await deliverChannelReply(
|
|
@@ -256,7 +251,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
256
251
|
deliveredReplies.length = 0;
|
|
257
252
|
deliverShouldFail = false;
|
|
258
253
|
mockPendingApprovals = [];
|
|
259
|
-
|
|
254
|
+
mockGuardianContact = null;
|
|
260
255
|
});
|
|
261
256
|
|
|
262
257
|
afterAll(() => {
|
|
@@ -277,9 +272,9 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
277
272
|
},
|
|
278
273
|
];
|
|
279
274
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
275
|
+
mockGuardianContact = {
|
|
276
|
+
contact: { displayName: "Mom" },
|
|
277
|
+
channel: {},
|
|
283
278
|
};
|
|
284
279
|
|
|
285
280
|
const notified = new Map<string, string>();
|
|
@@ -304,7 +299,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
304
299
|
expect(notified.has("req-1")).toBe(true);
|
|
305
300
|
});
|
|
306
301
|
|
|
307
|
-
test("uses
|
|
302
|
+
test("uses contact displayName from contact store", async () => {
|
|
308
303
|
mockPendingApprovals = [
|
|
309
304
|
{
|
|
310
305
|
requestId: "req-2",
|
|
@@ -314,9 +309,9 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
314
309
|
},
|
|
315
310
|
];
|
|
316
311
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
312
|
+
mockGuardianContact = {
|
|
313
|
+
contact: { displayName: "Guardian User" },
|
|
314
|
+
channel: {},
|
|
320
315
|
};
|
|
321
316
|
|
|
322
317
|
const notified = new Map<string, string>();
|
|
@@ -332,11 +327,11 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
332
327
|
|
|
333
328
|
expect(deliveredReplies).toHaveLength(1);
|
|
334
329
|
expect(deliveredReplies[0].payload.text).toBe(
|
|
335
|
-
"Waiting for
|
|
330
|
+
"Waiting for Guardian User's approval...",
|
|
336
331
|
);
|
|
337
332
|
});
|
|
338
333
|
|
|
339
|
-
test("falls back to user reference when
|
|
334
|
+
test("falls back to user reference when guardian has empty display name", async () => {
|
|
340
335
|
mockPendingApprovals = [
|
|
341
336
|
{
|
|
342
337
|
requestId: "req-3",
|
|
@@ -346,10 +341,10 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
346
341
|
},
|
|
347
342
|
];
|
|
348
343
|
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
344
|
+
// Guardian contact exists but has an empty displayName
|
|
345
|
+
mockGuardianContact = {
|
|
346
|
+
contact: { displayName: "" },
|
|
347
|
+
channel: {},
|
|
353
348
|
};
|
|
354
349
|
|
|
355
350
|
const notified = new Map<string, string>();
|
|
@@ -369,7 +364,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
369
364
|
);
|
|
370
365
|
});
|
|
371
366
|
|
|
372
|
-
test("falls back to user reference when no guardian
|
|
367
|
+
test("falls back to user reference when no guardian contact exists", async () => {
|
|
373
368
|
mockPendingApprovals = [
|
|
374
369
|
{
|
|
375
370
|
requestId: "req-4",
|
|
@@ -379,7 +374,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
379
374
|
},
|
|
380
375
|
];
|
|
381
376
|
|
|
382
|
-
|
|
377
|
+
mockGuardianContact = null;
|
|
383
378
|
|
|
384
379
|
const notified = new Map<string, string>();
|
|
385
380
|
await simulateNotifierPoll({
|
|
@@ -408,9 +403,9 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
408
403
|
},
|
|
409
404
|
];
|
|
410
405
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
406
|
+
mockGuardianContact = {
|
|
407
|
+
contact: { displayName: "Guardian" },
|
|
408
|
+
channel: {},
|
|
414
409
|
};
|
|
415
410
|
|
|
416
411
|
const notified = new Map<string, string>();
|
|
@@ -436,9 +431,9 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
436
431
|
});
|
|
437
432
|
|
|
438
433
|
test("sends separate messages for different requestIds", async () => {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
434
|
+
mockGuardianContact = {
|
|
435
|
+
contact: { displayName: "Guardian" },
|
|
436
|
+
channel: {},
|
|
442
437
|
};
|
|
443
438
|
|
|
444
439
|
const notified = new Map<string, string>();
|
|
@@ -478,9 +473,9 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
478
473
|
});
|
|
479
474
|
|
|
480
475
|
test("concurrent pollers for different conversations do not evict each other", async () => {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
476
|
+
mockGuardianContact = {
|
|
477
|
+
contact: { displayName: "Guardian" },
|
|
478
|
+
channel: {},
|
|
484
479
|
};
|
|
485
480
|
|
|
486
481
|
// Shared dedupe map simulating the module-level global
|
|
@@ -630,9 +625,9 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
630
625
|
},
|
|
631
626
|
];
|
|
632
627
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
628
|
+
mockGuardianContact = {
|
|
629
|
+
contact: { displayName: "Guardian" },
|
|
630
|
+
channel: {},
|
|
636
631
|
};
|
|
637
632
|
|
|
638
633
|
const notified = new Map<string, string>();
|
|
@@ -678,7 +673,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
678
673
|
expect(deliveredReplies).toHaveLength(0);
|
|
679
674
|
});
|
|
680
675
|
|
|
681
|
-
test("
|
|
676
|
+
test("uses contact displayName from guardian contact record", async () => {
|
|
682
677
|
mockPendingApprovals = [
|
|
683
678
|
{
|
|
684
679
|
requestId: "req-10",
|
|
@@ -688,12 +683,9 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
688
683
|
},
|
|
689
684
|
];
|
|
690
685
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
displayName: "Sarah",
|
|
695
|
-
username: "sarah_bot",
|
|
696
|
-
}),
|
|
686
|
+
mockGuardianContact = {
|
|
687
|
+
contact: { displayName: "Sarah" },
|
|
688
|
+
channel: {},
|
|
697
689
|
};
|
|
698
690
|
|
|
699
691
|
const notified = new Map<string, string>();
|
|
@@ -713,7 +705,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
713
705
|
);
|
|
714
706
|
});
|
|
715
707
|
|
|
716
|
-
test("
|
|
708
|
+
test("falls back to default when guardian contact has whitespace-only displayName", async () => {
|
|
717
709
|
mockPendingApprovals = [
|
|
718
710
|
{
|
|
719
711
|
requestId: "req-11",
|
|
@@ -723,9 +715,9 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
723
715
|
},
|
|
724
716
|
];
|
|
725
717
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
718
|
+
mockGuardianContact = {
|
|
719
|
+
contact: { displayName: " " },
|
|
720
|
+
channel: {},
|
|
729
721
|
};
|
|
730
722
|
|
|
731
723
|
const notified = new Map<string, string>();
|
|
@@ -740,7 +732,7 @@ describe("trusted-contact pending-approval notifier", () => {
|
|
|
740
732
|
});
|
|
741
733
|
|
|
742
734
|
expect(deliveredReplies).toHaveLength(1);
|
|
743
|
-
// Falls back to
|
|
735
|
+
// Falls back to default user reference
|
|
744
736
|
expect(deliveredReplies[0].payload.text).toBe(
|
|
745
737
|
"Waiting for my human's approval...",
|
|
746
738
|
);
|
|
@@ -38,7 +38,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
38
38
|
getPidPath: () => join(testDir, "test.pid"),
|
|
39
39
|
getDbPath: () => join(testDir, "test.db"),
|
|
40
40
|
getLogPath: () => join(testDir, "test.log"),
|
|
41
|
-
readHttpToken: () => "test-token",
|
|
42
41
|
ensureDataDir: () => {},
|
|
43
42
|
migrateToDataLayout: () => {},
|
|
44
43
|
migrateToWorkspaceLayout: () => {},
|
|
@@ -33,7 +33,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
33
33
|
getDbPath: () => join(testDir, "test.db"),
|
|
34
34
|
getLogPath: () => join(testDir, "test.log"),
|
|
35
35
|
ensureDataDir: () => {},
|
|
36
|
-
readHttpToken: () => "test-bearer-token",
|
|
37
36
|
}));
|
|
38
37
|
|
|
39
38
|
mock.module("../util/logger.js", () => ({
|
|
@@ -28,7 +28,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
28
28
|
getDbPath: () => join(testDir, "test.db"),
|
|
29
29
|
getLogPath: () => join(testDir, "test.log"),
|
|
30
30
|
ensureDataDir: () => {},
|
|
31
|
-
readHttpToken: () => "test-bearer-token",
|
|
32
31
|
}));
|
|
33
32
|
|
|
34
33
|
mock.module("../util/logger.js", () => ({
|
|
@@ -31,7 +31,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
31
31
|
getDbPath: () => join(testDir, "test.db"),
|
|
32
32
|
getLogPath: () => join(testDir, "test.log"),
|
|
33
33
|
ensureDataDir: () => {},
|
|
34
|
-
readHttpToken: () => "test-bearer-token",
|
|
35
34
|
}));
|
|
36
35
|
|
|
37
36
|
mock.module("../util/logger.js", () => ({
|
|
@@ -57,7 +57,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
57
57
|
getHooksDir: () => "",
|
|
58
58
|
getSocketPath: () => "",
|
|
59
59
|
getSessionTokenPath: () => "",
|
|
60
|
-
getHttpTokenPath: () => "",
|
|
61
60
|
getPlatformTokenPath: () => "",
|
|
62
61
|
getPidPath: () => "",
|
|
63
62
|
getWorkspaceConfigPath: () => "",
|
|
@@ -72,7 +71,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
72
71
|
writeLockfile: () => {},
|
|
73
72
|
readPlatformToken: () => null,
|
|
74
73
|
readSessionToken: () => null,
|
|
75
|
-
readHttpToken: () => null,
|
|
76
74
|
removeSocketFile: () => {},
|
|
77
75
|
getTCPPort: () => 8765,
|
|
78
76
|
isTCPEnabled: () => false,
|
|
@@ -26,7 +26,8 @@ mock.module("node:fs", () => ({
|
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
28
|
// Import after mocks are in place
|
|
29
|
-
const { resolveUserReference } =
|
|
29
|
+
const { resolveUserReference, resolveGuardianName, DEFAULT_USER_REFERENCE } =
|
|
30
|
+
await import("../config/user-reference.js");
|
|
30
31
|
|
|
31
32
|
describe("resolveUserReference", () => {
|
|
32
33
|
beforeEach(() => {
|
|
@@ -69,3 +70,48 @@ describe("resolveUserReference", () => {
|
|
|
69
70
|
expect(resolveUserReference()).toBe("Alice");
|
|
70
71
|
});
|
|
71
72
|
});
|
|
73
|
+
|
|
74
|
+
describe("resolveGuardianName", () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
mockFileExists = false;
|
|
77
|
+
mockFileContent = "";
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("returns USER.md name when present, ignoring guardianDisplayName", () => {
|
|
81
|
+
mockFileExists = true;
|
|
82
|
+
mockFileContent = [
|
|
83
|
+
"## Onboarding Snapshot",
|
|
84
|
+
"",
|
|
85
|
+
"- Preferred name/reference: John",
|
|
86
|
+
].join("\n");
|
|
87
|
+
expect(resolveGuardianName("Jane")).toBe("John");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('returns "my human" when USER.md explicitly sets name to default value', () => {
|
|
91
|
+
mockFileExists = true;
|
|
92
|
+
mockFileContent = [
|
|
93
|
+
"## Onboarding Snapshot",
|
|
94
|
+
"",
|
|
95
|
+
"- Preferred name/reference: my human",
|
|
96
|
+
].join("\n");
|
|
97
|
+
// The user's explicit choice must be respected even though it matches the default sentinel
|
|
98
|
+
expect(resolveGuardianName("Jane")).toBe("my human");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("falls back to guardianDisplayName when USER.md is empty", () => {
|
|
102
|
+
mockFileExists = false;
|
|
103
|
+
expect(resolveGuardianName("Jane")).toBe("Jane");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("falls back to DEFAULT_USER_REFERENCE when both are empty", () => {
|
|
107
|
+
mockFileExists = false;
|
|
108
|
+
expect(resolveGuardianName()).toBe(DEFAULT_USER_REFERENCE);
|
|
109
|
+
expect(resolveGuardianName(null)).toBe(DEFAULT_USER_REFERENCE);
|
|
110
|
+
expect(resolveGuardianName("")).toBe(DEFAULT_USER_REFERENCE);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("trims whitespace on guardianDisplayName fallback", () => {
|
|
114
|
+
mockFileExists = false;
|
|
115
|
+
expect(resolveGuardianName(" Jane ")).toBe("Jane");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -655,7 +655,7 @@ describe("WorkspaceGitService", () => {
|
|
|
655
655
|
cwd: testDir,
|
|
656
656
|
});
|
|
657
657
|
const oldGitignore =
|
|
658
|
-
"# Runtime state - excluded from git tracking\ndata/\nlogs/\n*.log\n*.sock\n*.pid\n*.sqlite\n*.sqlite-journal\n*.sqlite-wal\n*.sqlite-shm\n*.db\n*.db-journal\n*.db-wal\n*.db-shm\nvellum.sock\nvellum.pid\nsession-token\
|
|
658
|
+
"# Runtime state - excluded from git tracking\ndata/\nlogs/\n*.log\n*.sock\n*.pid\n*.sqlite\n*.sqlite-journal\n*.sqlite-wal\n*.sqlite-shm\n*.db\n*.db-journal\n*.db-wal\n*.db-shm\nvellum.sock\nvellum.pid\nsession-token\n";
|
|
659
659
|
writeFileSync(join(testDir, ".gitignore"), oldGitignore);
|
|
660
660
|
writeFileSync(join(testDir, "file.txt"), "content");
|
|
661
661
|
execFileSync("git", ["add", "-A"], { cwd: testDir });
|
|
@@ -725,7 +725,7 @@ describe("WorkspaceGitService", () => {
|
|
|
725
725
|
cwd: testDir,
|
|
726
726
|
});
|
|
727
727
|
const gitignoreContent =
|
|
728
|
-
"# Runtime state - excluded from git tracking\ndata/db/\ndata/qdrant/\ndata/ipc-blobs/\nlogs/\n*.log\n*.sock\n*.pid\n*.sqlite\n*.sqlite-journal\n*.sqlite-wal\n*.sqlite-shm\n*.db\n*.db-journal\n*.db-wal\n*.db-shm\nvellum.sock\nvellum.pid\nsession-token\
|
|
728
|
+
"# Runtime state - excluded from git tracking\ndata/db/\ndata/qdrant/\ndata/ipc-blobs/\nlogs/\n*.log\n*.sock\n*.pid\n*.sqlite\n*.sqlite-journal\n*.sqlite-wal\n*.sqlite-shm\n*.db\n*.db-journal\n*.db-wal\n*.db-shm\nvellum.sock\nvellum.pid\nsession-token\n";
|
|
729
729
|
writeFileSync(join(testDir, ".gitignore"), gitignoreContent);
|
|
730
730
|
writeFileSync(join(testDir, "file.txt"), "content");
|
|
731
731
|
execFileSync("git", ["add", "-A"], { cwd: testDir });
|
|
@@ -10,7 +10,7 @@ import { randomInt } from "node:crypto";
|
|
|
10
10
|
|
|
11
11
|
import type { ServerWebSocket } from "bun";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { resolveGuardianName } from "../config/user-reference.js";
|
|
14
14
|
import {
|
|
15
15
|
findGuardianForChannel,
|
|
16
16
|
listGuardianChannels,
|
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
toTrustContext,
|
|
32
32
|
} from "../runtime/actor-trust-resolver.js";
|
|
33
33
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
34
|
-
import { getGuardianBinding } from "../runtime/channel-guardian-service.js";
|
|
35
34
|
import {
|
|
36
35
|
composeVerificationVoice,
|
|
37
36
|
GUARDIAN_VERIFY_TEMPLATE_KEYS,
|
|
@@ -1597,70 +1596,21 @@ export class RelayConnection {
|
|
|
1597
1596
|
|
|
1598
1597
|
/**
|
|
1599
1598
|
* Resolve a human-readable guardian label for voice wait copy.
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
1599
|
+
* Delegates to the shared resolveGuardianName() which checks USER.md
|
|
1600
|
+
* first, then falls back to Contact.displayName, then DEFAULT_USER_REFERENCE.
|
|
1602
1601
|
*/
|
|
1603
1602
|
private resolveGuardianLabel(): string {
|
|
1604
1603
|
const assistantId =
|
|
1605
1604
|
this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
1606
1605
|
|
|
1607
|
-
//
|
|
1608
|
-
// binding for the assistant (mirrors the cross-channel fallback pattern
|
|
1609
|
-
// in access-request-helper.ts).
|
|
1610
|
-
let metadataJson: string | null = null;
|
|
1611
|
-
// Contacts-first: prefer the voice-bound guardian, then fall back to
|
|
1612
|
-
// any guardian channel (mirrors the voice-first pattern in the legacy path).
|
|
1606
|
+
// Look up the guardian contact for a displayName fallback
|
|
1613
1607
|
const voiceGuardian = findGuardianForChannel("voice", assistantId);
|
|
1614
1608
|
const guardianChannels = voiceGuardian
|
|
1615
1609
|
? null
|
|
1616
1610
|
: listGuardianChannels(assistantId);
|
|
1617
1611
|
const guardianContact = voiceGuardian?.contact ?? guardianChannels?.contact;
|
|
1618
|
-
if (guardianContact) {
|
|
1619
|
-
const meta: Record<string, string> = {};
|
|
1620
|
-
if (guardianContact.displayName) {
|
|
1621
|
-
meta.displayName = guardianContact.displayName;
|
|
1622
|
-
}
|
|
1623
|
-
// Preserve the username fallback: use the voice channel's externalUserId
|
|
1624
|
-
// so downstream parsing can fall back to @username when displayName is a
|
|
1625
|
-
// raw external ID (e.g., phone number from contact-sync).
|
|
1626
|
-
const voiceChannel =
|
|
1627
|
-
voiceGuardian?.channel ??
|
|
1628
|
-
guardianChannels?.channels.find((ch) => ch.type === "voice");
|
|
1629
|
-
if (voiceChannel?.externalUserId) {
|
|
1630
|
-
meta.username = voiceChannel.externalUserId;
|
|
1631
|
-
}
|
|
1632
|
-
if (Object.keys(meta).length > 0) {
|
|
1633
|
-
metadataJson = JSON.stringify(meta);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
if (!metadataJson) {
|
|
1637
|
-
const voiceBinding = getGuardianBinding(assistantId, "voice");
|
|
1638
|
-
if (voiceBinding?.metadataJson) {
|
|
1639
|
-
metadataJson = voiceBinding.metadataJson;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
if (metadataJson) {
|
|
1644
|
-
try {
|
|
1645
|
-
const parsed = JSON.parse(metadataJson) as Record<string, unknown>;
|
|
1646
|
-
if (
|
|
1647
|
-
typeof parsed.displayName === "string" &&
|
|
1648
|
-
parsed.displayName.trim().length > 0
|
|
1649
|
-
) {
|
|
1650
|
-
return parsed.displayName.trim();
|
|
1651
|
-
}
|
|
1652
|
-
if (
|
|
1653
|
-
typeof parsed.username === "string" &&
|
|
1654
|
-
parsed.username.trim().length > 0
|
|
1655
|
-
) {
|
|
1656
|
-
return `@${parsed.username.trim()}`;
|
|
1657
|
-
}
|
|
1658
|
-
} catch {
|
|
1659
|
-
// ignore malformed metadata
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
1612
|
|
|
1663
|
-
return
|
|
1613
|
+
return resolveGuardianName(guardianContact?.displayName);
|
|
1664
1614
|
}
|
|
1665
1615
|
|
|
1666
1616
|
/**
|
package/src/channels/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical per-channel
|
|
2
|
+
* Canonical per-channel policy registry.
|
|
3
3
|
*
|
|
4
4
|
* Every ChannelId must have an entry here. The `satisfies` constraint
|
|
5
5
|
* ensures that adding a new ChannelId to channels/types.ts will fail
|
|
@@ -13,11 +13,17 @@ export type ConversationStrategy =
|
|
|
13
13
|
| "continue_existing_conversation"
|
|
14
14
|
| "not_deliverable";
|
|
15
15
|
|
|
16
|
+
export interface ChannelInvitePolicy {
|
|
17
|
+
/** Whether inbound invite code redemption is supported on this channel. */
|
|
18
|
+
codeRedemptionEnabled: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export interface ChannelNotificationPolicy {
|
|
17
22
|
notification: {
|
|
18
23
|
deliveryEnabled: boolean;
|
|
19
24
|
conversationStrategy: ConversationStrategy;
|
|
20
25
|
};
|
|
26
|
+
invite: ChannelInvitePolicy;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
const CHANNEL_POLICIES = {
|
|
@@ -26,48 +32,69 @@ const CHANNEL_POLICIES = {
|
|
|
26
32
|
deliveryEnabled: true,
|
|
27
33
|
conversationStrategy: "start_new_conversation",
|
|
28
34
|
},
|
|
35
|
+
invite: {
|
|
36
|
+
codeRedemptionEnabled: false,
|
|
37
|
+
},
|
|
29
38
|
},
|
|
30
39
|
telegram: {
|
|
31
40
|
notification: {
|
|
32
41
|
deliveryEnabled: true,
|
|
33
42
|
conversationStrategy: "continue_existing_conversation",
|
|
34
43
|
},
|
|
44
|
+
invite: {
|
|
45
|
+
codeRedemptionEnabled: true,
|
|
46
|
+
},
|
|
35
47
|
},
|
|
36
48
|
sms: {
|
|
37
49
|
notification: {
|
|
38
50
|
deliveryEnabled: true,
|
|
39
51
|
conversationStrategy: "continue_existing_conversation",
|
|
40
52
|
},
|
|
53
|
+
invite: {
|
|
54
|
+
codeRedemptionEnabled: true,
|
|
55
|
+
},
|
|
41
56
|
},
|
|
42
57
|
whatsapp: {
|
|
43
58
|
notification: {
|
|
44
59
|
deliveryEnabled: false,
|
|
45
60
|
conversationStrategy: "continue_existing_conversation",
|
|
46
61
|
},
|
|
62
|
+
invite: {
|
|
63
|
+
codeRedemptionEnabled: false,
|
|
64
|
+
},
|
|
47
65
|
},
|
|
48
66
|
slack: {
|
|
49
67
|
notification: {
|
|
50
68
|
deliveryEnabled: true,
|
|
51
69
|
conversationStrategy: "continue_existing_conversation",
|
|
52
70
|
},
|
|
71
|
+
invite: {
|
|
72
|
+
codeRedemptionEnabled: true,
|
|
73
|
+
},
|
|
53
74
|
},
|
|
54
75
|
email: {
|
|
55
76
|
notification: {
|
|
56
77
|
deliveryEnabled: false,
|
|
57
78
|
conversationStrategy: "continue_existing_conversation",
|
|
58
79
|
},
|
|
80
|
+
invite: {
|
|
81
|
+
codeRedemptionEnabled: true,
|
|
82
|
+
},
|
|
59
83
|
},
|
|
60
84
|
voice: {
|
|
61
85
|
notification: {
|
|
62
86
|
deliveryEnabled: false,
|
|
63
87
|
conversationStrategy: "not_deliverable",
|
|
64
88
|
},
|
|
89
|
+
invite: {
|
|
90
|
+
codeRedemptionEnabled: false,
|
|
91
|
+
},
|
|
65
92
|
},
|
|
66
93
|
} as const satisfies Record<ChannelId, ChannelNotificationPolicy>;
|
|
67
94
|
|
|
68
95
|
export type ChannelPolicies = typeof CHANNEL_POLICIES;
|
|
69
96
|
|
|
70
|
-
/** Returns the full
|
|
97
|
+
/** Returns the full policy for a channel. */
|
|
71
98
|
export function getChannelPolicy(
|
|
72
99
|
channelId: ChannelId,
|
|
73
100
|
): ChannelNotificationPolicy {
|
|
@@ -97,3 +124,15 @@ export function getConversationStrategy(
|
|
|
97
124
|
): ConversationStrategy {
|
|
98
125
|
return CHANNEL_POLICIES[channelId].notification.conversationStrategy;
|
|
99
126
|
}
|
|
127
|
+
|
|
128
|
+
/** Returns the invite policy for the given channel. */
|
|
129
|
+
export function getChannelInvitePolicy(
|
|
130
|
+
channelId: ChannelId,
|
|
131
|
+
): ChannelInvitePolicy {
|
|
132
|
+
return CHANNEL_POLICIES[channelId].invite;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether invite code redemption is enabled for the given channel. */
|
|
136
|
+
export function isInviteCodeRedemptionEnabled(channelId: ChannelId): boolean {
|
|
137
|
+
return CHANNEL_POLICIES[channelId].invite.codeRedemptionEnabled;
|
|
138
|
+
}
|
|
@@ -69,6 +69,8 @@ When you need to **send** content to Slack proactively (e.g. a scheduled digest,
|
|
|
69
69
|
- `send_notification` is appropriate for short alerts and status updates where you want the router to pick the best channel. `messaging_send` is appropriate when you have specific content to deliver to a specific Slack destination.
|
|
70
70
|
- For scheduled tasks (cron/RRULE), always end with a `messaging_send` call so the results actually reach the user. Without it, the output only lives in the conversation log.
|
|
71
71
|
|
|
72
|
+
For setting up recurring digests, load the `slack-digest-setup` skill which covers the full configuration, scheduling, and delivery protocol.
|
|
73
|
+
|
|
72
74
|
## Watcher Integration
|
|
73
75
|
|
|
74
76
|
For real-time monitoring (not just on-demand scanning), the user can set up a Slack watcher using the watcher skill with the same channel IDs. Mention this if the user wants ongoing monitoring.
|