@agent-native/dispatch 0.40.30-nightly-20261021093329 → 0.40.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/CHANGELOG.md +15 -60
- package/dist/actions/connect-external-agent.d.ts.map +1 -1
- package/dist/actions/connect-external-agent.js +12 -13
- package/dist/actions/connect-external-agent.js.map +1 -1
- package/dist/actions/create-workspace-resource.d.ts +13 -13
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/import-agent.d.ts +13 -13
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/inspect-connect-agent-card.d.ts +5 -0
- package/dist/actions/inspect-connect-agent-card.d.ts.map +1 -0
- package/dist/actions/inspect-connect-agent-card.js +15 -0
- package/dist/actions/inspect-connect-agent-card.js.map +1 -0
- package/dist/actions/install-curated-community-app.d.ts +36 -0
- package/dist/actions/install-curated-community-app.d.ts.map +1 -0
- package/dist/actions/install-curated-community-app.js +26 -0
- package/dist/actions/install-curated-community-app.js.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts +3 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.js +15 -0
- package/dist/actions/list-connect-marketplace-apps.js.map +1 -0
- package/dist/actions/list-connected-agents.d.ts +2 -0
- package/dist/actions/list-connected-agents.js +7 -3
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list-curated-community-apps.d.ts +3 -0
- package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
- package/dist/actions/list-curated-community-apps.js +10 -0
- package/dist/actions/list-curated-community-apps.js.map +1 -0
- package/dist/actions/list-workspace-connections.d.ts.map +1 -1
- package/dist/actions/list-workspace-connections.js +13 -18
- package/dist/actions/list-workspace-connections.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/actions/remix-workspace-template.d.ts +6 -0
- package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
- package/dist/actions/update-workspace-app-metadata.js +26 -5
- package/dist/actions/update-workspace-app-metadata.js.map +1 -1
- package/dist/components/action-query-error.d.ts +2 -1
- package/dist/components/action-query-error.d.ts.map +1 -1
- package/dist/components/action-query-error.js +25 -2
- package/dist/components/action-query-error.js.map +1 -1
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +2 -2
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/community-apps-section.d.ts +2 -0
- package/dist/components/community-apps-section.d.ts.map +1 -0
- package/dist/components/community-apps-section.js +90 -0
- package/dist/components/community-apps-section.js.map +1 -0
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +1 -1
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +7 -0
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +63 -9
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/lib/connect-apps.d.ts +1 -2
- package/dist/lib/connect-apps.d.ts.map +1 -1
- package/dist/lib/connect-apps.js +41 -65
- package/dist/lib/connect-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +1 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +3 -2
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/connect.d.ts.map +1 -1
- package/dist/routes/pages/connect.js +5 -7
- package/dist/routes/pages/connect.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +11 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +379 -92
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.d.ts +45 -0
- package/dist/server/lib/community-apps.d.ts.map +1 -0
- package/dist/server/lib/community-apps.js +267 -0
- package/dist/server/lib/community-apps.js.map +1 -0
- package/dist/server/lib/community-source.d.ts +19 -0
- package/dist/server/lib/community-source.d.ts.map +1 -0
- package/dist/server/lib/community-source.js +118 -0
- package/dist/server/lib/community-source.js.map +1 -0
- package/dist/server/lib/connect-apps.d.ts +4 -0
- package/dist/server/lib/connect-apps.d.ts.map +1 -0
- package/dist/server/lib/connect-apps.js +99 -0
- package/dist/server/lib/connect-apps.js.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.js +44 -0
- package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/labs.d.ts +2 -0
- package/dist/shared/labs.d.ts.map +1 -0
- package/dist/shared/labs.js +8 -0
- package/dist/shared/labs.js.map +1 -0
- package/package.json +5 -4
- package/src/actions/connect-external-agent.spec.ts +101 -3
- package/src/actions/connect-external-agent.ts +12 -11
- package/src/actions/index.spec.ts +13 -0
- package/src/actions/index.ts +8 -0
- package/src/actions/inspect-connect-agent-card.ts +17 -0
- package/src/actions/install-curated-community-app.ts +32 -0
- package/src/actions/list-connect-marketplace-apps.ts +17 -0
- package/src/actions/list-connected-agents.spec.ts +17 -1
- package/src/actions/list-connected-agents.ts +7 -2
- package/src/actions/list-curated-community-apps.ts +12 -0
- package/src/actions/list-workspace-connections.spec.ts +14 -3
- package/src/actions/list-workspace-connections.ts +13 -17
- package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
- package/src/actions/update-workspace-app-metadata.ts +48 -24
- package/src/components/action-query-error.spec.tsx +20 -0
- package/src/components/action-query-error.tsx +40 -0
- package/src/components/chat-home-apps.spec.tsx +32 -1
- package/src/components/chat-home-apps.tsx +6 -1
- package/src/components/community-apps-section.tsx +224 -0
- package/src/components/dispatch-control-plane.tsx +1 -0
- package/src/components/layout/Layout.tsx +9 -0
- package/src/components/workspace-app-card.spec.tsx +226 -1
- package/src/components/workspace-app-card.tsx +115 -12
- package/src/lib/connect-apps.spec.ts +118 -66
- package/src/lib/connect-apps.ts +47 -82
- package/src/lib/workspace-apps.ts +1 -0
- package/src/routes/pages/$appId.tsx +1 -0
- package/src/routes/pages/apps.spec.tsx +227 -3
- package/src/routes/pages/apps.tsx +5 -0
- package/src/routes/pages/connect.spec.tsx +53 -6
- package/src/routes/pages/connect.tsx +14 -11
- package/src/server/index.ts +9 -0
- package/src/server/lib/app-creation-store.spec.ts +820 -16
- package/src/server/lib/app-creation-store.ts +504 -101
- package/src/server/lib/community-apps.spec.ts +367 -0
- package/src/server/lib/community-apps.ts +372 -0
- package/src/server/lib/community-source.spec.ts +151 -0
- package/src/server/lib/community-source.ts +186 -0
- package/src/server/lib/connect-apps.ts +149 -0
- package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
- package/src/server/lib/org-apps-directory-cache.ts +66 -0
- package/src/server/plugins/integrations.spec.ts +60 -0
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/labs.ts +9 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildAgentCardUrl,
|
|
5
|
+
normalizeConnectUrl,
|
|
6
|
+
parseConnectAgentCard,
|
|
7
|
+
type ConnectAgentCard,
|
|
8
|
+
type MarketplaceApp,
|
|
9
|
+
} from "../../lib/connect-apps.js";
|
|
10
|
+
|
|
11
|
+
const CONNECT_FETCH_TIMEOUT_MS = 5_000;
|
|
12
|
+
const MARKETPLACE_MAX_BYTES = 1_000_000;
|
|
13
|
+
const AGENT_CARD_MAX_BYTES = 256_000;
|
|
14
|
+
const MAX_MARKETPLACE_CANDIDATES = 50;
|
|
15
|
+
|
|
16
|
+
async function readLimitedText(
|
|
17
|
+
response: Response,
|
|
18
|
+
maxBytes: number,
|
|
19
|
+
label: string,
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
22
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
23
|
+
throw new Error(`${label} is too large`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!response.body) return "";
|
|
27
|
+
const reader = response.body.getReader();
|
|
28
|
+
const decoder = new TextDecoder();
|
|
29
|
+
let totalBytes = 0;
|
|
30
|
+
let body = "";
|
|
31
|
+
try {
|
|
32
|
+
while (true) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done) break;
|
|
35
|
+
totalBytes += value.byteLength;
|
|
36
|
+
if (totalBytes > maxBytes) {
|
|
37
|
+
await reader.cancel();
|
|
38
|
+
throw new Error(`${label} is too large`);
|
|
39
|
+
}
|
|
40
|
+
body += decoder.decode(value, { stream: true });
|
|
41
|
+
}
|
|
42
|
+
return body + decoder.decode();
|
|
43
|
+
} finally {
|
|
44
|
+
reader.releaseLock();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchJsonText(
|
|
49
|
+
url: string,
|
|
50
|
+
maxBytes: number,
|
|
51
|
+
label: string,
|
|
52
|
+
assertUrlAllowed?: (url: string) => void,
|
|
53
|
+
) {
|
|
54
|
+
const response = await ssrfSafeFetch(
|
|
55
|
+
url,
|
|
56
|
+
{
|
|
57
|
+
headers: { accept: "application/json" },
|
|
58
|
+
signal: AbortSignal.timeout(CONNECT_FETCH_TIMEOUT_MS),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
maxRedirects: 3,
|
|
62
|
+
requireDispatcher: true,
|
|
63
|
+
httpsOnly: true,
|
|
64
|
+
...(assertUrlAllowed ? { assertUrlAllowed } : {}),
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
if (!response.ok) throw new Error(`${label} returned ${response.status}`);
|
|
68
|
+
return readLimitedText(response, maxBytes, label);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function fetchConnectAgentCard(
|
|
72
|
+
appUrl: string,
|
|
73
|
+
): Promise<ConnectAgentCard> {
|
|
74
|
+
const target = normalizeConnectUrl(appUrl);
|
|
75
|
+
if (!target || target.protocol !== "https:") {
|
|
76
|
+
throw new Error("Enter a secure app URL.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cardUrl = new URL(buildAgentCardUrl(target.toString()));
|
|
80
|
+
const body = await fetchJsonText(
|
|
81
|
+
cardUrl.toString(),
|
|
82
|
+
AGENT_CARD_MAX_BYTES,
|
|
83
|
+
"Agent card",
|
|
84
|
+
(url) => {
|
|
85
|
+
const redirect = new URL(url);
|
|
86
|
+
if (
|
|
87
|
+
redirect.origin !== cardUrl.origin ||
|
|
88
|
+
redirect.pathname !== cardUrl.pathname
|
|
89
|
+
) {
|
|
90
|
+
throw new Error("Agent card redirects must stay on the requested URL.");
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
const card = parseConnectAgentCard(
|
|
95
|
+
JSON.parse(body),
|
|
96
|
+
target.origin,
|
|
97
|
+
target.pathname,
|
|
98
|
+
);
|
|
99
|
+
if (!card) {
|
|
100
|
+
throw new Error("This app has an invalid or incompatible agent card.");
|
|
101
|
+
}
|
|
102
|
+
return card;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function fetchMarketplaceApps(
|
|
106
|
+
feedUrl = "https://www.agent-native.com/apps.json",
|
|
107
|
+
): Promise<MarketplaceApp[]> {
|
|
108
|
+
const feed = normalizeConnectUrl(feedUrl);
|
|
109
|
+
if (!feed || feed.protocol !== "https:") {
|
|
110
|
+
throw new Error("Marketplace feed URL must use HTTPS.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const body = await fetchJsonText(
|
|
114
|
+
feed.toString(),
|
|
115
|
+
MARKETPLACE_MAX_BYTES,
|
|
116
|
+
"Marketplace feed",
|
|
117
|
+
);
|
|
118
|
+
const payload = JSON.parse(body) as { apps?: unknown };
|
|
119
|
+
if (!Array.isArray(payload.apps)) {
|
|
120
|
+
throw new Error("Marketplace feed is invalid");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const candidates = payload.apps
|
|
124
|
+
.filter((entry): entry is MarketplaceApp => {
|
|
125
|
+
if (!entry || typeof entry !== "object") return false;
|
|
126
|
+
const app = entry as Record<string, unknown>;
|
|
127
|
+
const url =
|
|
128
|
+
typeof app.url === "string" ? normalizeConnectUrl(app.url) : null;
|
|
129
|
+
return (
|
|
130
|
+
typeof app.id === "string" &&
|
|
131
|
+
typeof app.name === "string" &&
|
|
132
|
+
typeof app.description === "string" &&
|
|
133
|
+
Boolean(url && url.protocol === "https:") &&
|
|
134
|
+
Array.isArray(app.capabilities) &&
|
|
135
|
+
app.capabilities.every((capability) => typeof capability === "string")
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
.slice(0, MAX_MARKETPLACE_CANDIDATES);
|
|
139
|
+
|
|
140
|
+
const verified = await Promise.allSettled(
|
|
141
|
+
candidates.map(async (app) => {
|
|
142
|
+
const card = await fetchConnectAgentCard(app.url);
|
|
143
|
+
return card.connect ? { ...app, capabilities: ["connect"] } : null;
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
return verified.flatMap((result) =>
|
|
147
|
+
result.status === "fulfilled" && result.value ? [result.value] : [],
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createOrgDirectorySuccessCache } from "./org-apps-directory-cache.js";
|
|
4
|
+
|
|
5
|
+
describe("createOrgDirectorySuccessCache", () => {
|
|
6
|
+
it("coalesces same-key refreshes and isolates cache keys", async () => {
|
|
7
|
+
let resolveLoad!: (value: string[]) => void;
|
|
8
|
+
const load = vi.fn(
|
|
9
|
+
() =>
|
|
10
|
+
new Promise<string[]>((resolve) => {
|
|
11
|
+
resolveLoad = resolve;
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
const cache = createOrgDirectorySuccessCache<string[]>();
|
|
15
|
+
|
|
16
|
+
const first = cache.get("org-a|user:alice", load);
|
|
17
|
+
const second = cache.get("org-a|user:alice", load);
|
|
18
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
19
|
+
resolveLoad(["analytics"]);
|
|
20
|
+
await expect(Promise.all([first, second])).resolves.toEqual([
|
|
21
|
+
["analytics"],
|
|
22
|
+
["analytics"],
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
await cache.get("org-a|user:bob", async () => ["mail"]);
|
|
26
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("caches successful values until expiry and retries failures", async () => {
|
|
30
|
+
let now = 1_000;
|
|
31
|
+
const cache = createOrgDirectorySuccessCache<string[]>({
|
|
32
|
+
ttlMs: 60,
|
|
33
|
+
now: () => now,
|
|
34
|
+
});
|
|
35
|
+
const load = vi
|
|
36
|
+
.fn<() => Promise<string[]>>()
|
|
37
|
+
.mockResolvedValueOnce(["content"])
|
|
38
|
+
.mockRejectedValueOnce(new Error("directory unavailable"))
|
|
39
|
+
.mockResolvedValueOnce(["design"])
|
|
40
|
+
.mockResolvedValueOnce(["design-updated"]);
|
|
41
|
+
|
|
42
|
+
await expect(cache.get("org-a", load)).resolves.toEqual(["content"]);
|
|
43
|
+
now += 59;
|
|
44
|
+
await expect(cache.get("org-a", load)).resolves.toEqual(["content"]);
|
|
45
|
+
now += 2;
|
|
46
|
+
await expect(cache.get("org-a", load)).rejects.toThrow(
|
|
47
|
+
"directory unavailable",
|
|
48
|
+
);
|
|
49
|
+
await expect(cache.get("org-a", load)).resolves.toEqual(["design"]);
|
|
50
|
+
expect(load).toHaveBeenCalledTimes(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not repopulate after invalidation while a refresh is in flight", async () => {
|
|
54
|
+
let resolveLoad!: (value: string[]) => void;
|
|
55
|
+
const cache = createOrgDirectorySuccessCache<string[]>();
|
|
56
|
+
const staleLoad = cache.get(
|
|
57
|
+
"org-a",
|
|
58
|
+
() =>
|
|
59
|
+
new Promise<string[]>((resolve) => {
|
|
60
|
+
resolveLoad = resolve;
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
cache.clear();
|
|
65
|
+
resolveLoad(["before-edit"]);
|
|
66
|
+
await expect(staleLoad).resolves.toEqual(["before-edit"]);
|
|
67
|
+
await expect(
|
|
68
|
+
cache.get("org-a", async () => ["after-edit"]),
|
|
69
|
+
).resolves.toEqual(["after-edit"]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface CachedOrgDirectoryAgent {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
whenToUse?: string[];
|
|
6
|
+
url: string;
|
|
7
|
+
source?: "workspace" | "org-directory" | "remote-agent" | "builtin";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OrgDirectorySuccessCache<T> {
|
|
11
|
+
get(key: string, load: () => Promise<T>): Promise<T>;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createOrgDirectorySuccessCache<T>(input?: {
|
|
16
|
+
ttlMs?: number;
|
|
17
|
+
now?: () => number;
|
|
18
|
+
}): OrgDirectorySuccessCache<T> {
|
|
19
|
+
const ttlMs = input?.ttlMs ?? 60_000;
|
|
20
|
+
const now = input?.now ?? Date.now;
|
|
21
|
+
const values = new Map<string, { value: T; expiresAt: number }>();
|
|
22
|
+
const inFlight = new Map<string, Promise<T>>();
|
|
23
|
+
let generation = 0;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
async get(key, load) {
|
|
27
|
+
const cached = values.get(key);
|
|
28
|
+
if (cached && cached.expiresAt > now()) return cached.value;
|
|
29
|
+
values.delete(key);
|
|
30
|
+
|
|
31
|
+
const pending = inFlight.get(key);
|
|
32
|
+
if (pending) return pending;
|
|
33
|
+
|
|
34
|
+
const loadGeneration = generation;
|
|
35
|
+
const refresh = load()
|
|
36
|
+
.then((value) => {
|
|
37
|
+
if (loadGeneration === generation) {
|
|
38
|
+
values.set(key, { value, expiresAt: now() + ttlMs });
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
})
|
|
42
|
+
.finally(() => {
|
|
43
|
+
if (inFlight.get(key) === refresh) inFlight.delete(key);
|
|
44
|
+
});
|
|
45
|
+
inFlight.set(key, refresh);
|
|
46
|
+
return refresh;
|
|
47
|
+
},
|
|
48
|
+
clear() {
|
|
49
|
+
generation += 1;
|
|
50
|
+
values.clear();
|
|
51
|
+
inFlight.clear();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const orgAppsDirectoryCache =
|
|
57
|
+
createOrgDirectorySuccessCache<CachedOrgDirectoryAgent[]>();
|
|
58
|
+
|
|
59
|
+
export const orgAppsDirectoryAccessCache = createOrgDirectorySuccessCache<
|
|
60
|
+
Set<string>
|
|
61
|
+
>({ ttlMs: 15_000 });
|
|
62
|
+
|
|
63
|
+
export function invalidateOrgAppsDirectoryCache(): void {
|
|
64
|
+
orgAppsDirectoryCache.clear();
|
|
65
|
+
orgAppsDirectoryAccessCache.clear();
|
|
66
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
createIntegrationsPlugin: vi.fn(),
|
|
5
|
+
integrationsPlugin: vi.fn(),
|
|
6
|
+
getDispatchConfig: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@agent-native/core/server", () => ({
|
|
10
|
+
createIntegrationsPlugin: mocks.createIntegrationsPlugin,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("../../actions/index.js", () => ({
|
|
14
|
+
dispatchActions: { ask_app: {} },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("../index.js", () => ({
|
|
18
|
+
getDispatchConfig: mocks.getDispatchConfig,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../lib/dispatch-integrations.js", () => ({
|
|
22
|
+
beforeDispatchProcess: vi.fn(),
|
|
23
|
+
resolveDispatchExecutionContext: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe("Dispatch integrations plugin", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.resetModules();
|
|
29
|
+
mocks.createIntegrationsPlugin.mockReset();
|
|
30
|
+
mocks.integrationsPlugin.mockReset();
|
|
31
|
+
mocks.getDispatchConfig.mockReset();
|
|
32
|
+
mocks.createIntegrationsPlugin.mockReturnValue(mocks.integrationsPlugin);
|
|
33
|
+
mocks.getDispatchConfig.mockReturnValue({});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("routes delegation from the prompt resource context without a peer roster", async () => {
|
|
37
|
+
const { default: dispatchIntegrationsPlugin } =
|
|
38
|
+
await import("./integrations.js");
|
|
39
|
+
const nitroApp = {};
|
|
40
|
+
|
|
41
|
+
await dispatchIntegrationsPlugin(nitroApp);
|
|
42
|
+
|
|
43
|
+
expect(mocks.createIntegrationsPlugin).toHaveBeenCalledWith(
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
appId: "dispatch",
|
|
46
|
+
systemPrompt: expect.stringContaining(
|
|
47
|
+
'search with describe-workspace-apps query="<capability>"',
|
|
48
|
+
),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
const options = mocks.createIntegrationsPlugin.mock.calls[0][0];
|
|
52
|
+
expect(options.systemPrompt).not.toContain(
|
|
53
|
+
"Apps you can delegate to include",
|
|
54
|
+
);
|
|
55
|
+
expect(options.systemPrompt).toContain("the relevant-apps context");
|
|
56
|
+
expect(options.systemPrompt).not.toContain("available-apps context");
|
|
57
|
+
expect(options.systemPrompt).not.toContain("slides (decks/presentations)");
|
|
58
|
+
expect(mocks.integrationsPlugin).toHaveBeenCalledWith(nitroApp);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -19,9 +19,9 @@ const DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for thi
|
|
|
19
19
|
|
|
20
20
|
Default posture:
|
|
21
21
|
- Treat Slack, Telegram, and email as shared entrypoints into the workspace.
|
|
22
|
-
- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job.
|
|
23
|
-
- Use
|
|
24
|
-
- Hosted/connected A2A neighbors such as Analytics and Content come from the
|
|
22
|
+
- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job.
|
|
23
|
+
- Use relevant-apps prompt profiles first, then list-connected-agents when you need fresh details. If that context is absent or does not list the needed capability, search with describe-workspace-apps query="<capability>" before assuming a request must be handled locally.
|
|
24
|
+
- Hosted/connected A2A neighbors such as Analytics and Content come from the relevant-apps context or list-connected-agents. If neither identifies the needed capability, use describe-workspace-apps before concluding a connected agent is unavailable. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
|
|
25
25
|
- When asked whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.
|
|
26
26
|
- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
|
|
27
27
|
- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineLab } from "@agent-native/core/labs/registry";
|
|
2
|
+
|
|
3
|
+
export const COMMUNITY_APPS_LAB = defineLab({
|
|
4
|
+
key: "dispatch.community-apps",
|
|
5
|
+
displayName: "Community apps",
|
|
6
|
+
description:
|
|
7
|
+
"Connect trusted remote agents and install reviewed, pinned community app templates.",
|
|
8
|
+
keywords: "community apps templates connect install",
|
|
9
|
+
});
|