@vellumai/assistant 0.4.35 → 0.4.37
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +814 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +494 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +134 -245
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +175 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram channel invite adapter.
|
|
3
3
|
*
|
|
4
|
-
* Builds `https://t.me/<botUsername>?start=iv_<token>` deep links
|
|
5
|
-
* extracts invite tokens from `/start iv_<token>` command payloads
|
|
4
|
+
* Builds `https://t.me/<botUsername>?start=iv_<token>` deep links,
|
|
5
|
+
* extracts invite tokens from `/start iv_<token>` command payloads,
|
|
6
|
+
* and resolves the bot's channel handle for invite instructions.
|
|
6
7
|
*
|
|
7
8
|
* The `iv_` prefix distinguishes invite tokens from `gv_` (guardian
|
|
8
9
|
* verification) tokens that use the same `/start` deep-link mechanism.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { ChannelId } from "../../channels/types.js";
|
|
12
|
-
import {
|
|
13
|
+
import { getSecureKey } from "../../security/secure-keys.js";
|
|
14
|
+
import {
|
|
15
|
+
getCredentialMetadata,
|
|
16
|
+
upsertCredentialMetadata,
|
|
17
|
+
} from "../../tools/credentials/metadata-store.js";
|
|
18
|
+
import { getLogger } from "../../util/logger.js";
|
|
13
19
|
import type {
|
|
14
20
|
ChannelInviteAdapter,
|
|
15
|
-
GuardianInstruction,
|
|
16
21
|
InviteShareLink,
|
|
17
22
|
} from "../channel-invite-transport.js";
|
|
18
23
|
|
|
@@ -37,6 +42,66 @@ function getTelegramBotUsername(): string | undefined {
|
|
|
37
42
|
return process.env.TELEGRAM_BOT_USERNAME || undefined;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Ensure the Telegram bot username is resolved and cached in credential
|
|
47
|
+
* metadata. When the bot token was configured via CLI `credential set`,
|
|
48
|
+
* `credential_store` tool, or ingress secret redirect, the `getMe` API
|
|
49
|
+
* call that populates `accountInfo` is skipped — this function fills that
|
|
50
|
+
* gap so that invite share links can be generated.
|
|
51
|
+
*/
|
|
52
|
+
export async function ensureTelegramBotUsernameResolved(): Promise<void> {
|
|
53
|
+
const meta = getCredentialMetadata("telegram", "bot_token");
|
|
54
|
+
if (
|
|
55
|
+
meta?.accountInfo &&
|
|
56
|
+
typeof meta.accountInfo === "string" &&
|
|
57
|
+
meta.accountInfo.trim().length > 0
|
|
58
|
+
) {
|
|
59
|
+
return; // Username already cached
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const token = getSecureKey("credential:telegram:bot_token");
|
|
63
|
+
if (!token) return;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
68
|
+
let res: Response;
|
|
69
|
+
try {
|
|
70
|
+
res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
});
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
}
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
getLogger("telegram-invite").warn(
|
|
78
|
+
"Failed to resolve Telegram bot username: HTTP %d",
|
|
79
|
+
res.status,
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const body = (await res.json()) as {
|
|
84
|
+
ok: boolean;
|
|
85
|
+
result?: { username?: string };
|
|
86
|
+
};
|
|
87
|
+
const username = body.result?.username;
|
|
88
|
+
if (!username) {
|
|
89
|
+
getLogger("telegram-invite").warn(
|
|
90
|
+
"Telegram getMe response did not include a username",
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
upsertCredentialMetadata("telegram", "bot_token", {
|
|
95
|
+
accountInfo: username,
|
|
96
|
+
});
|
|
97
|
+
} catch (err) {
|
|
98
|
+
getLogger("telegram-invite").warn(
|
|
99
|
+
{ err },
|
|
100
|
+
"Failed to resolve Telegram bot username via getMe API",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
40
105
|
// ---------------------------------------------------------------------------
|
|
41
106
|
// Token prefix
|
|
42
107
|
// ---------------------------------------------------------------------------
|
|
@@ -68,23 +133,6 @@ export const telegramInviteAdapter: ChannelInviteAdapter = {
|
|
|
68
133
|
};
|
|
69
134
|
},
|
|
70
135
|
|
|
71
|
-
buildGuardianInstruction(params: {
|
|
72
|
-
inviteCode: string;
|
|
73
|
-
contactName?: string;
|
|
74
|
-
}): GuardianInstruction {
|
|
75
|
-
const botUsername = getTelegramBotUsername();
|
|
76
|
-
const contactLabel = params.contactName || "the contact";
|
|
77
|
-
if (!botUsername) {
|
|
78
|
-
return {
|
|
79
|
-
instruction: `Tell ${contactLabel} to message the assistant on Telegram and provide the code ${params.inviteCode}.`,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
instruction: `Tell ${contactLabel} to message @${botUsername} on Telegram and provide the code ${params.inviteCode}.`,
|
|
84
|
-
channelHandle: `@${botUsername}`,
|
|
85
|
-
};
|
|
86
|
-
},
|
|
87
|
-
|
|
88
136
|
resolveChannelHandle(): string | undefined {
|
|
89
137
|
const botUsername = getTelegramBotUsername();
|
|
90
138
|
if (!botUsername) return undefined;
|
|
@@ -126,10 +174,3 @@ export const telegramInviteAdapter: ChannelInviteAdapter = {
|
|
|
126
174
|
return undefined;
|
|
127
175
|
},
|
|
128
176
|
};
|
|
129
|
-
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// Backward-compatible alias
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
|
|
134
|
-
/** @deprecated Use `telegramInviteAdapter` instead. */
|
|
135
|
-
export const telegramInviteTransport = telegramInviteAdapter;
|
|
@@ -49,10 +49,3 @@ export const voiceInviteAdapter: ChannelInviteAdapter = {
|
|
|
49
49
|
return undefined;
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Backward-compatible alias
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
/** @deprecated Use `voiceInviteAdapter` instead. */
|
|
58
|
-
export const voiceInviteTransport = voiceInviteAdapter;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp channel invite adapter.
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp uses Meta WhatsApp Business API credentials, not Twilio.
|
|
5
|
+
* The Meta API identifies numbers by phone_number_id (a numeric string),
|
|
6
|
+
* which isn't a user-facing phone number. The display number is resolved
|
|
7
|
+
* from workspace config (`whatsapp.phoneNumber`), falling back to
|
|
8
|
+
* `undefined` (triggering generic instructions) when not configured.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ChannelId } from "../../channels/types.js";
|
|
12
|
+
import { getConfig } from "../../config/loader.js";
|
|
13
|
+
import type { ChannelInviteAdapter } from "../channel-invite-transport.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Phone number resolution
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the user-configured WhatsApp display phone number from workspace
|
|
21
|
+
* config. The Meta API's `phone_number_id` is not user-facing, so the
|
|
22
|
+
* display number must be explicitly configured by the user.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveWhatsAppDisplayNumber(): string | undefined {
|
|
25
|
+
try {
|
|
26
|
+
const config = getConfig();
|
|
27
|
+
return config.whatsapp.phoneNumber || undefined;
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Adapter implementation
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export const whatsappInviteAdapter: ChannelInviteAdapter = {
|
|
38
|
+
channel: "whatsapp" as ChannelId,
|
|
39
|
+
|
|
40
|
+
resolveChannelHandle(): string | undefined {
|
|
41
|
+
return resolveWhatsAppDisplayNumber();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -3,9 +3,12 @@ import {
|
|
|
3
3
|
getTollFreeVerificationStatus,
|
|
4
4
|
hasTwilioCredentials,
|
|
5
5
|
} from "../calls/twilio-rest.js";
|
|
6
|
+
import { getChannelInvitePolicy } from "../channels/config.js";
|
|
6
7
|
import { getTwilioPhoneNumberEnv } from "../config/env.js";
|
|
7
8
|
import { loadRawConfig } from "../config/loader.js";
|
|
9
|
+
import { getEmailService } from "../email/service.js";
|
|
8
10
|
import { getSecureKey } from "../security/secure-keys.js";
|
|
11
|
+
import { resolveWhatsAppDisplayNumber } from "./channel-invite-transports/whatsapp.js";
|
|
9
12
|
import type {
|
|
10
13
|
ChannelId,
|
|
11
14
|
ChannelProbe,
|
|
@@ -19,25 +22,13 @@ export const REMOTE_TTL_MS = 5 * 60 * 1000;
|
|
|
19
22
|
|
|
20
23
|
// ── SMS Probe ───────────────────────────────────────────────────────────────
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? "";
|
|
27
|
-
const enabled =
|
|
28
|
-
(ingress.enabled as boolean | undefined) ??
|
|
29
|
-
(publicBaseUrl ? true : false);
|
|
30
|
-
return enabled && publicBaseUrl.length > 0;
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
25
|
+
// Keep Twilio phone-number resolution inside an already-authorized module so
|
|
26
|
+
// the secure-key import boundary does not expand for shared config helpers.
|
|
27
|
+
function resolveSmsPhoneNumber(): string {
|
|
28
|
+
return resolveTwilioPhoneNumber();
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
* Resolve SMS from-number with canonical precedence:
|
|
38
|
-
* env override -> config sms.phoneNumber -> secure key fallback.
|
|
39
|
-
*/
|
|
40
|
-
function resolveSmsPhoneNumber(): string {
|
|
31
|
+
function resolveTwilioPhoneNumber(): string {
|
|
41
32
|
try {
|
|
42
33
|
const raw = loadRawConfig();
|
|
43
34
|
const smsConfig = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
@@ -56,6 +47,20 @@ function resolveSmsPhoneNumber(): string {
|
|
|
56
47
|
}
|
|
57
48
|
}
|
|
58
49
|
|
|
50
|
+
function hasIngressConfigured(): boolean {
|
|
51
|
+
try {
|
|
52
|
+
const raw = loadRawConfig();
|
|
53
|
+
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
54
|
+
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? "";
|
|
55
|
+
const enabled =
|
|
56
|
+
(ingress.enabled as boolean | undefined) ??
|
|
57
|
+
(publicBaseUrl ? true : false);
|
|
58
|
+
return enabled && publicBaseUrl.length > 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
const smsProbe: ChannelProbe = {
|
|
60
65
|
channel: "sms",
|
|
61
66
|
runLocalChecks(): ReadinessCheckResult[] {
|
|
@@ -171,32 +176,6 @@ const smsProbe: ChannelProbe = {
|
|
|
171
176
|
|
|
172
177
|
// ── Voice Probe ─────────────────────────────────────────────────────────────
|
|
173
178
|
|
|
174
|
-
/**
|
|
175
|
-
* Resolve voice from-number with the same precedence as SMS:
|
|
176
|
-
* env override -> config sms.phoneNumber -> secure key fallback.
|
|
177
|
-
*
|
|
178
|
-
* Voice and SMS share the same Twilio phone number infrastructure, so the
|
|
179
|
-
* resolution logic is identical to resolveSmsPhoneNumber.
|
|
180
|
-
*/
|
|
181
|
-
function resolveVoicePhoneNumber(): string {
|
|
182
|
-
try {
|
|
183
|
-
const raw = loadRawConfig();
|
|
184
|
-
const smsConfig = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
185
|
-
return (
|
|
186
|
-
getTwilioPhoneNumberEnv() ||
|
|
187
|
-
(smsConfig.phoneNumber as string) ||
|
|
188
|
-
getSecureKey("credential:twilio:phone_number") ||
|
|
189
|
-
""
|
|
190
|
-
);
|
|
191
|
-
} catch {
|
|
192
|
-
return (
|
|
193
|
-
getTwilioPhoneNumberEnv() ||
|
|
194
|
-
getSecureKey("credential:twilio:phone_number") ||
|
|
195
|
-
""
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
179
|
const voiceProbe: ChannelProbe = {
|
|
201
180
|
channel: "voice",
|
|
202
181
|
runLocalChecks(): ReadinessCheckResult[] {
|
|
@@ -211,7 +190,7 @@ const voiceProbe: ChannelProbe = {
|
|
|
211
190
|
: "Twilio Account SID and Auth Token are not configured",
|
|
212
191
|
});
|
|
213
192
|
|
|
214
|
-
const resolvedNumber =
|
|
193
|
+
const resolvedNumber = resolveSmsPhoneNumber();
|
|
215
194
|
const hasPhone = !!resolvedNumber;
|
|
216
195
|
results.push({
|
|
217
196
|
name: "phone_number",
|
|
@@ -275,6 +254,181 @@ const telegramProbe: ChannelProbe = {
|
|
|
275
254
|
// Telegram has no remote checks currently
|
|
276
255
|
};
|
|
277
256
|
|
|
257
|
+
// ── Email Probe ─────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
const emailProbe: ChannelProbe = {
|
|
260
|
+
channel: "email",
|
|
261
|
+
runLocalChecks(): ReadinessCheckResult[] {
|
|
262
|
+
const results: ReadinessCheckResult[] = [];
|
|
263
|
+
|
|
264
|
+
const hasApiKey = !!(
|
|
265
|
+
getSecureKey("agentmail") || getSecureKey("credential:agentmail:api_key")
|
|
266
|
+
);
|
|
267
|
+
results.push({
|
|
268
|
+
name: "agentmail_api_key",
|
|
269
|
+
passed: hasApiKey,
|
|
270
|
+
message: hasApiKey
|
|
271
|
+
? "AgentMail API key is configured"
|
|
272
|
+
: "AgentMail API key is not configured",
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const invitePolicy = getChannelInvitePolicy("email");
|
|
276
|
+
results.push({
|
|
277
|
+
name: "invite_policy",
|
|
278
|
+
passed: invitePolicy.codeRedemptionEnabled,
|
|
279
|
+
message: invitePolicy.codeRedemptionEnabled
|
|
280
|
+
? "Email invite code redemption is enabled"
|
|
281
|
+
: "Email invite code redemption is disabled",
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const hasIngress = hasIngressConfigured();
|
|
285
|
+
results.push({
|
|
286
|
+
name: "ingress",
|
|
287
|
+
passed: hasIngress,
|
|
288
|
+
message: hasIngress
|
|
289
|
+
? "Public ingress URL is configured"
|
|
290
|
+
: "Public ingress URL is not configured or disabled",
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return results;
|
|
294
|
+
},
|
|
295
|
+
async runRemoteChecks(): Promise<ReadinessCheckResult[]> {
|
|
296
|
+
// Only worth checking if the API key is present
|
|
297
|
+
const hasApiKey = !!(
|
|
298
|
+
getSecureKey("agentmail") || getSecureKey("credential:agentmail:api_key")
|
|
299
|
+
);
|
|
300
|
+
if (!hasApiKey) return [];
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const address = await getEmailService().getPrimaryInboxAddress();
|
|
304
|
+
const hasInbox = !!address;
|
|
305
|
+
return [
|
|
306
|
+
{
|
|
307
|
+
name: "inbox_configured",
|
|
308
|
+
passed: hasInbox,
|
|
309
|
+
message: hasInbox
|
|
310
|
+
? `Inbox address is configured (${address})`
|
|
311
|
+
: "No inbox address configured — create one with: vellum email setup inboxes",
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
316
|
+
return [
|
|
317
|
+
{
|
|
318
|
+
name: "inbox_configured",
|
|
319
|
+
passed: false,
|
|
320
|
+
message: `Failed to check inbox configuration: ${message}`,
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// ── WhatsApp Probe ──────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
const whatsappProbe: ChannelProbe = {
|
|
330
|
+
channel: "whatsapp",
|
|
331
|
+
runLocalChecks(): ReadinessCheckResult[] {
|
|
332
|
+
const results: ReadinessCheckResult[] = [];
|
|
333
|
+
|
|
334
|
+
const hasPhoneNumberId = !!getSecureKey(
|
|
335
|
+
"credential:whatsapp:phone_number_id",
|
|
336
|
+
);
|
|
337
|
+
results.push({
|
|
338
|
+
name: "whatsapp_phone_number_id",
|
|
339
|
+
passed: hasPhoneNumberId,
|
|
340
|
+
message: hasPhoneNumberId
|
|
341
|
+
? "WhatsApp phone number ID is configured"
|
|
342
|
+
: "WhatsApp phone number ID is not configured",
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const hasAccessToken = !!getSecureKey("credential:whatsapp:access_token");
|
|
346
|
+
results.push({
|
|
347
|
+
name: "whatsapp_access_token",
|
|
348
|
+
passed: hasAccessToken,
|
|
349
|
+
message: hasAccessToken
|
|
350
|
+
? "WhatsApp access token is configured"
|
|
351
|
+
: "WhatsApp access token is not configured",
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const hasAppSecret = !!getSecureKey("credential:whatsapp:app_secret");
|
|
355
|
+
results.push({
|
|
356
|
+
name: "whatsapp_app_secret",
|
|
357
|
+
passed: hasAppSecret,
|
|
358
|
+
message: hasAppSecret
|
|
359
|
+
? "WhatsApp app secret is configured"
|
|
360
|
+
: "WhatsApp app secret is not configured",
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const hasWebhookVerifyToken = !!getSecureKey(
|
|
364
|
+
"credential:whatsapp:webhook_verify_token",
|
|
365
|
+
);
|
|
366
|
+
results.push({
|
|
367
|
+
name: "whatsapp_webhook_verify_token",
|
|
368
|
+
passed: hasWebhookVerifyToken,
|
|
369
|
+
message: hasWebhookVerifyToken
|
|
370
|
+
? "WhatsApp webhook verify token is configured"
|
|
371
|
+
: "WhatsApp webhook verify token is not configured",
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const displayNumber = resolveWhatsAppDisplayNumber();
|
|
375
|
+
const hasDisplayNumber = !!displayNumber;
|
|
376
|
+
results.push({
|
|
377
|
+
name: "whatsapp_display_phone_number",
|
|
378
|
+
passed: hasDisplayNumber,
|
|
379
|
+
message: hasDisplayNumber
|
|
380
|
+
? `WhatsApp display phone number is configured (${displayNumber})`
|
|
381
|
+
: "WhatsApp display phone number is not configured — set whatsapp.phoneNumber in workspace config",
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const invitePolicy = getChannelInvitePolicy("whatsapp");
|
|
385
|
+
results.push({
|
|
386
|
+
name: "invite_policy",
|
|
387
|
+
passed: invitePolicy.codeRedemptionEnabled,
|
|
388
|
+
message: invitePolicy.codeRedemptionEnabled
|
|
389
|
+
? "WhatsApp invite code redemption is enabled"
|
|
390
|
+
: "WhatsApp invite code redemption is disabled",
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const hasIngress = hasIngressConfigured();
|
|
394
|
+
results.push({
|
|
395
|
+
name: "ingress",
|
|
396
|
+
passed: hasIngress,
|
|
397
|
+
message: hasIngress
|
|
398
|
+
? "Public ingress URL is configured"
|
|
399
|
+
: "Public ingress URL is not configured or disabled",
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return results;
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// ── Slack Probe ─────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
const slackProbe: ChannelProbe = {
|
|
409
|
+
channel: "slack",
|
|
410
|
+
runLocalChecks(): ReadinessCheckResult[] {
|
|
411
|
+
const hasBotToken = !!getSecureKey("credential:slack_channel:bot_token");
|
|
412
|
+
const hasAppToken = !!getSecureKey("credential:slack_channel:app_token");
|
|
413
|
+
return [
|
|
414
|
+
{
|
|
415
|
+
name: "bot_token",
|
|
416
|
+
passed: hasBotToken,
|
|
417
|
+
message: hasBotToken
|
|
418
|
+
? "Slack bot token is configured"
|
|
419
|
+
: "Slack bot token is not configured",
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "app_token",
|
|
423
|
+
passed: hasAppToken,
|
|
424
|
+
message: hasAppToken
|
|
425
|
+
? "Slack app token is configured"
|
|
426
|
+
: "Slack app token is not configured",
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
|
|
278
432
|
// ── Service ─────────────────────────────────────────────────────────────────
|
|
279
433
|
|
|
280
434
|
export class ChannelReadinessService {
|
|
@@ -416,11 +570,14 @@ export class ChannelReadinessService {
|
|
|
416
570
|
|
|
417
571
|
// ── Factory ─────────────────────────────────────────────────────────────────
|
|
418
572
|
|
|
419
|
-
/** Create a service instance with built-in SMS, Voice, and
|
|
573
|
+
/** Create a service instance with built-in SMS, Voice, Telegram, Email, WhatsApp, and Slack probes registered. */
|
|
420
574
|
export function createReadinessService(): ChannelReadinessService {
|
|
421
575
|
const service = new ChannelReadinessService();
|
|
422
576
|
service.registerProbe(smsProbe);
|
|
423
577
|
service.registerProbe(voiceProbe);
|
|
424
578
|
service.registerProbe(telegramProbe);
|
|
579
|
+
service.registerProbe(emailProbe);
|
|
580
|
+
service.registerProbe(whatsappProbe);
|
|
581
|
+
service.registerProbe(slackProbe);
|
|
425
582
|
return service;
|
|
426
583
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
createCanonicalGuardianDelivery,
|
|
19
19
|
} from "../memory/canonical-guardian-store.js";
|
|
20
20
|
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
21
|
+
import type { NotificationSourceChannel } from "../notifications/signal.js";
|
|
21
22
|
import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
|
|
22
23
|
import { getLogger } from "../util/logger.js";
|
|
23
24
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
|
|
@@ -144,7 +145,7 @@ export function bridgeConfirmationRequestToGuardian(
|
|
|
144
145
|
// Emit guardian.question notification so the guardian is alerted.
|
|
145
146
|
const signalPromise = emitNotificationSignal({
|
|
146
147
|
sourceEventName: "guardian.question",
|
|
147
|
-
sourceChannel,
|
|
148
|
+
sourceChannel: sourceChannel as NotificationSourceChannel,
|
|
148
149
|
sourceSessionId: conversationId,
|
|
149
150
|
attentionHints: {
|
|
150
151
|
requiresAction: true,
|
|
@@ -259,9 +259,9 @@ function initiateGuardianVoiceCall(
|
|
|
259
259
|
// Start outbound
|
|
260
260
|
// ---------------------------------------------------------------------------
|
|
261
261
|
|
|
262
|
-
export function startOutbound(
|
|
262
|
+
export async function startOutbound(
|
|
263
263
|
params: StartOutboundParams,
|
|
264
|
-
): OutboundActionResult {
|
|
264
|
+
): Promise<OutboundActionResult> {
|
|
265
265
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
266
266
|
const channel = params.channel;
|
|
267
267
|
const originConversationId = params.originConversationId;
|
|
@@ -275,7 +275,7 @@ export function startOutbound(
|
|
|
275
275
|
originConversationId,
|
|
276
276
|
);
|
|
277
277
|
} else if (channel === "telegram") {
|
|
278
|
-
return startOutboundTelegram(
|
|
278
|
+
return await startOutboundTelegram(
|
|
279
279
|
params.destination,
|
|
280
280
|
assistantId,
|
|
281
281
|
channel,
|
|
@@ -397,13 +397,13 @@ function startOutboundSms(
|
|
|
397
397
|
};
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
function startOutboundTelegram(
|
|
400
|
+
async function startOutboundTelegram(
|
|
401
401
|
destination: string | undefined,
|
|
402
402
|
assistantId: string,
|
|
403
403
|
channel: ChannelId,
|
|
404
404
|
rebind?: boolean,
|
|
405
405
|
originConversationId?: string,
|
|
406
|
-
): OutboundActionResult {
|
|
406
|
+
): Promise<OutboundActionResult> {
|
|
407
407
|
if (!destination) {
|
|
408
408
|
return {
|
|
409
409
|
success: false,
|
|
@@ -495,6 +495,9 @@ function startOutboundTelegram(
|
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
// Telegram handle/username: create a pending_bootstrap session with deep-link
|
|
498
|
+
const { ensureTelegramBotUsernameResolved } =
|
|
499
|
+
await import("./channel-invite-transports/telegram.js");
|
|
500
|
+
await ensureTelegramBotUsernameResolved();
|
|
498
501
|
const botUsername = getTelegramBotUsername();
|
|
499
502
|
if (!botUsername) {
|
|
500
503
|
return {
|
|
@@ -130,6 +130,7 @@ import { surfaceActionRouteDefinitions } from "./routes/surface-action-routes.js
|
|
|
130
130
|
import { surfaceContentRouteDefinitions } from "./routes/surface-content-routes.js";
|
|
131
131
|
import { trustRulesRouteDefinitions } from "./routes/trust-rules-routes.js";
|
|
132
132
|
import { twilioRouteDefinitions } from "./routes/twilio-routes.js";
|
|
133
|
+
import { usageRouteDefinitions } from "./routes/usage-routes.js";
|
|
133
134
|
|
|
134
135
|
// Re-export for consumers
|
|
135
136
|
export { isPrivateAddress } from "./middleware/auth.js";
|
|
@@ -684,6 +685,7 @@ export class RuntimeHttpServer {
|
|
|
684
685
|
...secretRouteDefinitions(),
|
|
685
686
|
...identityRouteDefinitions(),
|
|
686
687
|
...debugRouteDefinitions(),
|
|
688
|
+
...usageRouteDefinitions(),
|
|
687
689
|
|
|
688
690
|
// Browser relay — not extracted into a domain module because
|
|
689
691
|
// these two routes depend on the in-process extensionRelayServer
|