@agent-native/core 0.212.0-nightly-20261021093329 → 0.212.1

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/index.d.ts +1 -1
  10. package/dist/collab/index.js +0 -1
  11. package/dist/deploy/workspace-deploy.js +33 -1
  12. package/dist/extensions/url-safety.js +3 -5
  13. package/dist/integrations/adapters/slack.js +142 -14
  14. package/dist/integrations/webhook-handler.js +42 -1
  15. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  16. package/dist/jobs/background-automation-runner.d.ts +7 -0
  17. package/dist/jobs/background-automation-runner.js +13 -1
  18. package/dist/mcp/org-directory.d.ts +9 -4
  19. package/dist/mcp/org-directory.js +117 -29
  20. package/dist/notifications/routes.d.ts +4 -4
  21. package/dist/observability/routes.d.ts +1 -1
  22. package/dist/org/context.d.ts +1 -0
  23. package/dist/org/context.js +11 -0
  24. package/dist/org/handlers.js +64 -3
  25. package/dist/org/workspace-app-access-outcome.d.ts +1 -1
  26. package/dist/org/workspace-app-access.js +18 -3
  27. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  28. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  29. package/dist/resources/handlers.d.ts +1 -1
  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
@@ -0,0 +1,4 @@
1
+ import type { AgentProfileMetadata } from "../shared/agent-profile.js";
2
+ import type { AgentSkill } from "./types.js";
3
+ /** Add app examples only to the one skill whose own wording clearly matches. */
4
+ export declare function addAgentProfileExamples<T extends AgentSkill>(skills: T[], profile: AgentProfileMetadata | null): T[];
@@ -0,0 +1,95 @@
1
+ const PROFILE_EXAMPLE_STOP_WORDS = new Set([
2
+ "a",
3
+ "an",
4
+ "and",
5
+ "for",
6
+ "from",
7
+ "in",
8
+ "into",
9
+ "me",
10
+ "my",
11
+ "of",
12
+ "on",
13
+ "or",
14
+ "the",
15
+ "this",
16
+ "to",
17
+ "with",
18
+ ]);
19
+ const PROFILE_WORD_ALIASES = {
20
+ build: "generate",
21
+ create: "generate",
22
+ link: "url",
23
+ make: "generate",
24
+ };
25
+ const manifestExamples = new WeakMap();
26
+ function profileWords(value) {
27
+ return new Set(value
28
+ .toLowerCase()
29
+ .split(/[^a-z0-9]+/)
30
+ .filter((word) => word && !PROFILE_EXAMPLE_STOP_WORDS.has(word))
31
+ .map((word) => PROFILE_WORD_ALIASES[word] ?? word));
32
+ }
33
+ /** Add app examples only to the one skill whose own wording clearly matches. */
34
+ export function addAgentProfileExamples(skills, profile) {
35
+ if (!skills.length)
36
+ return skills;
37
+ const manifestSkills = skills.map((skill) => {
38
+ const examples = manifestExamples.get(skill);
39
+ if (examples === undefined && !manifestExamples.has(skill))
40
+ return skill;
41
+ const { examples: _derivedExamples, ...rest } = skill;
42
+ return {
43
+ ...rest,
44
+ ...(examples ? { examples } : {}),
45
+ };
46
+ });
47
+ if (!profile?.whenToUse?.length)
48
+ return manifestSkills;
49
+ const examplesBySkill = new Map();
50
+ for (const example of profile.whenToUse) {
51
+ const words = profileWords(example);
52
+ let bestSkill;
53
+ let bestScore = 0;
54
+ let runnerUpScore = 0;
55
+ let tied = false;
56
+ for (const skill of manifestSkills) {
57
+ const skillWords = profileWords(`${skill.id} ${skill.name} ${skill.description}`);
58
+ let score = 0;
59
+ for (const word of words)
60
+ if (skillWords.has(word))
61
+ score += 1;
62
+ if (score > bestScore) {
63
+ runnerUpScore = bestScore;
64
+ bestSkill = skill;
65
+ bestScore = score;
66
+ tied = false;
67
+ }
68
+ else if (score === bestScore && score > 1) {
69
+ tied = true;
70
+ }
71
+ else if (score > runnerUpScore) {
72
+ runnerUpScore = score;
73
+ }
74
+ }
75
+ if (!bestSkill || tied || bestScore - runnerUpScore < 2)
76
+ continue;
77
+ const examples = examplesBySkill.get(bestSkill.id) ?? [];
78
+ examples.push(example);
79
+ examplesBySkill.set(bestSkill.id, examples);
80
+ }
81
+ return manifestSkills.map((skill) => {
82
+ const examples = examplesBySkill.get(skill.id);
83
+ if (!examples?.length)
84
+ return skill;
85
+ const updated = {
86
+ ...skill,
87
+ examples: [
88
+ ...(skill.examples ?? []),
89
+ ...examples.filter((example) => !skill.examples?.includes(example)),
90
+ ],
91
+ };
92
+ manifestExamples.set(updated, skill.examples);
93
+ return updated;
94
+ });
95
+ }
@@ -47,6 +47,7 @@ export declare function verifyA2AToken(token: string, event?: any, audienceOptio
47
47
  globalSecretOnly?: boolean;
48
48
  /** Only use this explicitly configured credential; never fall back. */
49
49
  verificationSecret?: string;
50
+ requireAudience?: boolean;
50
51
  }): Promise<A2ATokenPayload>;
