@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.
Files changed (57) hide show
  1. package/dist/a2a/app-profile.d.ts +4 -0
  2. package/dist/a2a/app-profile.js +95 -0
  3. package/dist/a2a/server.d.ts +1 -0
  4. package/dist/a2a/server.js +45 -6
  5. package/dist/a2a/types.d.ts +6 -0
  6. package/dist/agent/jev-tool-prefetch.d.ts +6 -0
  7. package/dist/agent/jev-tool-prefetch.js +18 -0
  8. package/dist/agent/production-agent.js +30 -1
  9. package/dist/collab/awareness.d.ts +2 -2
  10. package/dist/collab/index.d.ts +1 -1
  11. package/dist/collab/index.js +0 -1
  12. package/dist/collab/struct-routes.d.ts +1 -1
  13. package/dist/deploy/workspace-deploy.js +33 -1
  14. package/dist/extensions/url-safety.js +3 -5
  15. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  16. package/dist/integrations/adapters/slack.js +142 -14
  17. package/dist/integrations/webhook-handler.js +42 -1
  18. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  19. package/dist/jobs/background-automation-runner.d.ts +7 -0
  20. package/dist/jobs/background-automation-runner.js +13 -1
  21. package/dist/mcp/org-directory.d.ts +9 -4
  22. package/dist/mcp/org-directory.js +117 -29
  23. package/dist/notifications/routes.d.ts +6 -6
  24. package/dist/org/context.d.ts +1 -0
  25. package/dist/org/context.js +11 -0
  26. package/dist/org/handlers.js +62 -3
  27. package/dist/progress/routes.d.ts +1 -1
  28. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  29. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  30. package/dist/resources/metadata.d.ts +2 -0
  31. package/dist/resources/metadata.js +27 -1
  32. package/dist/scripts/call-agent.js +44 -3
  33. package/dist/scripts/describe-workspace-apps.js +36 -4
  34. package/dist/server/agent-chat/action-filters-a2a.d.ts +1 -1
  35. package/dist/server/agent-chat/action-filters-a2a.js +1 -0
  36. package/dist/server/agent-chat/prompt-resources.d.ts +16 -0
  37. package/dist/server/agent-chat/prompt-resources.js +290 -54
  38. package/dist/server/agent-chat-plugin.d.ts +2 -0
  39. package/dist/server/agent-chat-plugin.js +51 -7
  40. package/dist/server/agent-discovery.d.ts +36 -3
  41. package/dist/server/agent-discovery.js +397 -39
  42. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  43. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  44. package/dist/server/request-context.d.ts +9 -0
  45. package/dist/server/workspace-app-selection.d.ts +19 -0
  46. package/dist/server/workspace-app-selection.js +141 -0
  47. package/dist/shared/agent-profile.d.ts +25 -0
  48. package/dist/shared/agent-profile.js +73 -0
  49. package/dist/shared/index.d.ts +1 -0
  50. package/dist/shared/index.js +1 -0
  51. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +4 -2
  52. package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +5 -2
  53. package/dist/triggers/actions/manage-automation.d.ts +7 -7
  54. package/migration-manifest.json +0 -6
  55. package/package.json +5 -4
  56. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +4 -2
  57. 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 firstChunk = chunks[0] ?? (hasProvidedBlocks ? "Response" : "");
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 finalBlocks = blocks ??
304
- buildResponseBlocks(firstChunk, {
305
- threadDeepLinkUrl: message.platformContext
306
- ?.threadDeepLinkUrl,
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(text),
492
- platformContext: opts?.threadDeepLinkUrl
493
- ? { threadDeepLinkUrl: opts.threadDeepLinkUrl }
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 = splitMessage(text || "_(no response)_", SLACK_SECTION_TEXT_MAX_LENGTH);
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 { actionsToEngineTools, filterInitialEngineTools, resolveOwnerEngineApiKey, } from "../agent/production-agent.js";
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 systemPrompt = `${await deps.getSystemPrompt(ownerEmail)}\n\n${messages.noOpInstruction}`;
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
- * - A short in-memory TTL cache (default 60s) keyed by directory origin and
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
- * Only successful authenticated responses are cached; unavailable results
37
- * remain immediately retryable.
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.AGENT_NATIVE_ORG_DIRECTORY_URL || env.AGENT_NATIVE_IDENTITY_HUB_URL;
6
- if (!raw || typeof raw !== "string")
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 !== "http:" && u.protocol !== "https:")
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 sameOrigin(a, b) {
97
+ function sameAppBaseUrl(a, b) {
70
98
  try {
71
99
  const ua = new URL(a);
72
100
  const ub = new URL(b);
73
- return ua.host === ub.host && ua.protocol === ub.protocol;
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 origin = resolveOrgDirectoryOrigin(env);
102
- if (!origin)
103
- return { status: "unavailable", reason: "not-configured" };
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 && sameOrigin(a.url, 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 { status: "available", apps: stripSelf(cached.apps) };
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(origin);
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 { status: "available", apps: stripSelf(cached.apps) };
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 import("../server/credential-provider.js");
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 fetch(directoryUrl, {
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
- return {
225
+ const unavailable = {
179
226
  status: "unavailable",
180
- reason: res.status === 401 || res.status === 403
181
- ? "authorization"
182
- : res.status === 429 || res.status >= 500
183
- ? "server-error"
184
- : "http-error",
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
- return {
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 SyntaxError
195
- ? "invalid-response"
196
- : "network",
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
  }>>;
@@ -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 = {
@@ -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({