@agent-native/dispatch 0.13.8 → 0.13.10

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.
@@ -14,15 +14,15 @@ import {
14
14
  type LoaderFunctionArgs,
15
15
  } from "react-router";
16
16
 
17
- import { DispatchShell } from "@/components/dispatch-shell";
18
- import { Badge } from "@/components/ui/badge";
19
- import { Button } from "@/components/ui/button";
20
- import { Spinner } from "@/components/ui/spinner";
21
- import { resolveServerCatchAllTarget } from "@/lib/catch-all-target";
17
+ import { DispatchShell } from "../../components/dispatch-shell";
18
+ import { Badge } from "../../components/ui/badge";
19
+ import { Button } from "../../components/ui/button";
20
+ import { Spinner } from "../../components/ui/spinner";
21
+ import { resolveServerCatchAllTarget } from "../../lib/catch-all-target";
22
22
  import {
23
23
  workspaceAppHref,
24
24
  type WorkspaceAppSummary,
25
- } from "@/lib/workspace-apps";
25
+ } from "../../lib/workspace-apps";
26
26
 
27
27
  export function meta() {
28
28
  return [{ title: "Workspace app - Dispatch" }];
@@ -15,9 +15,13 @@ vi.mock("@agent-native/core/org", () => ({
15
15
  resolveOrgIdForEmail: mocks.resolveOrgIdForEmail,
16
16
  }));
17
17
 
18
- import type { IncomingMessage } from "@agent-native/core/server";
18
+ import type {
19
+ IncomingMessage,
20
+ PlatformAdapter,
21
+ } from "@agent-native/core/server";
19
22
 
20
23
  import {
24
+ beforeDispatchProcess,
21
25
  identityKeyForIncoming,
22
26
  resolveDispatchOwner,
23
27
  } from "./dispatch-integrations.js";
@@ -54,6 +58,38 @@ function emailIncoming(
54
58
  };
55
59
  }
56
60
 