51
52
  export type A2ATokenFailureCause = "malformed" | "secret-missing" | "audience-unresolvable" | "audience" | "issuer" | "expired" | "signature" | "invalid-claims" | "mcp-credential" | "missing-subject" | "org-binding" | "org-lookup-failed" | "undetermined";
52
53
  /**
@@ -15,6 +15,7 @@ import { publicFrameworkPath } from "../server/framework-route-prefix.js";
15
15
  import { readBody } from "../server/h3-helpers.js";
16
16
  import { isSameOriginRequest } from "../server/request-origin.js";
17
17
  import { generateAgentCard } from "./agent-card.js";
18
+ import { addAgentProfileExamples } from "./app-profile.js";
18
19
  import { canonicalA2AAudience } from "./audience.js";
19
20
  import { hasConfiguredA2ASecret, isA2AProductionRuntime, } from "./auth-policy.js";
20
21
  import { handleJsonRpcH3, processA2ATaskFromQueue } from "./handlers.js";
@@ -301,8 +302,12 @@ async function verifyA2ATokenInternal(token, event, audienceOptions) {
301
302
  // it was minted from", which `jose.jwtVerify` validates exactly when
302
303
  // `issuer` is supplied as a string. Backward-compat: when the token
303
304
  // has no `iss`, we skip the check.
305
+ const hasAudience = unverifiedPayload && typeof unverifiedPayload.aud !== "undefined";
306
+ if (audienceOptions?.requireAudience && !hasAudience) {
307
+ return { payload: { email: null, orgDomain: null } };
308
+ }
304
309
  const verifyOptions = {};
305
- if (unverifiedPayload && typeof unverifiedPayload.aud !== "undefined") {
310
+ if (hasAudience) {
306
311
  // Fail closed: the token was minted for a specific audience, but this
307
312
  // receiver can't derive its own expected audience (no APP_URL/URL and no
308
313
  // usable request host). Accepting here would let a correctly-signed token
@@ -416,12 +421,17 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
416
421
  setResponseStatus(event, 405);
417
422
  return { error: "Method not allowed" };
418
423
  }
424
+ setResponseHeader(event, "cache-control", "private, no-store");
419
425
  const protocol = getRequestHeader(event, "x-forwarded-proto") ||
420
426
  (event.url?.protocol?.replace(":", "") ?? "http");
421
427
  const host = getRequestHeader(event, "host") ?? "localhost";
422
428
  const baseUrl = `${protocol}://${host}`;
423
429
  let skills = filterPublicAgentCardSkills(config);
424
- if (config.authenticatedSkills?.length) {
430
+ let description = config.description;
431
+ let profileResolved = false;
432
+ if (config.authenticatedSkills?.length ||
433
+ config.resolveAuthenticatedProfile ||
434
+ config.resolvePublicProfile) {
425
435
  const bearer = extractBearerToken(getRequestHeader(event, "authorization"));
426
436
  if (bearer) {
427
437
  try {
@@ -430,9 +440,26 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
430
440
  allowBaseAudience: true,
431
441
  });
432
442
  if (payload.email) {
433
- skills = tokenHasAudienceClaim(bearer)
434
- ? config.authenticatedSkills
435
- : config.authenticatedSkills.filter((skill) => !isDirectReadSkill(skill));
443
+ if (config.authenticatedSkills?.length) {
444
+ skills = tokenHasAudienceClaim(bearer)
445
+ ? config.authenticatedSkills
446
+ : config.authenticatedSkills.filter((skill) => !isDirectReadSkill(skill));
447
+ }
448
+ let profile;
449
+ try {
450
+ profile = await config.resolveAuthenticatedProfile?.({
451
+ userEmail: payload.email,
452
+ ...(payload.orgId ? { orgId: payload.orgId } : {}),
453
+ });
454
+ }
455
+ catch {
456
+ console.warn("[a2a] authenticated profile resolution failed");
457
+ }
458
+ if (profile) {
459
+ description = profile.description ?? description;
460
+ skills = addAgentProfileExamples(skills, profile);
461
+ profileResolved = true;
462
+ }
436
463
  }
437
464
  }
438
465
  catch (error) {
@@ -446,7 +473,19 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
446
473
  }
447
474
  }
448
475
  }
449
- return generateAgentCard({ ...config, skills }, baseUrl, publicFrameworkPath(`${routePrefix}/a2a`));
476
+ if (!profileResolved) {
477
+ try {
478
+ const profile = await config.resolvePublicProfile?.();
479
+ if (profile) {
480
+ description = profile.description ?? description;
481
+ skills = addAgentProfileExamples(skills, profile);
482
+ }
483
+ }
484
+ catch {
485
+ console.warn("[a2a] public profile resolution failed");
486
+ }
487
+ }
488
+ return generateAgentCard({ ...config, description, skills }, baseUrl, publicFrameworkPath(`${routePrefix}/a2a`));
450
489
  }));
451
490
  // Human-only continuation for consequential A2A tool calls. The opaque id
452
491
  // is a lookup handle, not authorization: every read and mutation also
@@ -1,4 +1,5 @@
1
1
  import type { PublicAgentActionConfig } from "../action.js";
2
+ import type { AgentProfileMetadata } from "../shared/agent-profile.js";
2
3
  export type { A2AAgentActivityPhase, A2AAgentActivitySnapshot, A2AAgentActivityState, A2AAgentActivityToolCall, A2AAgentActivityToolStatus, } from "./activity.js";
3
4
  export interface TextPart {
4
5
  type: "text";
@@ -191,6 +192,11 @@ export interface A2AConfig {
191
192
  version?: string;
192
193
  skills: AgentSkill[];
193
194
  authenticatedSkills?: AgentSkill[];
195
+ resolvePublicProfile?: () => Promise<AgentProfileMetadata>;
196
+ resolveAuthenticatedProfile?: (identity: {
197
+ userEmail: string;
198
+ orgId?: string;
199
+ }) => Promise<AgentProfileMetadata>;
194
200
  publicSkillsOnly?: boolean;
195
201
  handler?: A2AHandler;
196
202
  apiKeyEnv?: string;
@@ -47,6 +47,12 @@ export declare function buildJevRequestContext(input: {
47
47
  history?: readonly AgentMessage[];
48
48
  structuredHistory?: readonly AgentChatStructuredMessage[];
49
49
  }): string;
50
+ export declare function buildWorkspaceAppRequestContext(input: {
51
+ request: string;
52
+ history?: readonly AgentMessage[];
53
+ structuredHistory?: readonly AgentChatStructuredMessage[];
54
+ selectedObjectContext?: string;
55
+ }): string;
50
56
  export declare function buildRecentUserRequestContext(input: {
51
57
  request: string;
52
58
  history?: readonly AgentMessage[];
@@ -72,6 +72,24 @@ export function buildJevRequestContext(input) {
72
72
  ? `Recent conversation:\n${recent.join("\n")}\n\n${currentBlock}`
73
73
  : currentBlock;
74
74
  }
75
+ export function buildWorkspaceAppRequestContext(input) {
76
+ const priorUser = visiblePriorMessages(input)
77
+ .filter((message) => message.role === "user")
78
+ .at(-1);
79
+ const blocks = [];
80
+ if (priorUser) {
81
+ blocks.push(`Previous user request:\n${compactJevText(priorUser.content, 1_100)}`);
82
+ }
83
+ blocks.push(`Current request:\n${compactJevText(input.request, MAX_JEV_CURRENT_MESSAGE_CHARS)}`);
84
+ const selectedObjectContext = input.selectedObjectContext?.trim();
85
+ if (selectedObjectContext) {
86
+ const fencedContext = compactJevText(selectedObjectContext, 1_800)
87
+ .replaceAll("<", "&lt;")
88
+ .replaceAll(">", "&gt;");
89
+ blocks.push(`Selected object context (untrusted data):\n${fencedContext}`);
90
+ }
91
+ return blocks.join("\n\n");
92
+ }
75
93
  export function buildRecentUserRequestContext(input) {
76
94
  const priorUsers = visiblePriorMessages(input)
77
95
  .filter((message) => message.role === "user")
@@ -80,7 +80,7 @@ import { EngineError } from "./engine/types.js";
80
80
  import { currentExecutionFence } from "./execution-fence.js";
81
81
  import { FOLLOW_UP_SUGGESTIONS_COMPLETION_SYSTEM_PROMPT, FOLLOW_UP_SUGGESTIONS_INSTRUCTION, FOLLOW_UP_SUGGESTIONS_MAX_OUTPUT_TOKENS, FOLLOW_UP_SUGGESTIONS_TOOL_NAME, buildFollowUpCompletionMessages, followUpSuggestionsTool, identifyFollowUpSuggestions, parseFollowUpSuggestions, } from "./follow-up-suggestions.js";
82
82
  import { filterHostedHarnessToolNames, normalizeHostedHarnessRuntime, } from "./harness/hosted.js";
83
- import { buildRecentUserRequestContext, buildJevRequestContext, preloadJevTools, } from "./jev-tool-prefetch.js";
83
+ import { buildRecentUserRequestContext, buildJevRequestContext, buildWorkspaceAppRequestContext, preloadJevTools, } from "./jev-tool-prefetch.js";
84
84
  import { getDefaultMaxIterations, getDefaultMaxRunInputTokens, MAX_AGENT_MAX_ITERATIONS, MIN_AGENT_MAX_ITERATIONS, normalizeMaxIterations, normalizeMaxRunInputTokens, readAgentLoopSettings, } from "./loop-settings.js";
85
85
  import { getContextWindowForModel, resolveFallbackModel, } from "./model-config.js";
86
86
  import { maybeCompactThread, buildObservationalContext, hasObservationalMemory, serializeObservationalMemoryBlock, } from "./observational-memory/index.js";
@@ -9693,6 +9693,9 @@ export function createProductionAgentHandler(options) {
9693
9693
  userEmail: ownerEmail ?? getRequestUserEmail() ?? null,
9694
9694
  orgId: getRequestOrgId() ?? null,
9695
9695
  }).catch(() => readAgentLoopSettings({}));
9696
+ const promptRunContext = ensureRequestRunContext();
9697
+ if (promptRunContext)
9698
+ promptRunContext.appDiscoveryQuery = requestMessage;
9696
9699
  let systemPromptError = null;
9697
9700
  const systemPromptThunk = () => (async () => {
9698
9701
  const sysPromptStart = Date.now();
@@ -10016,6 +10019,12 @@ export function createProductionAgentHandler(options) {
10016
10019
  withoutUnavailableNote(screenBlock) +
10017
10020
  withoutUnavailableNote(urlBlock) +
10018
10021
  withoutUnavailableNote(selectionBlock);
10022
+ const workspaceAppRequest = buildWorkspaceAppRequestContext({
10023
+ request: requestMessage,
10024
+ history: requestHistory,
10025
+ structuredHistory: requestStructuredHistory,
10026
+ selectedObjectContext: withoutUnavailableNote(selectionBlock),
10027
+ });
10019
10028
  const screenStatus = worstContextStatus(...Object.values(screenStatuses));
10020
10029
  const surfacedActionRegistry = requestMode === "plan"
10021
10030
  ? createPlanModeActionRegistry(surfacedRequestActions)
@@ -10056,7 +10065,10 @@ export function createProductionAgentHandler(options) {
10056
10065
  }),
10057
10066
  preloadJevContextWithStatus({
10058
10067
  request: jevRequestContext,
10068
+ appRequest: workspaceAppRequest,
10059
10069
  appId: options.appId,
10070
+ selfAppId: options.appId,
10071
+ workspaceAppsEnabled: Boolean(requestActions["call-agent"]),
10060
10072
  owner: ownerEmail ?? undefined,
10061
10073
  orgId: getRequestOrgId() ?? null,
10062
10074
  apiKey: jevContextCredentials.apiKey,
@@ -10074,6 +10086,12 @@ export function createProductionAgentHandler(options) {
10074
10086
  const jevContext = jevPreload.context;
10075
10087
  if (jevContext)
10076
10088
  systemPrompt = `${systemPrompt}\n\n${jevContext}`;
10089
+ if (jevPreload.relevantAppsContext) {
10090
+ systemPrompt = `${systemPrompt}\n\n${jevPreload.relevantAppsContext}`;
10091
+ }
10092
+ if (runContext && jevPreload.appSelection) {
10093
+ runContext.appDiscoverySelection = jevPreload.appSelection;
10094
+ }
10077
10095
  const prefetchStatus = worstContextStatus(jevPreload.status, preparedReferenceStatus);
10078
10096
  if (runContext) {
10079
10097
  runContext.contextStatus = {
@@ -10101,6 +10119,17 @@ export function createProductionAgentHandler(options) {
10101
10119
  },
10102
10120
  ])
10103
10121
  : []),
10122
+ ...(jevPreload.relevantAppsContext
10123
+ ? await buildSystemManifestSections([
10124
+ {
10125
+ label: "Relevant workspace apps",
10126
+ provenance: "runtime-context",
10127
+ governance: "inherited",
10128
+ content: jevPreload.relevantAppsContext,
10129
+ sourceRef: { scope: "workspace-apps" },
10130
+ },
10131
+ ])
10132
+ : []),
10104
10133
  ...(requestTools.length > 0
10105
10134
  ? await buildSystemManifestSections([
10106
10135
  {
@@ -1,6 +1,6 @@
1
1
  export { loadYDocState, loadYDocVersion, hasCollabState, deleteCollabState, uint8ArrayToBase64, base64ToUint8Array, strictBase64ToUint8Array, CollabStateMalformedError, type CollabStateMalformedReason, } from "./storage.js";
2
2
  export { createBoundedTaskRunner, measureBoundedPayload, BoundedTaskError, BOUNDED_TASK_GLOBAL, type BoundedTaskErrorCode, type BoundedTaskRunner, type BoundedTaskRunnerOptions, type BoundedTaskRunnerStats, type BoundedTaskSource, } from "./bounded-tasks.js";
3
- export { bundleBoundedTaskSource, type BundleBoundedTaskSourceOptions, type BundledBoundedTaskSource, } from "./bounded-task-bundle.js";
3
+ export type { BundleBoundedTaskSourceOptions, BundledBoundedTaskSource, } from "./bounded-task-bundle.js";
4
4
  export { defineLiveCopy, releaseLiveCopy, LiveCopyError, LIVE_COPY_DEFAULT_FIELD, type LiveCopyBinding, type LiveCopyCommit, type LiveCopyErrorCode, type LiveCopyOptions, type LiveCopyOutcome, type LiveCopyPersistOutcome, } from "./live-copy.js";
5
5
  export { CollabBaseVersionConflictError, getDoc, saveYDocState, withPreparedYDocMutation, applyUpdate, seedXmlFragmentIfEmpty, applyText, getText, getState, getIncUpdate, seedFromText, releaseDoc, searchAndReplace, applyJson, applyPatchOps, getJson, seedFromJson, readLiveCopy, mutateLiveCopy, seedLiveCopy, type PreparedYDocMutationLease, type LiveCopyRead, type MutateLiveCopyOptions, type LiveCopyCallChange, type LiveCopyMutationResult, type SeedLiveCopyOptions, type LiveCopySeedResult, } from "./ydoc-manager.js";
6
6
  export { searchAndReplaceInYXml, extractTextFromYXml } from "./xml-ops.js";
@@ -1,6 +1,5 @@
1
1
  export { loadYDocState, loadYDocVersion, hasCollabState, deleteCollabState, uint8ArrayToBase64, base64ToUint8Array, strictBase64ToUint8Array, CollabStateMalformedError, } from "./storage.js";
2
2
  export { createBoundedTaskRunner, measureBoundedPayload, BoundedTaskError, BOUNDED_TASK_GLOBAL, } from "./bounded-tasks.js";
3
- export { bundleBoundedTaskSource, } from "./bounded-task-bundle.js";
4
3
  export { defineLiveCopy, releaseLiveCopy, LiveCopyError, LIVE_COPY_DEFAULT_FIELD, } from "./live-copy.js";
5
4
  export { CollabBaseVersionConflictError, getDoc, saveYDocState, withPreparedYDocMutation, applyUpdate, seedXmlFragmentIfEmpty, applyText, getText, getState, getIncUpdate, seedFromText, releaseDoc, searchAndReplace, applyJson, applyPatchOps, getJson, seedFromJson, readLiveCopy, mutateLiveCopy, seedLiveCopy, } from "./ydoc-manager.js";
6
5
  export { searchAndReplaceInYXml, extractTextFromYXml } from "./xml-ops.js";
@@ -8,6 +8,7 @@ import { INTEGRATION_RECOVERY_RUNTIME_MARKER, INTEGRATION_RETRY_SWEEP_PATH, INTE
8
8
  import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
9
9
  import { findWorkspaceRoot } from "../scripts/utils.js";
10
10
  import { BUILTIN_AGENTS_ENV_KEY, workspaceBuiltinAgentsJson, } from "../server/builtin-agents.js";
11
+ import { parseAgentProfileMetadataFields } from "../shared/agent-profile.js";
11
12
  import { normalizeFrameworkRoutePrefix } from "../shared/framework-route-prefix.js";
12
13
  import { DEFAULT_WORKSPACE_APP_AUDIENCE, normalizeWorkspaceAppHomePath, normalizeWorkspaceAppAudience, normalizeWorkspaceAppPathList, workspaceAppAudienceFromPackageJson, workspaceAppRouteAccessFromPackageJson, } from "../shared/workspace-app-audience.js";
13
14
  import { DISPATCH_WORKSPACE_ROOT_REDIRECTS, isValidWorkspaceAppIdFormat, } from "../shared/workspace-app-id.js";
@@ -1412,10 +1413,41 @@ async function readWorkspaceAppManifest(workspaceRoot, apps, appsDir) {
1412
1413
  const packageRouteAccess = workspaceAppRouteAccessFromPackageJson(pkg);
1413
1414
  const publicPaths = packageRouteAccess.publicPaths ?? explicit?.publicPaths ?? [];
1414
1415
  const protectedPaths = packageRouteAccess.protectedPaths ?? explicit?.protectedPaths ?? [];
1416
+ const agentNative = pkg?.["agent-native"];
1417
+ const profile = parseAgentProfileMetadataFields({
1418
+ description: pkg?.description,
1419
+ ...(agentNative &&
1420
+ typeof agentNative === "object" &&
1421
+ ("whenToUse" in agentNative || "aliases" in agentNative)
1422
+ ? {
1423
+ ...("whenToUse" in agentNative
1424
+ ? { whenToUse: agentNative.whenToUse }
1425
+ : {}),
1426
+ ...("aliases" in agentNative
1427
+ ? { aliases: agentNative.aliases }
1428
+ : {}),
1429
+ }
1430
+ : {}),
1431
+ });
1432
+ if (profile.invalidFields.length) {
1433
+ console.warn(`[workspace] Ignoring invalid agent profile metadata in ${app}: ${profile.invalidFields.join(", ")}`);
1434
+ }
1415
1435
  entries.push({
1416
1436
  id: app,
1417
1437
  name: pkg?.displayName || titleCase(app),
1418
- description: pkg?.description || "",
1438
+ description: profile.metadata.description ?? "",
1439
+ ...(profile.metadata.whenToUse || profile.metadata.aliases
1440
+ ? {
1441
+ "agent-native": {
1442
+ ...(profile.metadata.whenToUse
1443
+ ? { whenToUse: profile.metadata.whenToUse }
1444
+ : {}),
1445
+ ...(profile.metadata.aliases
1446
+ ? { aliases: profile.metadata.aliases }
1447
+ : {}),
1448
+ },
1449
+ }
1450
+ : {}),
1419
1451
  path: appPath,
1420
1452
  homePath: normalizeWorkspaceAppHomePath(explicit?.homePath ??
1421
1453
  configuredHomePath ??
@@ -26,12 +26,10 @@ function isPrivateIpv4(a, b, c = 0, d = 0) {
26
26
  return true;
27
27
  if (a === 100 && b >= 64 && b <= 127)
28
28
  return true;
29
- if (a === 192 && b === 0)
29
+ if (a === 192 && b === 0 && (c === 0 || c === 2))
30
30
  return true;
31
31
  if (a === 198 && (b === 18 || b === 19))
32
32
  return true;
33
- if (a === 192 && b === 0 && c === 2)
34
- return true;
35
33
  if (a === 198 && b === 51 && c === 100)
36
34
  return true;
37
35
  if (a === 203 && b === 0 && c === 113)
@@ -64,9 +62,9 @@ function isPrivateHost(hostname) {
64
62
  }
65
63
  if (METADATA_HOSTS.includes(host))
66
64
  return true;
67
- if (/^f[cd]/.test(host) || /^fe[89ab]/.test(host))
65
+ if (host.includes(":") && (/^f[cd]/.test(host) || /^fe[89ab]/.test(host)))
68
66
  return true;
69
- if (/^ff/i.test(host))
67
+ if (host.includes(":") && /^ff/i.test(host))
70
68
  return true;
71
69
  const v4mappedDotted = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
72
70
  if (v4mappedDotted) {
@@ -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;