@vellumai/assistant 0.4.29 → 0.4.31
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/ARCHITECTURE.md +39 -37
- package/Dockerfile +14 -8
- package/README.md +7 -8
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +76 -43
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
- package/scripts/test.sh +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
- package/src/__tests__/actor-token-service.test.ts +4 -3
- package/src/__tests__/app-executors.test.ts +7 -17
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
- package/src/__tests__/browser-skill-endstate.test.ts +10 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +44 -44
- package/src/__tests__/channel-approval.test.ts +8 -0
- package/src/__tests__/channel-approvals.test.ts +39 -1
- package/src/__tests__/channel-guardian.test.ts +15 -5
- package/src/__tests__/channel-reply-delivery.test.ts +31 -0
- package/src/__tests__/config-schema.test.ts +0 -9
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/gemini-image-service.test.ts +2 -2
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +6 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +40 -15
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
- package/src/__tests__/integrations-cli.test.ts +3 -27
- package/src/__tests__/intent-routing.test.ts +3 -0
- package/src/__tests__/invite-redemption-service.test.ts +1 -1
- package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
- package/src/__tests__/ipc-snapshot.test.ts +4 -31
- package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
- package/src/__tests__/nl-approval-parser.test.ts +305 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
- package/src/__tests__/provider-error-scenarios.test.ts +68 -0
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/retry-after-extraction.test.ts +111 -0
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
- package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-media-retry.test.ts +147 -0
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
- package/src/__tests__/skill-feature-flags.test.ts +18 -12
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
- package/src/__tests__/slack-block-formatting.test.ts +100 -0
- package/src/__tests__/slack-inbound-verification.test.ts +346 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/starter-task-flow.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
- package/src/__tests__/trusted-contact-verification.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +1 -1
- package/src/amazon/client.ts +7 -24
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/relay-server.ts +44 -11
- package/src/channels/config.ts +1 -1
- package/src/cli/integrations.ts +10 -66
- package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
- package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
- package/src/config/bundled-skills/browser/TOOLS.json +59 -2
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
- package/src/config/bundled-skills/contacts/SKILL.md +49 -53
- package/src/config/bundled-skills/contacts/TOOLS.json +26 -22
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +40 -62
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +17 -43
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +18 -57
- package/src/config/bundled-skills/document/TOOLS.json +8 -0
- package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
- package/src/config/bundled-skills/followups/TOOLS.json +12 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
- package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
- package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
- package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
- package/src/config/bundled-skills/notifications/SKILL.md +3 -2
- package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
- package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
- package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
- package/src/config/bundled-skills/schedule/SKILL.md +33 -15
- package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
- package/src/config/bundled-skills/slack/SKILL.md +30 -1
- package/src/config/bundled-skills/slack/TOOLS.json +89 -2
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/channel-permission-profiles.ts +155 -0
- package/src/config/env.ts +4 -1
- package/src/config/memory-schema.ts +0 -10
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +221 -56
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +35 -4
- package/src/daemon/assistant-attachments.ts +23 -3
- package/src/daemon/guardian-verification-intent.ts +7 -4
- package/src/daemon/handlers/apps.ts +1 -2
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/config-inbox.ts +16 -134
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +21 -88
- package/src/daemon/handlers/sessions.ts +2 -2
- package/src/daemon/ipc-contract/apps.ts +0 -1
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/ipc-contract/inbox.ts +7 -66
- package/src/daemon/ipc-contract/sessions.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/session-agent-loop-handlers.ts +9 -0
- package/src/daemon/session-agent-loop.ts +2 -45
- package/src/daemon/session-attachments.ts +5 -1
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-error.ts +18 -0
- package/src/daemon/session-lifecycle.ts +4 -5
- package/src/daemon/session-media-retry.ts +15 -1
- package/src/daemon/session-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- package/src/daemon/session-surfaces.ts +0 -1
- package/src/daemon/session-tool-setup.ts +7 -4
- package/src/events/domain-events.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/home-base/prebuilt/seed.ts +0 -1
- package/src/influencer/client.ts +7 -24
- package/src/media/gemini-image-service.ts +48 -3
- package/src/memory/app-store.ts +0 -4
- package/src/memory/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/conversation-attention-store.ts +3 -1
- package/src/memory/db-init.ts +8 -0
- package/src/memory/job-handlers/conflict.ts +0 -7
- package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +12 -17
- package/src/memory/slack-thread-store.ts +187 -0
- package/src/messaging/index.ts +0 -1
- package/src/messaging/providers/slack/client.ts +84 -26
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/messaging/types.ts +0 -38
- package/src/notifications/adapters/slack.ts +90 -0
- package/src/notifications/destination-resolver.ts +42 -1
- package/src/notifications/emit-signal.ts +17 -1
- package/src/oauth/provider-profiles.ts +22 -0
- package/src/providers/anthropic/client.ts +3 -0
- package/src/providers/openai/client.ts +3 -0
- package/src/providers/retry.ts +9 -1
- package/src/runtime/actor-trust-resolver.ts +8 -0
- package/src/runtime/auth/require-bound-guardian.ts +44 -0
- package/src/runtime/auth/route-policy.ts +4 -8
- package/src/runtime/channel-approval-types.ts +18 -0
- package/src/runtime/channel-approvals.ts +8 -0
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +62 -3
- package/src/runtime/gateway-client.ts +36 -2
- package/src/runtime/gateway-internal-client.ts +86 -0
- package/src/runtime/guardian-action-service.ts +128 -0
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/guardian-verification-templates.ts +16 -1
- package/src/runtime/http-server.ts +29 -46
- package/src/runtime/invite-redemption-service.ts +1 -1
- package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
- package/src/runtime/nl-approval-parser.ts +138 -0
- package/src/runtime/routes/approval-routes.ts +1 -40
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/channel-route-shared.ts +35 -1
- package/src/runtime/routes/contact-routes.ts +494 -47
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/global-search-routes.ts +2 -2
- package/src/runtime/routes/guardian-action-routes.ts +19 -111
- package/src/runtime/routes/guardian-approval-interception.ts +78 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
- package/src/runtime/routes/inbound-message-handler.ts +40 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +227 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
- package/src/runtime/routes/migration-routes.ts +17 -17
- package/src/runtime/slack-block-formatting.ts +176 -0
- package/src/schedule/scheduler.ts +11 -2
- package/src/tools/apps/executors.ts +16 -15
- package/src/tools/calls/call-end.ts +1 -1
- package/src/tools/computer-use/definitions.ts +16 -0
- package/src/tools/credentials/vault.ts +86 -2
- package/src/tools/network/script-proxy/session-manager.ts +28 -3
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/terminal/shell.ts +15 -5
- package/src/tools/tool-approval-handler.ts +48 -4
- package/src/tools/types.ts +38 -1
- package/src/util/errors.ts +5 -1
- package/src/util/retry.ts +21 -0
- package/src/watcher/providers/slack.ts +33 -3
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -32
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
- /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Slack inbound trusted contact verification.
|
|
3
|
+
*
|
|
4
|
+
* When an unknown Slack user messages the bot, the system should:
|
|
5
|
+
* 1. Create an outbound verification session bound to the user's identity
|
|
6
|
+
* 2. Send the verification code to the user's DM via the gateway
|
|
7
|
+
* 3. Reply in the original channel telling the user to check their DMs
|
|
8
|
+
* 4. Notify the guardian of the access attempt
|
|
9
|
+
* 5. When the user replies with the code in the DM, verify and activate
|
|
10
|
+
*/
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Test isolation: in-memory SQLite via temp directory
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const testDir = mkdtempSync(join(tmpdir(), "slack-inbound-verification-test-"));
|
|
21
|
+
|
|
22
|
+
mock.module("../util/platform.js", () => ({
|
|
23
|
+
getRootDir: () => testDir,
|
|
24
|
+
getDataDir: () => testDir,
|
|
25
|
+
isMacOS: () => process.platform === "darwin",
|
|
26
|
+
isLinux: () => process.platform === "linux",
|
|
27
|
+
isWindows: () => process.platform === "win32",
|
|
28
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
29
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
30
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
31
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
32
|
+
ensureDataDir: () => {},
|
|
33
|
+
readHttpToken: () => "test-bearer-token",
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
mock.module("../util/logger.js", () => ({
|
|
37
|
+
getLogger: () =>
|
|
38
|
+
new Proxy({} as Record<string, unknown>, {
|
|
39
|
+
get: () => () => {},
|
|
40
|
+
}),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../security/secret-ingress.js", () => ({
|
|
44
|
+
checkIngressForSecrets: () => ({ blocked: false }),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
mock.module("../config/env.js", () => ({
|
|
48
|
+
isHttpAuthDisabled: () => true,
|
|
49
|
+
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Track emitNotificationSignal calls
|
|
53
|
+
const emitSignalCalls: Array<Record<string, unknown>> = [];
|
|
54
|
+
mock.module("../notifications/emit-signal.js", () => ({
|
|
55
|
+
emitNotificationSignal: async (params: Record<string, unknown>) => {
|
|
56
|
+
emitSignalCalls.push(params);
|
|
57
|
+
return {
|
|
58
|
+
signalId: "mock-signal-id",
|
|
59
|
+
deduplicated: false,
|
|
60
|
+
dispatched: true,
|
|
61
|
+
reason: "mock",
|
|
62
|
+
deliveryResults: [],
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// Track deliverChannelReply calls
|
|
68
|
+
const deliverReplyCalls: Array<{
|
|
69
|
+
url: string;
|
|
70
|
+
payload: Record<string, unknown>;
|
|
71
|
+
}> = [];
|
|
72
|
+
mock.module("../runtime/gateway-client.js", () => ({
|
|
73
|
+
deliverChannelReply: async (
|
|
74
|
+
url: string,
|
|
75
|
+
payload: Record<string, unknown>,
|
|
76
|
+
) => {
|
|
77
|
+
deliverReplyCalls.push({ url, payload });
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
import { createGuardianBinding } from "../contacts/contacts-write.js";
|
|
82
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
83
|
+
import { findActiveSession } from "../runtime/channel-guardian-service.js";
|
|
84
|
+
import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
|
|
85
|
+
|
|
86
|
+
initializeDb();
|
|
87
|
+
|
|
88
|
+
afterAll(() => {
|
|
89
|
+
resetDb();
|
|
90
|
+
try {
|
|
91
|
+
rmSync(testDir, { recursive: true });
|
|
92
|
+
} catch {
|
|
93
|
+
/* best effort */
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Helpers
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
const TEST_BEARER_TOKEN = "test-token";
|
|
102
|
+
|
|
103
|
+
function resetState(): void {
|
|
104
|
+
const db = getDb();
|
|
105
|
+
db.run("DELETE FROM channel_guardian_approval_requests");
|
|
106
|
+
db.run("DELETE FROM channel_guardian_verification_challenges");
|
|
107
|
+
db.run("DELETE FROM channel_guardian_rate_limits");
|
|
108
|
+
db.run("DELETE FROM channel_inbound_events");
|
|
109
|
+
db.run("DELETE FROM conversations");
|
|
110
|
+
db.run("DELETE FROM notification_events");
|
|
111
|
+
db.run("DELETE FROM canonical_guardian_requests");
|
|
112
|
+
db.run("DELETE FROM canonical_guardian_deliveries");
|
|
113
|
+
db.run("DELETE FROM contact_channels");
|
|
114
|
+
db.run("DELETE FROM contacts");
|
|
115
|
+
emitSignalCalls.length = 0;
|
|
116
|
+
deliverReplyCalls.length = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildSlackInboundRequest(
|
|
120
|
+
overrides: Record<string, unknown> = {},
|
|
121
|
+
): Request {
|
|
122
|
+
const body: Record<string, unknown> = {
|
|
123
|
+
sourceChannel: "slack",
|
|
124
|
+
interface: "slack",
|
|
125
|
+
conversationExternalId: "C0123CHANNEL",
|
|
126
|
+
externalMessageId: `msg-${Date.now()}-${Math.random()
|
|
127
|
+
.toString(36)
|
|
128
|
+
.slice(2, 8)}`,
|
|
129
|
+
content: "Hello, can I use this assistant?",
|
|
130
|
+
actorExternalId: "U0123UNKNOWN",
|
|
131
|
+
actorDisplayName: "Alice Unknown",
|
|
132
|
+
actorUsername: "alice_unknown",
|
|
133
|
+
replyCallbackUrl: "http://localhost:7830/deliver/slack",
|
|
134
|
+
...overrides,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return new Request("http://localhost:8080/channels/inbound", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify(body),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Tests
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe("Slack inbound trusted contact verification", () => {
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
resetState();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("unknown Slack user receives verification challenge via DM", async () => {
|
|
157
|
+
const req = buildSlackInboundRequest();
|
|
158
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
159
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
160
|
+
|
|
161
|
+
expect(json.denied).toBe(true);
|
|
162
|
+
expect(json.reason).toBe("verification_challenge_sent");
|
|
163
|
+
expect(json.verificationSessionId).toBeDefined();
|
|
164
|
+
|
|
165
|
+
// Verification code is NOT sent to the requester — only the guardian
|
|
166
|
+
// receives it via the access request notification flow
|
|
167
|
+
|
|
168
|
+
// Channel reply tells user the owner has been notified
|
|
169
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
170
|
+
expect(
|
|
171
|
+
(deliverReplyCalls[0].payload as Record<string, unknown>).text,
|
|
172
|
+
).toContain("notified the owner");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("verification session is identity-bound to the Slack user", async () => {
|
|
176
|
+
const req = buildSlackInboundRequest();
|
|
177
|
+
await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
178
|
+
|
|
179
|
+
// An active outbound session should exist for the slack channel
|
|
180
|
+
const session = findActiveSession("self", "slack");
|
|
181
|
+
expect(session).not.toBeNull();
|
|
182
|
+
expect(session!.expectedExternalUserId).toBe("U0123UNKNOWN");
|
|
183
|
+
expect(session!.expectedChatId).toBe("U0123UNKNOWN");
|
|
184
|
+
expect(session!.identityBindingStatus).toBe("bound");
|
|
185
|
+
expect(session!.verificationPurpose).toBe("trusted_contact");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("guardian is notified of the access attempt alongside verification", async () => {
|
|
189
|
+
// Set up a guardian binding so the notification can target it
|
|
190
|
+
createGuardianBinding({
|
|
191
|
+
assistantId: "self",
|
|
192
|
+
channel: "slack",
|
|
193
|
+
guardianExternalUserId: "U_GUARDIAN",
|
|
194
|
+
guardianDeliveryChatId: "D_GUARDIAN_DM",
|
|
195
|
+
guardianPrincipalId: "guardian-principal",
|
|
196
|
+
verifiedVia: "test",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const req = buildSlackInboundRequest();
|
|
200
|
+
await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
201
|
+
|
|
202
|
+
// Guardian should have been notified
|
|
203
|
+
expect(emitSignalCalls.length).toBe(1);
|
|
204
|
+
expect(emitSignalCalls[0].sourceEventName).toBe("ingress.access_request");
|
|
205
|
+
expect(emitSignalCalls[0].sourceChannel).toBe("slack");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("duplicate challenge is not sent when session already exists", async () => {
|
|
209
|
+
// First message creates the session
|
|
210
|
+
const req1 = buildSlackInboundRequest();
|
|
211
|
+
const resp1 = await handleChannelInbound(
|
|
212
|
+
req1,
|
|
213
|
+
undefined,
|
|
214
|
+
TEST_BEARER_TOKEN,
|
|
215
|
+
);
|
|
216
|
+
const json1 = (await resp1.json()) as Record<string, unknown>;
|
|
217
|
+
expect(json1.reason).toBe("verification_challenge_sent");
|
|
218
|
+
|
|
219
|
+
// Second message from the same user — session already exists, so
|
|
220
|
+
// falls through to standard deny path
|
|
221
|
+
const req2 = buildSlackInboundRequest({
|
|
222
|
+
externalMessageId: `msg-${Date.now()}-second`,
|
|
223
|
+
});
|
|
224
|
+
const resp2 = await handleChannelInbound(
|
|
225
|
+
req2,
|
|
226
|
+
undefined,
|
|
227
|
+
TEST_BEARER_TOKEN,
|
|
228
|
+
);
|
|
229
|
+
const json2 = (await resp2.json()) as Record<string, unknown>;
|
|
230
|
+
expect(json2.denied).toBe(true);
|
|
231
|
+
expect(json2.reason).toBe("not_a_member");
|
|
232
|
+
|
|
233
|
+
// No DM was sent at all
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("different Slack user is not suppressed by existing session for another user", async () => {
|
|
237
|
+
// First message from user A creates a session
|
|
238
|
+
const req1 = buildSlackInboundRequest({
|
|
239
|
+
actorExternalId: "U_USER_A",
|
|
240
|
+
actorDisplayName: "User A",
|
|
241
|
+
});
|
|
242
|
+
const resp1 = await handleChannelInbound(
|
|
243
|
+
req1,
|
|
244
|
+
undefined,
|
|
245
|
+
TEST_BEARER_TOKEN,
|
|
246
|
+
);
|
|
247
|
+
const json1 = (await resp1.json()) as Record<string, unknown>;
|
|
248
|
+
expect(json1.reason).toBe("verification_challenge_sent");
|
|
249
|
+
|
|
250
|
+
// Second message from user B — should get their own challenge
|
|
251
|
+
const req2 = buildSlackInboundRequest({
|
|
252
|
+
actorExternalId: "U_USER_B",
|
|
253
|
+
actorDisplayName: "User B",
|
|
254
|
+
externalMessageId: `msg-${Date.now()}-user-b`,
|
|
255
|
+
});
|
|
256
|
+
const resp2 = await handleChannelInbound(
|
|
257
|
+
req2,
|
|
258
|
+
undefined,
|
|
259
|
+
TEST_BEARER_TOKEN,
|
|
260
|
+
);
|
|
261
|
+
const json2 = (await resp2.json()) as Record<string, unknown>;
|
|
262
|
+
expect(json2.reason).toBe("verification_challenge_sent");
|
|
263
|
+
expect(json2.verificationSessionId).toBeDefined();
|
|
264
|
+
|
|
265
|
+
// No DMs sent to requesters — guardian gets code via notification flow
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("non-Slack channels still use standard access request flow", async () => {
|
|
269
|
+
const req = buildSlackInboundRequest({
|
|
270
|
+
sourceChannel: "telegram",
|
|
271
|
+
interface: "telegram",
|
|
272
|
+
replyCallbackUrl: "http://localhost:7830/deliver/telegram",
|
|
273
|
+
});
|
|
274
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
275
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
276
|
+
|
|
277
|
+
// Standard deny path — no verification challenge
|
|
278
|
+
expect(json.denied).toBe(true);
|
|
279
|
+
expect(json.reason).toBe("not_a_member");
|
|
280
|
+
|
|
281
|
+
// No Slack DM was sent
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("user can verify by replying with the code in the DM", async () => {
|
|
285
|
+
// Step 1: Unknown user sends a message, gets verification challenge
|
|
286
|
+
const req = buildSlackInboundRequest();
|
|
287
|
+
await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
288
|
+
|
|
289
|
+
const session = findActiveSession("self", "slack");
|
|
290
|
+
expect(session).not.toBeNull();
|
|
291
|
+
|
|
292
|
+
// The challenge hash is stored in the session — extract the secret
|
|
293
|
+
// from the DM text sent to the user. The code is embedded in the
|
|
294
|
+
// template text. Since we're using a 6-digit code for identity-bound
|
|
295
|
+
// sessions, extract it from the session's challengeHash by consuming
|
|
296
|
+
// the challenge directly.
|
|
297
|
+
// The session was created with createOutboundSession which generates
|
|
298
|
+
// a 6-digit code. We can validate by calling validateAndConsumeChallenge
|
|
299
|
+
// with the correct secret. Since the mock captures the DM text, we
|
|
300
|
+
// can extract the code indirectly. But for testing, we just verify
|
|
301
|
+
// the session properties and that validateAndConsumeChallenge works
|
|
302
|
+
// with the correct identity.
|
|
303
|
+
|
|
304
|
+
// The actual secret was sent in the DM. For this test, let's use the
|
|
305
|
+
// session directly via the channel-guardian-service to verify the
|
|
306
|
+
// consume path works.
|
|
307
|
+
// The DM text contains the verification code implicitly (it's in the
|
|
308
|
+
// template message). Since we need to test the full round-trip, let's
|
|
309
|
+
// verify via the inbound handler by sending the code as a message.
|
|
310
|
+
|
|
311
|
+
// Extract the session's challenge hash and verify that submitting the
|
|
312
|
+
// correct code works. We create a fresh session with a known secret for
|
|
313
|
+
// this part of the test.
|
|
314
|
+
resetState();
|
|
315
|
+
|
|
316
|
+
// Create a verification session manually to test the consume path
|
|
317
|
+
const { createOutboundSession } =
|
|
318
|
+
await import("../runtime/channel-guardian-service.js");
|
|
319
|
+
|
|
320
|
+
const outboundSession = createOutboundSession({
|
|
321
|
+
assistantId: "self",
|
|
322
|
+
channel: "slack",
|
|
323
|
+
expectedExternalUserId: "U0123UNKNOWN",
|
|
324
|
+
expectedChatId: "U0123UNKNOWN",
|
|
325
|
+
identityBindingStatus: "bound",
|
|
326
|
+
destinationAddress: "U0123UNKNOWN",
|
|
327
|
+
verificationPurpose: "trusted_contact",
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// User replies with the code in the DM
|
|
331
|
+
const verifyReq = buildSlackInboundRequest({
|
|
332
|
+
conversationExternalId: "U0123UNKNOWN",
|
|
333
|
+
content: outboundSession.secret,
|
|
334
|
+
externalMessageId: `msg-verify-${Date.now()}`,
|
|
335
|
+
});
|
|
336
|
+
const verifyResp = await handleChannelInbound(
|
|
337
|
+
verifyReq,
|
|
338
|
+
undefined,
|
|
339
|
+
TEST_BEARER_TOKEN,
|
|
340
|
+
);
|
|
341
|
+
const verifyJson = (await verifyResp.json()) as Record<string, unknown>;
|
|
342
|
+
|
|
343
|
+
expect(verifyJson.accepted).toBe(true);
|
|
344
|
+
expect(verifyJson.guardianVerification).toBe("verified");
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { parseReactionCallbackData } from "../runtime/routes/channel-route-shared.js";
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// parseReactionCallbackData
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
describe("parseReactionCallbackData", () => {
|
|
10
|
+
test("maps +1 emoji to approve_once", () => {
|
|
11
|
+
const result = parseReactionCallbackData("reaction:+1");
|
|
12
|
+
expect(result).toEqual({
|
|
13
|
+
action: "approve_once",
|
|
14
|
+
source: "slack_reaction",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("maps thumbsup emoji to approve_once", () => {
|
|
19
|
+
const result = parseReactionCallbackData("reaction:thumbsup");
|
|
20
|
+
expect(result).toEqual({
|
|
21
|
+
action: "approve_once",
|
|
22
|
+
source: "slack_reaction",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("maps -1 emoji to reject", () => {
|
|
27
|
+
const result = parseReactionCallbackData("reaction:-1");
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
action: "reject",
|
|
30
|
+
source: "slack_reaction",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("maps thumbsdown emoji to reject", () => {
|
|
35
|
+
const result = parseReactionCallbackData("reaction:thumbsdown");
|
|
36
|
+
expect(result).toEqual({
|
|
37
|
+
action: "reject",
|
|
38
|
+
source: "slack_reaction",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("maps alarm_clock emoji to approve_10m", () => {
|
|
43
|
+
const result = parseReactionCallbackData("reaction:alarm_clock");
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
action: "approve_10m",
|
|
46
|
+
source: "slack_reaction",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("maps white_check_mark emoji to approve_always", () => {
|
|
51
|
+
const result = parseReactionCallbackData("reaction:white_check_mark");
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
action: "approve_always",
|
|
54
|
+
source: "slack_reaction",
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("returns null for unknown emoji", () => {
|
|
59
|
+
const result = parseReactionCallbackData("reaction:tada");
|
|
60
|
+
expect(result).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("returns null for empty emoji name", () => {
|
|
64
|
+
const result = parseReactionCallbackData("reaction:");
|
|
65
|
+
expect(result).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns null for non-reaction callback data", () => {
|
|
69
|
+
const result = parseReactionCallbackData("apr:req-1:approve_once");
|
|
70
|
+
expect(result).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("returns null for plain text", () => {
|
|
74
|
+
const result = parseReactionCallbackData("yes");
|
|
75
|
+
expect(result).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -64,10 +64,11 @@ describe("slack skill TOOLS.json", () => {
|
|
|
64
64
|
expect(names).toContain("slack_edit_message");
|
|
65
65
|
expect(names).toContain("slack_delete_message");
|
|
66
66
|
expect(names).toContain("slack_leave_channel");
|
|
67
|
+
expect(names).toContain("slack_channel_permissions");
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
test("has
|
|
70
|
-
expect(toolsJson.tools.length).toBe(
|
|
70
|
+
test("has 8 tools total", () => {
|
|
71
|
+
expect(toolsJson.tools.length).toBe(8);
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
test("all tools have required fields", () => {
|
|
@@ -177,7 +177,8 @@ function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
|
177
177
|
|
|
178
178
|
function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
179
179
|
return {
|
|
180
|
-
|
|
180
|
+
actorPrincipalId: "test-principal-id",
|
|
181
|
+
actorExternalUserId: "guardian-1",
|
|
181
182
|
channel: "telegram",
|
|
182
183
|
guardianPrincipalId: "test-principal-id",
|
|
183
184
|
...overrides,
|
|
@@ -237,7 +237,8 @@ function makeToolContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
|
237
237
|
|
|
238
238
|
function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
239
239
|
return {
|
|
240
|
-
|
|
240
|
+
actorPrincipalId: "test-principal-id",
|
|
241
|
+
actorExternalUserId: "guardian-1",
|
|
241
242
|
channel: "telegram",
|
|
242
243
|
guardianPrincipalId: "test-principal-id",
|
|
243
244
|
...overrides,
|
|
@@ -449,7 +449,9 @@ describe("trusted contact verification → member activation", () => {
|
|
|
449
449
|
);
|
|
450
450
|
|
|
451
451
|
expect(result.success).toBe(true);
|
|
452
|
-
|
|
452
|
+
if (result.success) {
|
|
453
|
+
expect(result.verificationType).toBe("guardian");
|
|
454
|
+
}
|
|
453
455
|
|
|
454
456
|
const guardianResult = findGuardianForChannel("telegram", "self");
|
|
455
457
|
expect(guardianResult).toBeNull();
|
|
@@ -26,7 +26,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
26
26
|
|
|
27
27
|
import { upsertMember } from "../contacts/contacts-write.js";
|
|
28
28
|
import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
|
|
29
|
-
import { createInvite, revokeInvite } from "../memory/
|
|
29
|
+
import { createInvite, revokeInvite } from "../memory/invite-store.js";
|
|
30
30
|
import { redeemVoiceInviteCode } from "../runtime/invite-redemption-service.js";
|
|
31
31
|
import { generateVoiceCode, hashVoiceCode } from "../util/voice-code.js";
|
|
32
32
|
|
package/src/amazon/client.ts
CHANGED
|
@@ -53,11 +53,8 @@ import type {
|
|
|
53
53
|
ExtensionResponse,
|
|
54
54
|
} from "../browser-extension-relay/protocol.js";
|
|
55
55
|
import { extensionRelayServer } from "../browser-extension-relay/server.js";
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
isSigningKeyInitialized,
|
|
59
|
-
mintEdgeRelayToken,
|
|
60
|
-
} from "../runtime/auth/token-service.js";
|
|
56
|
+
import { isSigningKeyInitialized } from "../runtime/auth/token-service.js";
|
|
57
|
+
import { gatewayPost } from "../runtime/gateway-internal-client.js";
|
|
61
58
|
import type { ExtractedCredential } from "../tools/browser/network-recording-types.js";
|
|
62
59
|
import { type AmazonSession, loadSession } from "./session.js";
|
|
63
60
|
|
|
@@ -91,26 +88,12 @@ export async function sendRelayCommand(
|
|
|
91
88
|
"Auth signing key not initialized — browser-relay commands require the daemon to be running",
|
|
92
89
|
);
|
|
93
90
|
}
|
|
94
|
-
const token = mintEdgeRelayToken();
|
|
95
|
-
|
|
96
|
-
const resp = await fetch(
|
|
97
|
-
`${getGatewayInternalBaseUrl()}/v1/browser-relay/command`,
|
|
98
|
-
{
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: {
|
|
101
|
-
"Content-Type": "application/json",
|
|
102
|
-
Authorization: `Bearer ${token}`,
|
|
103
|
-
},
|
|
104
|
-
body: JSON.stringify(command),
|
|
105
|
-
},
|
|
106
|
-
);
|
|
107
91
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return (await resp.json()) as ExtensionResponse;
|
|
92
|
+
const { data } = await gatewayPost<ExtensionResponse>(
|
|
93
|
+
"/v1/browser-relay/command",
|
|
94
|
+
command,
|
|
95
|
+
);
|
|
96
|
+
return data;
|
|
114
97
|
}
|
|
115
98
|
|
|
116
99
|
/** Thrown when the session is missing or expired. The CLI handles this specially. */
|
|
@@ -150,7 +150,9 @@ export interface ApplyGuardianDecisionParams {
|
|
|
150
150
|
approval: GuardianApprovalRequest;
|
|
151
151
|
/** The parsed decision (action + source + optional requestId). */
|
|
152
152
|
decision: ApprovalDecisionResult;
|
|
153
|
-
/**
|
|
153
|
+
/** Principal ID of the actor making the decision (undefined in callback/interception paths without JWT/auth context). */
|
|
154
|
+
actorPrincipalId: string | undefined;
|
|
155
|
+
/** Channel-native external user ID of the deciding actor (Telegram user ID, phone, etc.). */
|
|
154
156
|
actorExternalUserId: string | undefined;
|
|
155
157
|
/** Channel the decision arrived on. */
|
|
156
158
|
actorChannel: ChannelId;
|
|
@@ -180,6 +182,7 @@ export function applyGuardianDecision(
|
|
|
180
182
|
const {
|
|
181
183
|
approval,
|
|
182
184
|
decision,
|
|
185
|
+
actorPrincipalId,
|
|
183
186
|
actorExternalUserId,
|
|
184
187
|
actorChannel,
|
|
185
188
|
decisionContext,
|
|
@@ -222,22 +225,23 @@ export function applyGuardianDecision(
|
|
|
222
225
|
: ("approved" as const);
|
|
223
226
|
updateApprovalDecision(approval.id, {
|
|
224
227
|
status: approvalStatus,
|
|
225
|
-
decidedByExternalUserId: actorExternalUserId,
|
|
228
|
+
decidedByExternalUserId: actorExternalUserId ?? actorPrincipalId,
|
|
226
229
|
});
|
|
227
230
|
|
|
228
231
|
// Mint a scoped grant when a guardian approves a tool-approval request.
|
|
229
|
-
// Skip when
|
|
232
|
+
// Skip when neither actor identity is available -- minting a grant without
|
|
230
233
|
// a known guardian identity is meaningless (e.g. requester self-cancel).
|
|
234
|
+
const effectiveGuardianId = actorExternalUserId ?? actorPrincipalId;
|
|
231
235
|
if (
|
|
232
236
|
effectiveDecision.action !== "reject" &&
|
|
233
237
|
matchedInfo &&
|
|
234
|
-
|
|
238
|
+
effectiveGuardianId
|
|
235
239
|
) {
|
|
236
240
|
tryMintToolApprovalGrant({
|
|
237
241
|
approvalInfo: matchedInfo,
|
|
238
242
|
approval,
|
|
239
243
|
decisionChannel: actorChannel,
|
|
240
|
-
guardianExternalUserId:
|
|
244
|
+
guardianExternalUserId: effectiveGuardianId,
|
|
241
245
|
});
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -507,7 +511,7 @@ export async function applyCanonicalGuardianDecision(
|
|
|
507
511
|
const resolved = resolveCanonicalGuardianRequest(requestId, "pending", {
|
|
508
512
|
status: targetStatus,
|
|
509
513
|
answerText: userText,
|
|
510
|
-
decidedByExternalUserId: actorContext.
|
|
514
|
+
decidedByExternalUserId: actorContext.actorExternalUserId,
|
|
511
515
|
decidedByPrincipalId: actorContext.guardianPrincipalId,
|
|
512
516
|
});
|
|
513
517
|
|
|
@@ -574,7 +578,7 @@ export async function applyCanonicalGuardianDecision(
|
|
|
574
578
|
request: resolved,
|
|
575
579
|
actorChannel: actorContext.channel,
|
|
576
580
|
guardianExternalUserId:
|
|
577
|
-
actorContext.
|
|
581
|
+
actorContext.actorExternalUserId ??
|
|
578
582
|
resolved.guardianExternalUserId ??
|
|
579
583
|
undefined,
|
|
580
584
|
});
|
|
@@ -42,8 +42,10 @@ const log = getLogger("guardian-request-resolvers");
|
|
|
42
42
|
|
|
43
43
|
/** Actor context for the entity making the decision. */
|
|
44
44
|
export interface ActorContext {
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/** Auth-identity principal ID of the deciding actor (undefined for callback-only actors). */
|
|
46
|
+
actorPrincipalId: string | undefined;
|
|
47
|
+
/** Channel-native external user ID (Telegram user ID, E.164 phone, etc.) of the deciding actor (undefined for desktop actors). Maps to `decided_by_external_user_id` DB column. */
|
|
48
|
+
actorExternalUserId: string | undefined;
|
|
47
49
|
/** Channel the decision arrived on. */
|
|
48
50
|
channel: string;
|
|
49
51
|
/** Principal ID for authorization — must match the request's guardianPrincipalId. */
|
|
@@ -349,7 +351,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
349
351
|
request.requesterChatId ?? request.requesterExternalUserId ?? "";
|
|
350
352
|
const requesterLabel =
|
|
351
353
|
requesterExternalUserId || requesterChatId || "the requester";
|
|
352
|
-
const decidedByExternalUserId = ctx.actor.
|
|
354
|
+
const decidedByExternalUserId = ctx.actor.actorExternalUserId ?? "";
|
|
353
355
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
354
356
|
const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
|
|
355
357
|
const desktopBearerToken = mintDaemonDeliveryToken();
|