61
+ function telegramIncoming(
62
+ overrides: Partial<IncomingMessage> = {},
63
+ ): IncomingMessage {
64
+ return {
65
+ platform: "telegram",
66
+ externalThreadId: "12345",
67
+ text: "ask analytics about traffic",
68
+ senderId: "777",
69
+ senderName: "Steve",
70
+ platformContext: { chatId: 12345, fromId: 777, rawText: "hello" },
71
+ timestamp: 1,
72
+ ...overrides,
73
+ };
74
+ }
75
+
76
+ const noopAdapter: PlatformAdapter = {
77
+ platform: "telegram",
78
+ label: "Telegram",
79
+ getRequiredEnvKeys: () => [],
80
+ handleVerification: async () => ({ handled: false }),
81
+ verifyWebhook: async () => true,
82
+ parseIncomingMessage: async () => null,
83
+ sendResponse: async () => {},
84
+ formatAgentResponse: (text: string) => ({ text, platformContext: {} }),
85
+ getStatus: async () => ({
86
+ platform: "telegram",
87
+ label: "Telegram",
88
+ enabled: true,
89
+ configured: true,
90
+ }),
91
+ };
92
+
57
93
  beforeEach(() => {
58
94
  mocks.resolveLinkedOwner.mockResolvedValue(null);
59
95
  mocks.consumeLinkToken.mockResolvedValue("owner@example.test");
@@ -75,6 +111,10 @@ describe("identityKeyForIncoming", () => {
75
111
  it("scopes Slack identities by team", () => {
76
112
  expect(identityKeyForIncoming(slackIncoming())).toBe("T123:U123");
77
113
  });
114
+
115
+ it("uses Telegram sender ids as link identities", () => {
116
+ expect(identityKeyForIncoming(telegramIncoming())).toBe("777");
117
+ });
78
118
  });
79
119
 
80
120
  describe("resolveDispatchOwner", () => {
@@ -184,3 +224,54 @@ describe("resolveDispatchOwner", () => {
184
224
  ).resolves.toBe("victim@member.test");
185
225
  });
186
226
  });
227
+
228
+ describe("beforeDispatchProcess", () => {
229
+ it("asks unlinked Telegram users to link before using org context", async () => {
230
+ vi.stubEnv("APP_URL", "https://dispatch.agent-native.test");
231
+
232
+ const result = await beforeDispatchProcess(telegramIncoming(), noopAdapter);
233
+
234
+ expect(result).toEqual({
235
+ handled: true,
236
+ responseText:
237
+ "Telegram is connected, but this Telegram account is not linked to an Agent-Native user yet. Tap https://dispatch.agent-native.test/identities, create a Telegram link token, then send `/link <token>` here. After that I can use your Builder.io org and connected apps.",
238
+ });
239
+ expect(mocks.resolveLinkedOwner).toHaveBeenCalledWith("telegram", "777", {
240
+ allowAnyOrgFallback: true,
241
+ });
242
+ });
243
+
244
+ it("lets linked Telegram users proceed to normal agent processing", async () => {
245
+ mocks.resolveLinkedOwner.mockResolvedValueOnce("steve@builder.io");
246
+
247
+ await expect(
248
+ beforeDispatchProcess(telegramIncoming(), noopAdapter),
249
+ ).resolves.toEqual({ handled: false });
250
+ });
251
+
252
+ it("still consumes Telegram link commands before enforcing the link gate", async () => {
253
+ const result = await beforeDispatchProcess(
254
+ telegramIncoming({
255
+ text: "token-123",
256
+ platformContext: {
257
+ chatId: 12345,
258
+ fromId: 777,
259
+ rawText: "/link token-123",
260
+ },
261
+ }),
262
+ noopAdapter,
263
+ );
264
+
265
+ expect(result).toEqual({
266
+ handled: true,
267
+ responseText:
268
+ "Linked successfully. Future telegram messages will use owner@example.test's personal dispatch context.",
269
+ });
270
+ expect(mocks.consumeLinkToken).toHaveBeenCalledWith({
271
+ platform: "telegram",
272
+ token: "token-123",
273
+ externalUserId: "777",
274
+ externalUserName: "Steve",
275
+ });
276
+ });
277
+ });
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
3
  import { resolveOrgIdForEmail } from "@agent-native/core/org";
4
+ import { withConfiguredAppBasePath } from "@agent-native/core/server";
4
5
  import type {
5
6
  IncomingMessage,
6
7
  PlatformAdapter,
@@ -77,6 +78,40 @@ function configuredDefaultOwnerForIncoming(
77
78
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
78
79
  }
79
80
 
81
+ function platformRequiresExplicitLink(incoming: IncomingMessage): boolean {
82
+ // Telegram does not provide a verified email address. Require an explicit
83
+ // identity link before it can act as a Builder/Agent-Native user.
84
+ return incoming.platform === "telegram";
85
+ }
86
+
87
+ function configuredDispatchIdentitiesUrl(): string | null {
88
+ const raw =
89
+ process.env.WORKSPACE_GATEWAY_URL ||
90
+ process.env.APP_URL ||
91
+ process.env.URL ||
92
+ process.env.DEPLOY_URL ||
93
+ process.env.BETTER_AUTH_URL ||
94
+ "";
95
+ if (!raw.trim()) return null;
96
+ const base = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
97
+ try {
98
+ return new URL(
99
+ "identities",
100
+ `${withConfiguredAppBasePath(base).replace(/\/+$/, "")}/`,
101
+ ).toString();
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ function formatTelegramLinkRequiredMessage(): string {
108
+ const identitiesUrl = configuredDispatchIdentitiesUrl();
109
+ const linkStep = identitiesUrl
110
+ ? `Tap ${identitiesUrl}, create a Telegram link token, then send \`/link <token>\` here.`
111
+ : "Open Dispatch while signed in, create a Telegram link token, then send `/link <token>` here.";
112
+ return `Telegram is connected, but this Telegram account is not linked to an Agent-Native user yet. ${linkStep} After that I can use your Builder.io org and connected apps.`;
113
+ }
114
+
80
115
  async function resolveSlackSenderProfile(
81
116
  incoming: IncomingMessage,
82
117
  ): Promise<SlackSenderProfile> {
@@ -228,6 +263,19 @@ export async function beforeDispatchProcess(
228
263
  contextString(incoming.platformContext.rawText) || trimmed;
229
264
  const match = commandText.match(/^\/link(?:@\w+)?\s+([a-zA-Z0-9_-]+)$/);
230
265
  if (!match) {
266
+ if (platformRequiresExplicitLink(incoming)) {
267
+ const owner = await resolveLinkedOwner(
268
+ incoming.platform,
269
+ identityKeyForIncoming(incoming),
270
+ { allowAnyOrgFallback: true },
271
+ );
272
+ if (!owner) {
273
+ return {
274
+ handled: true,
275
+ responseText: formatTelegramLinkRequiredMessage(),
276
+ };
277
+ }
278
+ }
231
279
  return handleRemoteCodeCommand(incoming, adapter, {
232
280
  resolveOwner: () => resolveDispatchOwner(incoming),
233
281
  });
@@ -7,6 +7,17 @@ import {
7
7
  resolveDispatchOwner,
8
8
  } from "../lib/dispatch-integrations.js";
9
9
 
10
+ const dispatchIntegrationActions = {
11
+ ...dispatchActions,
12
+ // Messaging integrations should use the core call-agent tool for cross-app
13
+ // delegation because it queues A2A continuations when serverless budgets are
14
+ // tight. The MCP-facing ask_app action is still available outside this path.
15
+ ask_app: {
16
+ ...dispatchActions.ask_app,
17
+ agentTool: false,
18
+ },
19
+ };
20
+
10
21
  const DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).
11
22
 
12
23
  Default posture:
@@ -21,6 +32,7 @@ Default posture:
21
32
 
22
33
  When a user asks for something:
23
34
  - If it belongs to analytics, content, slides, clips, assets, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.
35
+ - In messaging integrations, use call-agent for cross-app delegation; do not use ask_app.
24
36
  - After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.
25
37
  - Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
26
38
  - If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
@@ -49,7 +61,7 @@ const dispatchIntegrationsPlugin = async (nitroApp: any) => {
49
61
 
50
62
  const plugin = createIntegrationsPlugin({
51
63
  appId: "dispatch",
52
- actions: dispatchActions,
64
+ actions: dispatchIntegrationActions,
53
65
  resolveOwner: resolveDispatchOwner,
54
66
  beforeProcess: beforeDispatchProcess,
55
67
  systemPrompt,