@agent-native/core 0.212.0-nightly-20261021093329 → 0.212.0
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/dist/a2a/app-profile.d.ts +4 -0
- package/dist/a2a/app-profile.js +95 -0
- package/dist/a2a/server.d.ts +1 -0
- package/dist/a2a/server.js +45 -6
- package/dist/a2a/types.d.ts +6 -0
- package/dist/agent/jev-tool-prefetch.d.ts +6 -0
- package/dist/agent/jev-tool-prefetch.js +18 -0
- package/dist/agent/production-agent.js +30 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/index.d.ts +1 -1
- package/dist/collab/index.js +0 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/workspace-deploy.js +33 -1
- package/dist/extensions/url-safety.js +3 -5
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/adapters/slack.js +142 -14
- package/dist/integrations/webhook-handler.js +42 -1
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/jobs/background-automation-runner.d.ts +7 -0
- package/dist/jobs/background-automation-runner.js +13 -1
- package/dist/mcp/org-directory.d.ts +9 -4
- package/dist/mcp/org-directory.js +117 -29
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/org/context.d.ts +1 -0
- package/dist/org/context.js +11 -0
- package/dist/org/handlers.js +62 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/metadata.d.ts +2 -0
- package/dist/resources/metadata.js +27 -1
- package/dist/scripts/call-agent.js +44 -3
- package/dist/scripts/describe-workspace-apps.js +36 -4
- package/dist/server/agent-chat/action-filters-a2a.d.ts +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +1 -0
- package/dist/server/agent-chat/prompt-resources.d.ts +16 -0
- package/dist/server/agent-chat/prompt-resources.js +290 -54
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.js +51 -7
- package/dist/server/agent-discovery.d.ts +36 -3
- package/dist/server/agent-discovery.js +397 -39
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/request-context.d.ts +9 -0
- package/dist/server/workspace-app-selection.d.ts +19 -0
- package/dist/server/workspace-app-selection.js +141 -0
- package/dist/shared/agent-profile.d.ts +25 -0
- package/dist/shared/agent-profile.js +73 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +4 -2
- package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +5 -2
- package/dist/triggers/actions/manage-automation.d.ts +7 -7
- package/migration-manifest.json +0 -6
- package/package.json +5 -4
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +4 -2
- package/src/templates/factory/.agents/skills/review-prs/SKILL.md +5 -2
|
@@ -281,10 +281,25 @@ export function slackAdapter(options = {}) {
|
|
|
281
281
|
const channelId = context.platformContext.channelId;
|
|
282
282
|
const threadTs = context.platformContext.threadTs;
|
|
283
283
|
const blocks = message.platformContext?.blocks;
|
|
284
|
+
const responseImages = getSlackResponseImages(message.platformContext?.imageUrls);
|
|
285
|
+
const { images, linkedImages } = await resolveOwnedSlackResponseImages(responseImages.images);
|
|
286
|
+
const deliveryText = [
|
|
287
|
+
message.text,
|
|
288
|
+
...responseImages.linkedImages.map((image) => `Image: ${image.url}`),
|
|
289
|
+
...linkedImages.map((image) => `Image: ${image.url}`),
|
|
290
|
+
]
|
|
291
|
+
.filter(Boolean)
|
|
292
|
+
.join("\n");
|
|
284
293
|
const placeholderRef = opts?.placeholderRef;
|
|
285
|
-
const chunks = splitNonEmptyMessage(message.text, SLACK_MAX_LENGTH);
|
|
286
294
|
const hasProvidedBlocks = Array.isArray(blocks) && blocks.length > 0;
|
|
287
|
-
const
|
|
295
|
+
const hasImages = images.length > 0;
|
|
296
|
+
const chunks = splitNonEmptyMessage(deliveryText, SLACK_MAX_LENGTH);
|
|
297
|
+
const firstChunk = chunks[0] ??
|
|
298
|
+
(hasProvidedBlocks
|
|
299
|
+
? "Response"
|
|
300
|
+
: hasImages
|
|
301
|
+
? images[0].alt || "Image"
|
|
302
|
+
: "");
|
|
288
303
|
if (!firstChunk) {
|
|
289
304
|
if (threadTs) {
|
|
290
305
|
setSlackAssistantStatus(token, channelId, threadTs, "");
|
|
@@ -300,11 +315,26 @@ export function slackAdapter(options = {}) {
|
|
|
300
315
|
const reconciledRefs = opts?.idempotencyKey && freshChunkIndexes.length > 0
|
|
301
316
|
? await reconcileSlackDeliveryChunks(token, channelId, threadTs, opts.idempotencyKey, freshChunkIndexes, opts.reconcileAfter, opts.signal)
|
|
302
317
|
: new Map();
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
318
|
+
const imageBlocks = images.map(({ url, alt }) => ({
|
|
319
|
+
type: "image",
|
|
320
|
+
image_url: url,
|
|
321
|
+
alt_text: alt || "Image",
|
|
322
|
+
}));
|
|
323
|
+
const finalBlocks = hasProvidedBlocks && restChunks.length === 0
|
|
324
|
+
? [...blocks, ...imageBlocks].slice(0, 50)
|
|
325
|
+
: hasProvidedBlocks
|
|
326
|
+
? [
|
|
327
|
+
...buildResponseBlocks(firstChunk, {
|
|
328
|
+
threadDeepLinkUrl: message.platformContext
|
|
329
|
+
?.threadDeepLinkUrl,
|
|
330
|
+
}, imageBlocks).filter((block) => block?.type === "section"),
|
|
331
|
+
...blocks.filter((block) => block?.type !== "section"),
|
|
332
|
+
...imageBlocks,
|
|
333
|
+
].slice(0, 50)
|
|
334
|
+
: buildResponseBlocks(restChunks.length > 0 ? firstChunk : deliveryText, {
|
|
335
|
+
threadDeepLinkUrl: message.platformContext
|
|
336
|
+
?.threadDeepLinkUrl,
|
|
337
|
+
}, imageBlocks);
|
|
308
338
|
const baseBody = {
|
|
309
339
|
channel: channelId,
|
|
310
340
|
text: firstChunk,
|
|
@@ -487,11 +517,23 @@ export function slackAdapter(options = {}) {
|
|
|
487
517
|
}
|
|
488
518
|
},
|
|
489
519
|
formatAgentResponse(text, opts) {
|
|
520
|
+
const imageUrls = [];
|
|
521
|
+
const textWithImageLinks = text.replace(/!\[([^\]]*)\]\((https:\/\/[^\s)]+)\)/g, (_markdownImage, alt, url) => {
|
|
522
|
+
const image = parseSlackResponseImage(url, alt);
|
|
523
|
+
if (image) {
|
|
524
|
+
imageUrls.push(image);
|
|
525
|
+
return "";
|
|
526
|
+
}
|
|
527
|
+
return slackImageLinkText(url, alt);
|
|
528
|
+
});
|
|
490
529
|
return {
|
|
491
|
-
text: markdownToSlackMrkdwn(
|
|
492
|
-
platformContext:
|
|
493
|
-
|
|
494
|
-
|
|
530
|
+
text: markdownToSlackMrkdwn(textWithImageLinks),
|
|
531
|
+
platformContext: {
|
|
532
|
+
...(opts?.threadDeepLinkUrl
|
|
533
|
+
? { threadDeepLinkUrl: opts.threadDeepLinkUrl }
|
|
534
|
+
: {}),
|
|
535
|
+
...(imageUrls.length > 0 ? { imageUrls } : {}),
|
|
536
|
+
},
|
|
495
537
|
};
|
|
496
538
|
},
|
|
497
539
|
async getStatus(_baseUrl) {
|
|
@@ -875,12 +917,15 @@ function setSlackAssistantStatus(token, channelId, threadTs, status) {
|
|
|
875
917
|
}),
|
|
876
918
|
}).catch(() => { });
|
|
877
919
|
}
|
|
878
|
-
function buildResponseBlocks(text, opts) {
|
|
879
|
-
const sectionChunks =
|
|
920
|
+
function buildResponseBlocks(text, opts, imageBlocks = []) {
|
|
921
|
+
const sectionChunks = text.trim()
|
|
922
|
+
? splitMessage(text, SLACK_SECTION_TEXT_MAX_LENGTH)
|
|
923
|
+
: [];
|
|
880
924
|
const blocks = sectionChunks.map((chunk) => ({
|
|
881
925
|
type: "section",
|
|
882
926
|
text: { type: "mrkdwn", text: chunk },
|
|
883
927
|
}));
|
|
928
|
+
blocks.push(...imageBlocks);
|
|
884
929
|
if (opts.threadDeepLinkUrl) {
|
|
885
930
|
blocks.push({
|
|
886
931
|
type: "actions",
|
|
@@ -894,7 +939,90 @@ function buildResponseBlocks(text, opts) {
|
|
|
894
939
|
],
|
|
895
940
|
});
|
|
896
941
|
}
|
|
897
|
-
return blocks;
|
|
942
|
+
return blocks.slice(0, 50);
|
|
943
|
+
}
|
|
944
|
+
function getSlackResponseImages(value) {
|
|
945
|
+
if (!Array.isArray(value))
|
|
946
|
+
return { images: [], linkedImages: [] };
|
|
947
|
+
const images = [];
|
|
948
|
+
const linkedImages = [];
|
|
949
|
+
for (const image of value) {
|
|
950
|
+
if (!image || typeof image !== "object")
|
|
951
|
+
continue;
|
|
952
|
+
const { url, alt } = image;
|
|
953
|
+
if (typeof url !== "string")
|
|
954
|
+
continue;
|
|
955
|
+
const normalizedUrl = normalizeSlackImageLinkUrl(url);
|
|
956
|
+
if (!normalizedUrl)
|
|
957
|
+
continue;
|
|
958
|
+
const item = {
|
|
959
|
+
url: normalizedUrl,
|
|
960
|
+
alt: typeof alt === "string" ? alt.trim().slice(0, 2_000) : "",
|
|
961
|
+
};
|
|
962
|
+
if (parseSlackResponseImage(normalizedUrl, item.alt)) {
|
|
963
|
+
images.push(item);
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
linkedImages.push(item);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return { images, linkedImages };
|
|
970
|
+
}
|
|
971
|
+
function parseSlackResponseImage(value, alt) {
|
|
972
|
+
if (value.length > 3_000 || !URL.canParse(value))
|
|
973
|
+
return null;
|
|
974
|
+
const url = new URL(value);
|
|
975
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
976
|
+
return null;
|
|
977
|
+
return { url: url.toString(), alt: alt.trim().slice(0, 2_000) };
|
|
978
|
+
}
|
|
979
|
+
function normalizeSlackImageLinkUrl(value) {
|
|
980
|
+
if (!URL.canParse(value))
|
|
981
|
+
return null;
|
|
982
|
+
const url = new URL(value);
|
|
983
|
+
url.username = "";
|
|
984
|
+
url.password = "";
|
|
985
|
+
return url.toString();
|
|
986
|
+
}
|
|
987
|
+
function slackImageLinkText(value, alt) {
|
|
988
|
+
const url = normalizeSlackImageLinkUrl(value);
|
|
989
|
+
return url ? `[${alt.trim().slice(0, 2_000) || "Image"}](${url})` : alt;
|
|
990
|
+
}
|
|
991
|
+
async function resolveOwnedSlackResponseImages(images) {
|
|
992
|
+
if (images.length === 0)
|
|
993
|
+
return { images: [], linkedImages: [] };
|
|
994
|
+
try {
|
|
995
|
+
const { findFileUploadProviderOwningUrl } = await import("../../file-upload/registry.js");
|
|
996
|
+
const checks = await Promise.all(images.map(async (image) => {
|
|
997
|
+
try {
|
|
998
|
+
return {
|
|
999
|
+
image,
|
|
1000
|
+
owned: Boolean(await findFileUploadProviderOwningUrl(image.url)),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
catch (error) {
|
|
1004
|
+
return {
|
|
1005
|
+
image,
|
|
1006
|
+
owned: false,
|
|
1007
|
+
unverifiable: error instanceof Error ? error.name : "unknown",
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
}));
|
|
1011
|
+
const unverifiableTypes = checks.flatMap((check) => "unverifiable" in check ? [check.unverifiable] : []);
|
|
1012
|
+
if (unverifiableTypes.length > 0) {
|
|
1013
|
+
console.warn("[slack] Could not verify ownership of response image URLs; keeping links instead.", { count: unverifiableTypes.length, errorTypes: unverifiableTypes });
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
images: checks.filter((check) => check.owned).map((check) => check.image),
|
|
1017
|
+
linkedImages: checks
|
|
1018
|
+
.filter((check) => !check.owned)
|
|
1019
|
+
.map((check) => check.image),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
console.warn("[slack] File upload ownership checks are unavailable; keeping response image links.", { errorType: error instanceof Error ? error.name : "unknown" });
|
|
1024
|
+
return { images: [], linkedImages: images };
|
|
1025
|
+
}
|
|
898
1026
|
}
|
|
899
1027
|
async function postFresh(token, channelId, threadTs, body, signal) {
|
|
900
1028
|
const hasBlocks = Array.isArray(body.blocks) && body.blocks.length > 0;
|
|
@@ -4,7 +4,8 @@ import { isInBackgroundFunctionRuntime } from "../agent/durable-background.js";
|
|
|
4
4
|
import { formatLlmCredentialErrorMessage, isLlmCredentialError, } from "../agent/engine/credential-errors.js";
|
|
5
5
|
import { getConfiguredEngineNameForRequest, getStoredModelForEngine, normalizeModelForEngine, resolveEngine, } from "../agent/engine/index.js";
|
|
6
6
|
import { resolveMainChatMaxOutputTokens } from "../agent/engine/output-tokens.js";
|
|
7
|
-
import {
|
|
7
|
+
import { buildWorkspaceAppRequestContext } from "../agent/jev-tool-prefetch.js";
|
|
8
|
+
import { actionsToEngineTools, filterInitialEngineTools, getJevContextCredentials, resolveOwnerEngineApiKey, } from "../agent/production-agent.js";
|
|
8
9
|
import { appendDurableContinuationContext, runAgentLoopDirectWithSoftTimeout, } from "../agent/run-loop-with-resume.js";
|
|
9
10
|
import { startRun, } from "../agent/run-manager.js";
|
|
10
11
|
import { buildCurrentTimeUserContext, buildRuntimeContextPrompt, } from "../agent/runtime-context.js";
|
|
@@ -13,6 +14,7 @@ import { attachToolSearch } from "../agent/tool-search.js";
|
|
|
13
14
|
import { createThread, getThread, grantThreadUserShare, setThreadSourceIfMissing, } from "../chat-threads/store.js";
|
|
14
15
|
import { updateThreadData } from "../chat-threads/store.js";
|
|
15
16
|
import { getOrgA2ASecret } from "../org/context.js";
|
|
17
|
+
import { preloadJevContextWithStatus } from "../server/agent-chat/prompt-resources.js";
|
|
16
18
|
import { withConfiguredAppBasePath } from "../server/app-base-path.js";
|
|
17
19
|
import { getAppProductionUrl } from "../server/app-url.js";
|
|
18
20
|
import { runWithRequestContext } from "../server/request-context.js";
|
|
@@ -403,6 +405,45 @@ async function processIncomingMessage(incoming, options, opts = {}) {
|
|
|
403
405
|
}
|
|
404
406
|
}
|
|
405
407
|
effectiveSystemPrompt += await loadIntegrationMemoryPrompt(incoming.integrationScopeId).catch(() => "");
|
|
408
|
+
const workspaceAppHistory = (incoming.contextMessages ?? [])
|
|
409
|
+
.filter((message) => {
|
|
410
|
+
const isCurrent = message.text.trim() === incoming.text.trim() &&
|
|
411
|
+
(!incoming.senderId || message.senderId === incoming.senderId);
|
|
412
|
+
return !isCurrent;
|
|
413
|
+
})
|
|
414
|
+
.map((message) => ({
|
|
415
|
+
role: message.senderId && message.senderId === incoming.senderId
|
|
416
|
+
? "user"
|
|
417
|
+
: "assistant",
|
|
418
|
+
content: message.text,
|
|
419
|
+
}));
|
|
420
|
+
const appRequest = buildWorkspaceAppRequestContext({
|
|
421
|
+
request: incoming.text,
|
|
422
|
+
history: workspaceAppHistory,
|
|
423
|
+
selectedObjectContext: buildProviderConversationContext(incoming),
|
|
424
|
+
});
|
|
425
|
+
const appPreload = await runWithRequestContext({
|
|
426
|
+
userEmail: ownerEmail,
|
|
427
|
+
...requestContext,
|
|
428
|
+
isIntegrationCaller: true,
|
|
429
|
+
}, async () => {
|
|
430
|
+
const jevCredentials = await getJevContextCredentials(ownerEmail);
|
|
431
|
+
return preloadJevContextWithStatus({
|
|
432
|
+
request: appRequest,
|
|
433
|
+
appRequest,
|
|
434
|
+
owner: ownerEmail,
|
|
435
|
+
orgId,
|
|
436
|
+
apiKey: jevCredentials.apiKey,
|
|
437
|
+
personalApiKey: jevCredentials.personalApiKey,
|
|
438
|
+
builderAuth: jevCredentials.builderAuth,
|
|
439
|
+
selfAppId: options.appId,
|
|
440
|
+
workspaceAppsEnabled: Boolean(actions["call-agent"]),
|
|
441
|
+
appDiscoveryOnly: true,
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
if (appPreload.relevantAppsContext) {
|
|
445
|
+
effectiveSystemPrompt += `\n\n${appPreload.relevantAppsContext}`;
|
|
446
|
+
}
|
|
406
447
|
const budgetReservations = await reserveApplicableIntegrationBudgets({
|
|
407
448
|
incoming,
|
|
408
449
|
ownerEmail,
|
|
@@ -13,10 +13,10 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
13
13
|
enabled?: undefined;
|
|
14
14
|
nextRun?: undefined;
|
|
15
15
|
} | {
|
|
16
|
-
deleted?: undefined;
|
|
17
16
|
resourceId: string;
|
|
18
17
|
name: string;
|
|
19
18
|
enabled: boolean;
|
|
20
19
|
nextRun: string;
|
|
20
|
+
deleted?: undefined;
|
|
21
21
|
}>;
|
|
22
22
|
export default _default;
|
|
@@ -24,6 +24,13 @@ export interface BackgroundAutomationContext {
|
|
|
24
24
|
export interface BackgroundAutomationDeps {
|
|
25
25
|
getActions: (automation?: BackgroundAutomationContext) => Record<string, ActionEntry> | Promise<Record<string, ActionEntry>>;
|
|
26
26
|
getSystemPrompt: (owner: string) => Promise<string>;
|
|
27
|
+
getWorkspaceAppContext?: (input: {
|
|
28
|
+
request: string;
|
|
29
|
+
ownerEmail: string;
|
|
30
|
+
orgId?: string;
|
|
31
|
+
appId?: string;
|
|
32
|
+
workspaceAppsEnabled: boolean;
|
|
33
|
+
}) => Promise<string>;
|
|
27
34
|
getInitialToolNames?: (automation?: BackgroundAutomationContext) => string[] | undefined;
|
|
28
35
|
engine?: AgentEngine;
|
|
29
36
|
apiKey?: string;
|
|
@@ -844,7 +844,19 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
|
|
|
844
844
|
assertHardDeadline(options.hardDeadlineAt);
|
|
845
845
|
const model = await resolveBackgroundAutomationModel(engine, automation.meta.model, deps);
|
|
846
846
|
assertHardDeadline(options.hardDeadlineAt);
|
|
847
|
-
const
|
|
847
|
+
const [baseSystemPrompt, workspaceAppContext] = await Promise.all([
|
|
848
|
+
deps.getSystemPrompt(ownerEmail),
|
|
849
|
+
baseActions["call-agent"] && deps.getWorkspaceAppContext
|
|
850
|
+
? deps.getWorkspaceAppContext({
|
|
851
|
+
request: prompt,
|
|
852
|
+
ownerEmail,
|
|
853
|
+
orgId,
|
|
854
|
+
appId: deps.appId,
|
|
855
|
+
workspaceAppsEnabled: true,
|
|
856
|
+
})
|
|
857
|
+
: Promise.resolve(""),
|
|
858
|
+
]);
|
|
859
|
+
const systemPrompt = `${baseSystemPrompt}${workspaceAppContext ? `\n\n${workspaceAppContext}` : ""}\n\n${messages.noOpInstruction}`;
|
|
848
860
|
assertHardDeadline(options.hardDeadlineAt);
|
|
849
861
|
const resume = options.resume;
|
|
850
862
|
const thread = resume
|
|
@@ -31,10 +31,11 @@
|
|
|
31
31
|
* `fetchOrgApps()` remains the best-effort compatibility wrapper: on ANY
|
|
32
32
|
* error it returns `[]` and NEVER throws, so cross-app verbs still degrade
|
|
33
33
|
* silently to their exact current local-only behavior.
|
|
34
|
-
* -
|
|
34
|
+
* - Credentialed requests require HTTPS and use manual redirects. A short
|
|
35
|
+
* in-memory TTL cache (default 60s) is keyed by directory origin and
|
|
35
36
|
* caller identity/org scope so sibling app lists never cross tenants.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* Successful responses are cached for 60s; transient unavailable results
|
|
38
|
+
* are cached for 30s to avoid repeating failing directory requests.
|
|
38
39
|
* - No secrets are ever logged.
|
|
39
40
|
*
|
|
40
41
|
* Bundled alongside `mountMCP` (no Node-only top-level imports). The A2A
|
|
@@ -46,8 +47,11 @@ export interface OrgApp {
|
|
|
46
47
|
url: string;
|
|
47
48
|
a2aUrl: string;
|
|
48
49
|
capabilities?: string[];
|
|
50
|
+
description?: string;
|
|
51
|
+
whenToUse?: string[];
|
|
52
|
+
aliases?: string[];
|
|
49
53
|
}
|
|
50
|
-
export type OrgDirectoryUnavailableReason = "not-configured" | "authentication" | "authorization" | "timeout" | "network" | "invalid-response" | "server-error" | "http-error";
|
|
54
|
+
export type OrgDirectoryUnavailableReason = "not-configured" | "authentication" | "unauthorized" | "authorization" | "invalid-origin" | "missing-route" | "timeout" | "network" | "invalid-response" | "ssrf-blocked" | "server-error" | "http-error";
|
|
51
55
|
export type OrgDirectoryFetchResult = {
|
|
52
56
|
status: "available";
|
|
53
57
|
apps: OrgApp[];
|
|
@@ -59,6 +63,7 @@ export declare function resolveOrgDirectoryOrigin(env?: NodeJS.ProcessEnv): stri
|
|
|
59
63
|
export interface FetchOrgAppsOptions {
|
|
60
64
|
selfId?: string;
|
|
61
65
|
selfOrigin?: string;
|
|
66
|
+
directoryBaseUrl?: string;
|
|
62
67
|
includeDirectoryApp?: boolean;
|
|
63
68
|
serviceOrgId?: string;
|
|
64
69
|
env?: NodeJS.ProcessEnv;
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import { canonicalA2AAudience } from "../a2a/audience.js";
|
|
2
|
+
import { parseAgentProfileMetadataFields } from "../shared/agent-profile.js";
|
|
2
3
|
const SUCCESS_TTL_MS = 60_000;
|
|
4
|
+
const UNAVAILABLE_TTL_MS = 30_000;
|
|
3
5
|
const cache = new Map();
|
|
4
6
|
export function resolveOrgDirectoryOrigin(env = process.env) {
|
|
5
|
-
const raw = env
|
|
6
|
-
if (!raw
|
|
7
|
+
const raw = configuredOrgDirectoryUrl(env);
|
|
8
|
+
if (!raw)
|
|
7
9
|
return null;
|
|
8
10
|
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
9
11
|
if (!trimmed)
|
|
10
12
|
return null;
|
|
11
13
|
try {
|
|
12
14
|
const u = new URL(trimmed);
|
|
13
|
-
if (u.protocol !== "
|
|
15
|
+
if (u.protocol !== "https:")
|
|
14
16
|
return null;
|
|
15
17
|
return trimmed;
|
|
16
18
|
}
|
|
17
19
|
catch {
|
|
20
|
+
// coercion-ok: The caller converts malformed configured URLs to invalid-origin.
|
|
18
21
|
return null;
|
|
19
22
|
}
|
|
20
23
|
}
|
|
24
|
+
function configuredOrgDirectoryUrl(env) {
|
|
25
|
+
for (const key of [
|
|
26
|
+
"AGENT_NATIVE_ORG_DIRECTORY_URL",
|
|
27
|
+
"AGENT_NATIVE_IDENTITY_HUB_URL",
|
|
28
|
+
]) {
|
|
29
|
+
const value = env[key]?.trim();
|
|
30
|
+
if (value)
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
21
35
|
function isAbsoluteHttpUrl(value) {
|
|
22
36
|
try {
|
|
23
37
|
const url = new URL(value);
|
|
@@ -32,10 +46,20 @@ function normalizeApp(raw, strict = false) {
|
|
|
32
46
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
33
47
|
return null;
|
|
34
48
|
const r = raw;
|
|
49
|
+
const profile = parseAgentProfileMetadataFields({
|
|
50
|
+
...(r.description !== undefined ? { description: r.description } : {}),
|
|
51
|
+
...(r.whenToUse !== undefined ? { whenToUse: r.whenToUse } : {}),
|
|
52
|
+
...(r.aliases !== undefined ? { aliases: r.aliases } : {}),
|
|
53
|
+
});
|
|
35
54
|
const id = typeof r.id === "string" ? r.id.trim().toLowerCase() : "";
|
|
36
55
|
const url = typeof r.url === "string" ? r.url.trim() : "";
|
|
37
56
|
if (!id || !url || !isAbsoluteHttpUrl(url))
|
|
38
57
|
return null;
|
|
58
|
+
if (strict && profile.invalidFields.length > 0) {
|
|
59
|
+
console.warn("[org-directory] dropped invalid peer profile fields", {
|
|
60
|
+
fields: profile.invalidFields,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
39
63
|
if (strict &&
|
|
40
64
|
((r.name !== undefined && typeof r.name !== "string") ||
|
|
41
65
|
(r.a2aUrl !== undefined && typeof r.a2aUrl !== "string") ||
|
|
@@ -58,19 +82,25 @@ function normalizeApp(raw, strict = false) {
|
|
|
58
82
|
: Array.isArray(r.capabilities)
|
|
59
83
|
? r.capabilities.filter((c) => typeof c === "string")
|
|
60
84
|
: undefined;
|
|
85
|
+
const { description, whenToUse, aliases } = profile.metadata;
|
|
61
86
|
return {
|
|
62
87
|
id,
|
|
63
88
|
name,
|
|
64
89
|
url: url.replace(/\/+$/, ""),
|
|
65
90
|
a2aUrl: a2aUrl.replace(/\/+$/, ""),
|
|
66
91
|
...(capabilities && capabilities.length ? { capabilities } : {}),
|
|
92
|
+
...(description ? { description } : {}),
|
|
93
|
+
...(whenToUse?.length ? { whenToUse } : {}),
|
|
94
|
+
...(aliases?.length ? { aliases } : {}),
|
|
67
95
|
};
|
|
68
96
|
}
|
|
69
|
-
function
|
|
97
|
+
function sameAppBaseUrl(a, b) {
|
|
70
98
|
try {
|
|
71
99
|
const ua = new URL(a);
|
|
72
100
|
const ub = new URL(b);
|
|
73
|
-
|
|
101
|
+
const normalizePath = (pathname) => pathname.replace(/\/+$/, "") || "/";
|
|
102
|
+
return (ua.origin === ub.origin &&
|
|
103
|
+
normalizePath(ua.pathname) === normalizePath(ub.pathname));
|
|
74
104
|
}
|
|
75
105
|
catch {
|
|
76
106
|
return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
@@ -98,15 +128,25 @@ function serviceScopedCacheKey(origin, orgId, includeDirectoryApp = false, stric
|
|
|
98
128
|
}
|
|
99
129
|
async function fetchOrgAppsResultInternal(opts, strictValidation = true) {
|
|
100
130
|
const env = opts?.env ?? process.env;
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
const hasExplicitBaseUrl = opts?.directoryBaseUrl !== undefined;
|
|
132
|
+
const hasConfiguredEnvUrl = configuredOrgDirectoryUrl(env) !== null;
|
|
133
|
+
const origin = hasExplicitBaseUrl
|
|
134
|
+
? normalizeDirectoryBaseUrl(opts?.directoryBaseUrl ?? "")
|
|
135
|
+
: resolveOrgDirectoryOrigin(env);
|
|
136
|
+
if (!origin) {
|
|
137
|
+
return {
|
|
138
|
+
status: "unavailable",
|
|
139
|
+
reason: hasExplicitBaseUrl || hasConfiguredEnvUrl
|
|
140
|
+
? "invalid-origin"
|
|
141
|
+
: "not-configured",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
104
144
|
const selfId = (opts?.selfId ?? "").trim().toLowerCase();
|
|
105
145
|
const selfOrigin = (opts?.selfOrigin ?? "").trim();
|
|
106
146
|
const stripSelf = (apps) => apps.filter((a) => {
|
|
107
147
|
if (selfId && a.id === selfId)
|
|
108
148
|
return false;
|
|
109
|
-
if (selfOrigin &&
|
|
149
|
+
if (selfOrigin && sameAppBaseUrl(a.url, selfOrigin))
|
|
110
150
|
return false;
|
|
111
151
|
return true;
|
|
112
152
|
});
|
|
@@ -116,13 +156,15 @@ async function fetchOrgAppsResultInternal(opts, strictValidation = true) {
|
|
|
116
156
|
const serviceCacheKey = serviceScopedCacheKey(origin, serviceOrgId, opts?.includeDirectoryApp, strictValidation);
|
|
117
157
|
const cached = cache.get(serviceCacheKey);
|
|
118
158
|
if (cached && cached.expiresAt > Date.now()) {
|
|
119
|
-
return
|
|
159
|
+
return cached.result.status === "available"
|
|
160
|
+
? { status: "available", apps: stripSelf(cached.result.apps) }
|
|
161
|
+
: cached.result;
|
|
120
162
|
}
|
|
121
163
|
cacheKey = serviceCacheKey;
|
|
122
164
|
}
|
|
123
165
|
try {
|
|
124
166
|
const directoryUrl = `${origin}/_agent-native/org/apps`;
|
|
125
|
-
const audience = canonicalA2AAudience(
|
|
167
|
+
const audience = canonicalA2AAudience(directoryUrl);
|
|
126
168
|
const auth = serviceOrgId
|
|
127
169
|
? await resolveOrgDirectoryServiceAuth(serviceOrgId, audience)
|
|
128
170
|
: await resolveOrgDirectoryCallerAuth(audience);
|
|
@@ -135,13 +177,18 @@ async function fetchOrgAppsResultInternal(opts, strictValidation = true) {
|
|
|
135
177
|
cacheKey = scopedCacheKey(origin, auth, opts?.includeDirectoryApp, strictValidation);
|
|
136
178
|
const cached = cache.get(cacheKey);
|
|
137
179
|
if (cached && cached.expiresAt > now) {
|
|
138
|
-
return
|
|
180
|
+
return cached.result.status === "available"
|
|
181
|
+
? { status: "available", apps: stripSelf(cached.result.apps) }
|
|
182
|
+
: cached.result;
|
|
139
183
|
}
|
|
140
184
|
}
|
|
141
|
-
const { resolveVercelDeploymentProtectionHeaders } = await
|
|
185
|
+
const [{ resolveVercelDeploymentProtectionHeaders }, { ssrfSafeFetch }] = await Promise.all([
|
|
186
|
+
import("../server/credential-provider.js"),
|
|
187
|
+
import("../extensions/url-safety.js"),
|
|
188
|
+
]);
|
|
142
189
|
const protectionHeaders = resolveVercelDeploymentProtectionHeaders(directoryUrl);
|
|
143
190
|
for (let i = 0; i < attempts.length; i++) {
|
|
144
|
-
const res = await
|
|
191
|
+
const res = await ssrfSafeFetch(directoryUrl, {
|
|
145
192
|
method: "GET",
|
|
146
193
|
headers: {
|
|
147
194
|
Authorization: `Bearer ${attempts[i]}`,
|
|
@@ -151,11 +198,11 @@ async function fetchOrgAppsResultInternal(opts, strictValidation = true) {
|
|
|
151
198
|
? { "X-Agent-Native-Include-Directory-App": "1" }
|
|
152
199
|
: {}),
|
|
153
200
|
},
|
|
154
|
-
...(protectionHeaders["x-vercel-protection-bypass"]
|
|
155
|
-
? { redirect: "manual" }
|
|
156
|
-
: {}),
|
|
157
201
|
signal: AbortSignal.timeout(4000),
|
|
158
|
-
});
|
|
202
|
+
}, { maxRedirects: 0, followRedirects: false, httpsOnly: true });
|
|
203
|
+
if (res.status >= 300 && res.status < 400) {
|
|
204
|
+
return { status: "unavailable", reason: "http-error" };
|
|
205
|
+
}
|
|
159
206
|
if (res.ok) {
|
|
160
207
|
const json = (await res.json());
|
|
161
208
|
if (!Array.isArray(json?.apps))
|
|
@@ -167,7 +214,7 @@ async function fetchOrgAppsResultInternal(opts, strictValidation = true) {
|
|
|
167
214
|
const apps = normalizedApps.filter((a) => a !== null);
|
|
168
215
|
if (cacheKey) {
|
|
169
216
|
cache.set(cacheKey, {
|
|
170
|
-
apps,
|
|
217
|
+
result: { status: "available", apps },
|
|
171
218
|
expiresAt: Date.now() + SUCCESS_TTL_MS,
|
|
172
219
|
});
|
|
173
220
|
}
|
|
@@ -175,29 +222,70 @@ async function fetchOrgAppsResultInternal(opts, strictValidation = true) {
|
|
|
175
222
|
}
|
|
176
223
|
if (res.status === 401 && i < attempts.length - 1)
|
|
177
224
|
continue;
|
|
178
|
-
|
|
225
|
+
const unavailable = {
|
|
179
226
|
status: "unavailable",
|
|
180
|
-
reason: res.status ===
|
|
181
|
-
? "
|
|
182
|
-
: res.status ===
|
|
183
|
-
? "
|
|
184
|
-
:
|
|
227
|
+
reason: res.status === 404
|
|
228
|
+
? "missing-route"
|
|
229
|
+
: res.status === 401
|
|
230
|
+
? "unauthorized"
|
|
231
|
+
: res.status === 403
|
|
232
|
+
? "authorization"
|
|
233
|
+
: res.status === 429 || res.status >= 500
|
|
234
|
+
? "server-error"
|
|
235
|
+
: "http-error",
|
|
185
236
|
};
|
|
237
|
+
if (cacheKey && unavailable.reason === "server-error") {
|
|
238
|
+
cache.set(cacheKey, {
|
|
239
|
+
result: unavailable,
|
|
240
|
+
expiresAt: Date.now() + UNAVAILABLE_TTL_MS,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return unavailable;
|
|
186
244
|
}
|
|
187
245
|
}
|
|
188
246
|
catch (error) {
|
|
189
|
-
|
|
247
|
+
const unavailable = {
|
|
190
248
|
status: "unavailable",
|
|
191
249
|
reason: error instanceof Error &&
|
|
192
250
|
(error.name === "TimeoutError" || error.name === "AbortError")
|
|
193
251
|
? "timeout"
|
|
194
|
-
: error instanceof
|
|
195
|
-
? "
|
|
196
|
-
:
|
|
252
|
+
: error instanceof Error && error.message.startsWith("SSRF blocked:")
|
|
253
|
+
? "ssrf-blocked"
|
|
254
|
+
: error instanceof SyntaxError
|
|
255
|
+
? "invalid-response"
|
|
256
|
+
: "network",
|
|
197
257
|
};
|
|
258
|
+
if (cacheKey &&
|
|
259
|
+
(unavailable.reason === "timeout" || unavailable.reason === "network")) {
|
|
260
|
+
cache.set(cacheKey, {
|
|
261
|
+
result: unavailable,
|
|
262
|
+
expiresAt: Date.now() + UNAVAILABLE_TTL_MS,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return unavailable;
|
|
198
266
|
}
|
|
199
267
|
return { status: "unavailable", reason: "authorization" };
|
|
200
268
|
}
|
|
269
|
+
function normalizeDirectoryBaseUrl(value) {
|
|
270
|
+
const trimmed = value.trim();
|
|
271
|
+
if (!trimmed)
|
|
272
|
+
return null;
|
|
273
|
+
try {
|
|
274
|
+
const url = new URL(trimmed);
|
|
275
|
+
if (url.protocol !== "https:" ||
|
|
276
|
+
url.username ||
|
|
277
|
+
url.password ||
|
|
278
|
+
url.search ||
|
|
279
|
+
url.hash) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// coercion-ok: The caller returns this as invalid-origin, not as an empty directory.
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
201
289
|
export async function fetchOrgAppsResult(opts) {
|
|
202
290
|
return fetchOrgAppsResultInternal(opts, true);
|
|
203
291
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
2
|
-
count: number;
|
|
3
|
-
updated?: undefined;
|
|
4
2
|
error?: undefined;
|
|
3
|
+
count: number;
|
|
5
4
|
ok?: undefined;
|
|
5
|
+
updated?: undefined;
|
|
6
6
|
} | {
|
|
7
|
+
error?: undefined;
|
|
7
8
|
count?: undefined;
|
|
8
9
|
updated: number;
|
|
9
|
-
error?: undefined;
|
|
10
10
|
ok?: undefined;
|
|
11
11
|
} | {
|
|
12
12
|
count?: undefined;
|
|
13
|
-
updated?: undefined;
|
|
14
13
|
error: string;
|
|
15
14
|
ok?: undefined;
|
|
16
|
-
} | {
|
|
17
|
-
count?: undefined;
|
|
18
15
|
updated?: undefined;
|
|
16
|
+
} | {
|
|
19
17
|
error?: undefined;
|
|
18
|
+
count?: undefined;
|
|
20
19
|
ok: boolean;
|
|
20
|
+
updated?: undefined;
|
|
21
21
|
}>>;
|
package/dist/org/context.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export declare function createOrganization(name: string, email: string, role?: O
|
|
|
54
54
|
}>;
|
|
55
55
|
export declare function bootstrapAdminOrganization(rawEmail: string, event?: H3Event): Promise<boolean>;
|
|
56
56
|
export declare function getOrgDomain(orgId: string): Promise<string | null>;
|
|
57
|
+
export declare function getOrgWorkspaceUrl(orgId: string): Promise<string | null>;
|
|
57
58
|
export declare function getOrgA2ASecret(orgId: string): Promise<string | null>;
|
|
58
59
|
export declare function getA2ASecretByDomain(domain: string): Promise<string | null>;
|
|
59
60
|
export type OrgDomainLookup = {
|
package/dist/org/context.js
CHANGED
|
@@ -741,6 +741,17 @@ export async function getOrgDomain(orgId) {
|
|
|
741
741
|
const domain = String(rows[0].allowed_domain || "");
|
|
742
742
|
return domain || null;
|
|
743
743
|
}
|
|
744
|
+
export async function getOrgWorkspaceUrl(orgId) {
|
|
745
|
+
const exec = getDbExec();
|
|
746
|
+
const { rows } = await exec.execute({
|
|
747
|
+
sql: `SELECT workspace_url FROM organizations WHERE id = ? LIMIT 1`,
|
|
748
|
+
args: [orgId],
|
|
749
|
+
});
|
|
750
|
+
if (!rows[0])
|
|
751
|
+
return null;
|
|
752
|
+
const workspaceUrl = String(rows[0].workspace_url ?? "").trim();
|
|
753
|
+
return workspaceUrl || null;
|
|
754
|
+
}
|
|
744
755
|
export async function getOrgA2ASecret(orgId) {
|
|
745
756
|
const exec = getDbExec();
|
|
746
757
|
const { rows } = await exec.execute({
|