@agent-native/core 0.225.0-nightly-20261105212332 → 0.225.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.
@@ -2,6 +2,7 @@ export declare function isBlockedExtensionUrl(url: string): boolean;
2
2
  export declare function isBlockedExtensionUrlWithDns(url: string): Promise<boolean>;
3
3
  export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonly string[], destinationUrl?: string, options?: {
4
4
  required?: boolean;
5
+ loopbackOnly?: boolean;
5
6
  }): Promise<unknown>;
6
7
  /**
7
8
  * SSRF-safe `fetch` for any server-side request to a user/agent-supplied URL.
@@ -29,6 +30,8 @@ export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonl
29
30
  * example, a credential's origin allowlist) on top of the SSRF checks. It runs
30
31
  * before the initial request and before every redirect hop, so sensitive
31
32
  * headers and bodies are never forwarded to a destination the caller rejects.
33
+ * `allowedLocalhostOrigin` permits only that exact localhost origin and requires
34
+ * a dispatcher that rejects every non-loopback DNS result at connect time.
32
35
  */
33
36
  export declare function ssrfSafeFetch(url: string, init?: RequestInit, options?: {
34
37
  maxRedirects?: number;
@@ -37,6 +40,7 @@ export declare function ssrfSafeFetch(url: string, init?: RequestInit, options?:
37
40
  httpsOnly?: boolean;
38
41
  assertUrlAllowed?: (url: string) => void | Promise<void>;
39
42
  allowedPrivateOrigins?: readonly string[];
43
+ allowedLocalhostOrigin?: string;
40
44
  }): Promise<Response>;
41
45
  export { isBlockedExtensionUrl as isBlockedToolUrl };
42
46
  export { isBlockedExtensionUrlWithDns as isBlockedToolUrlWithDns };
@@ -237,7 +237,7 @@ async function createSsrfSafeDispatcherUncached(allowedPrivateOrigins = [], dest
237
237
  }
238
238
  return new Agent({
239
239
  connect: {
240
- lookup: (hostname, options, callback) => {
240
+ lookup: (hostname, lookupOptions, callback) => {
241
241
  lookup(hostname, { all: true, verbatim: true }, (err, addresses) => {
242
242
  if (err)
243
243
  return callback(err);
@@ -246,13 +246,16 @@ async function createSsrfSafeDispatcherUncached(allowedPrivateOrigins = [], dest
246
246
  : [{ address: addresses, family: 4 }];
247
247
  for (const record of list) {
248
248
  const allowedOrigin = allowedPrivateOriginKeys.has(`${normalizeLookupHostname(hostname)}:${destinationPort}`);
249
- if (isPrivateHost(record.address) && !allowedOrigin) {
250
- const e = new Error(`Connect blocked: ${hostname} resolved to private address ${record.address}`);
249
+ const loopback = record.address === "127.0.0.1" || record.address === "::1";
250
+ if (options.loopbackOnly
251
+ ? !loopback
252
+ : isPrivateHost(record.address) && !allowedOrigin) {
253
+ const e = new Error(`Connect blocked: ${hostname} resolved to ${options.loopbackOnly ? "non-loopback" : "private"} address ${record.address}`);
251
254
  e.code = "EAI_BLOCKED";
252
255
  return callback(e);
253
256
  }
254
257
  }
255
- if (options && options.all) {
258
+ if (lookupOptions.all) {
256
259
  return callback(null, list);
257
260
  }
258
261
  const first = list[0];
@@ -264,6 +267,14 @@ async function createSsrfSafeDispatcherUncached(allowedPrivateOrigins = [], dest
264
267
  }
265
268
  export async function createSsrfSafeDispatcher(allowedPrivateOrigins = [], destinationUrl, options = {}) {
266
269
  const allowedPrivateOrigin = allowedPrivateOriginForDestination(destinationUrl, allowedPrivateOrigins);
270
+ if (options.loopbackOnly) {
271
+ if (!allowedPrivateOrigin ||
272
+ new URL(allowedPrivateOrigin).hostname !== "localhost" ||
273
+ !allowedPrivateOrigins.includes(allowedPrivateOrigin)) {
274
+ throw new Error("SSRF protection requires an exact localhost origin for loopback-only dispatch.");
275
+ }
276
+ options = { ...options, required: true };
277
+ }
267
278
  if (!allowedPrivateOrigin) {
268
279
  sharedSsrfDispatcher ??= createSsrfSafeDispatcherUncached();
269
280
  const dispatcher = await sharedSsrfDispatcher;
@@ -274,6 +285,7 @@ export async function createSsrfSafeDispatcher(allowedPrivateOrigins = [], desti
274
285
  const cacheKey = JSON.stringify([
275
286
  allowedPrivateOrigin,
276
287
  options.required === true,
288
+ options.loopbackOnly === true,
277
289
  ]);
278
290
  let dispatcher = privateSsrfDispatchers.get(cacheKey);
279
291
  if (!dispatcher) {
@@ -320,6 +332,8 @@ export async function createSsrfSafeDispatcher(allowedPrivateOrigins = [], desti
320
332
  * example, a credential's origin allowlist) on top of the SSRF checks. It runs
321
333
  * before the initial request and before every redirect hop, so sensitive
322
334
  * headers and bodies are never forwarded to a destination the caller rejects.
335
+ * `allowedLocalhostOrigin` permits only that exact localhost origin and requires
336
+ * a dispatcher that rejects every non-loopback DNS result at connect time.
323
337
  */
324
338
  export async function ssrfSafeFetch(url, init = {}, options = {}) {
325
339
  const maxRedirects = options.maxRedirects ?? 3;
@@ -339,11 +353,22 @@ export async function ssrfSafeFetch(url, init = {}, options = {}) {
339
353
  };
340
354
  let currentUrl = url;
341
355
  for (let hop = 0; hop <= maxRedirects; hop++) {
356
+ const allowedLocalhostOrigin = (() => {
357
+ if (!options.allowedLocalhostOrigin || !URL.canParse(currentUrl))
358
+ return undefined;
359
+ const parsed = new URL(currentUrl);
360
+ return parsed.hostname === "localhost" &&
361
+ (parsed.protocol === "http:" || parsed.protocol === "https:") &&
362
+ options.allowedLocalhostOrigin === parsed.origin
363
+ ? parsed.origin
364
+ : undefined;
365
+ })();
342
366
  await options.assertUrlAllowed?.(currentUrl);
343
367
  if (options.httpsOnly && new URL(currentUrl).protocol !== "https:") {
344
368
  throw new Error(`SSRF blocked: refusing to fetch non-HTTPS address (${currentUrl})`);
345
369
  }
346
- if (!isAllowedPrivateOrigin(currentUrl) &&
370
+ if (!allowedLocalhostOrigin &&
371
+ !isAllowedPrivateOrigin(currentUrl) &&
347
372
  (await isBlockedExtensionUrlWithDns(currentUrl))) {
348
373
  throw new Error(`SSRF blocked: refusing to fetch private/internal address (${currentUrl})`);
349
374
  }
@@ -351,7 +376,12 @@ export async function ssrfSafeFetch(url, init = {}, options = {}) {
351
376
  ...currentInit,
352
377
  redirect: "manual",
353
378
  };
354
- const dispatcher = await createSsrfSafeDispatcher(options.allowedPrivateOrigins, currentUrl, { required: options.requireDispatcher });
379
+ const dispatcher = await createSsrfSafeDispatcher(allowedLocalhostOrigin
380
+ ? [allowedLocalhostOrigin]
381
+ : options.allowedPrivateOrigins, currentUrl, {
382
+ required: Boolean(allowedLocalhostOrigin || options.requireDispatcher),
383
+ loopbackOnly: Boolean(allowedLocalhostOrigin),
384
+ });
355
385
  if (dispatcher)
356
386
  fetchOpts.dispatcher = dispatcher;
357
387
  const response = await fetch(currentUrl, fetchOpts);
@@ -1,17 +1,17 @@
1
1
  {
2
- "@agent-native/agentkit": "0.225.0-nightly-20261105212332",
2
+ "@agent-native/agentkit": "0.225.0",
3
3
  "@agent-native/code-agents-ui": "0.1.75",
4
- "@agent-native/core": "0.225.0-nightly-20261105212332",
5
- "@agent-native/core-corpus": "0.225.0-nightly-20261105212332",
6
- "@agent-native/creative-context": "0.8.65",
7
- "@agent-native/dispatch": "0.42.1-nightly-20261105212332",
4
+ "@agent-native/core": "0.225.0",
5
+ "@agent-native/core-corpus": "0.225.0",
6
+ "@agent-native/creative-context": "0.8.66",
7
+ "@agent-native/dispatch": "0.42.1",
8
8
  "@agent-native/embedding": "0.2.0",
9
9
  "@agent-native/migrate": "0.0.7",
10
- "@agent-native/otel": "0.1.32",
11
- "@agent-native/pinpoint": "0.1.108",
12
- "@agent-native/recap-cli": "0.5.96",
13
- "@agent-native/scheduling": "0.2.53-nightly-20261105212332",
10
+ "@agent-native/otel": "0.1.33",
11
+ "@agent-native/pinpoint": "0.1.109",
12
+ "@agent-native/recap-cli": "0.5.97",
13
+ "@agent-native/scheduling": "0.2.53",
14
14
  "@agent-native/shared-app-config": "0.1.2",
15
- "@agent-native/skills": "0.4.1-nightly-20261105212332",
16
- "@agent-native/toolkit": "0.225.0-nightly-20261105212332"
15
+ "@agent-native/skills": "0.4.1",
16
+ "@agent-native/toolkit": "0.225.0"
17
17
  }
@@ -20,7 +20,9 @@ function extractMemberEmail(event) {
20
20
  }
21
21
  const nanoid = () => globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
22
22
  Math.random().toString(36).slice(2) + Date.now().toString(36);
23
+ import { workspacePrivateOrigins } from "../a2a/workspace-private-origins.js";
23
24
  import { warnAgent } from "../agent/action-warnings.js";
25
+ import { isFirstPartyApp } from "../app-config/app-identity.js";
24
26
  import { getAppConfig } from "../app-config/index.js";
25
27
  import { routeAuditAttribution } from "../audit/attribution.js";
26
28
  import { recordOrgAdminAuditEvent } from "../audit/org-admin.js";
@@ -31,6 +33,7 @@ import { ssrfSafeFetch } from "../extensions/url-safety.js";
31
33
  import { evaluateFeatureFlagStrict } from "../feature-flags/store.js";
32
34
  import { verifyFederatedWorkspaceIconOwner } from "../icon-assets/workspace-transport.js";
33
35
  import { offboardMember } from "../identity/offboard.js";
36
+ import { normalizeAppBasePath } from "../server/app-base-path.js";
34
37
  import { getAppProductionUrl, resolveAppRuntimeUrl, } from "../server/app-url.js";
35
38
  import { isTrustedSelfHostedRuntime, resolveVercelDeploymentProtectionHeaders, } from "../server/credential-provider.js";
36
39
  import { sendEmail, isEmailConfigured } from "../server/email.js";
@@ -1796,10 +1799,26 @@ export const syncA2ASecretHandler = defineEventHandler(async (event) => {
1796
1799
  ]);
1797
1800
  // Only first-party built-ins and apps mounted on this deployment may receive
1798
1801
  // the organization secret. A workspace manifest can contain external URLs.
1799
- const [discoveredAgents, builtins, workspaceApps] = await Promise.all([
1800
- discoverAgents(),
1802
+ const app = getAppConfig().app;
1803
+ // Use one strict workspace snapshot for both sender identity and discovery.
1804
+ // Other agent sources remain best-effort; they are not workspace recipients.
1805
+ const workspaceApps = await loadWorkspaceAppsManifest(true);
1806
+ const matchingMounts = app.basePath
1807
+ ? (workspaceApps ?? []).filter((entry) => typeof entry.path === "string" &&
1808
+ entry.path.startsWith("/") &&
1809
+ !entry.path.startsWith("//") &&
1810
+ normalizeAppBasePath(entry.path) ===
1811
+ normalizeAppBasePath(app.basePath))
1812
+ : [];
1813
+ const selfAppId = app.workspaceId ??
1814
+ (matchingMounts.length === 1 ? matchingMounts[0]?.id : undefined) ??
1815
+ (!workspaceApps?.length && isFirstPartyApp(app) ? app.slug : undefined);
1816
+ const [discoveredAgents, builtins] = await Promise.all([
1817
+ discoverAgents(selfAppId, {
1818
+ workspaceAppsManifest: workspaceApps,
1819
+ requireResolvableWorkspaceUrls: true,
1820
+ }),
1801
1821
  Promise.resolve(getBuiltinAgentsForSeeding()),
1802
- loadWorkspaceAppsManifest(),
1803
1822
  ]);
1804
1823
  const registeredBuiltins = new Map(builtins.map((agent) => [agent.id, agent.url]));
1805
1824
  const registeredWorkspacePaths = new Map();
@@ -1830,6 +1849,8 @@ export const syncA2ASecretHandler = defineEventHandler(async (event) => {
1830
1849
  };
1831
1850
  const agents = discoveredAgents.filter((agent) => {
1832
1851
  if (agent.source === "builtin") {
1852
+ if (isFirstPartyApp(app) && agent.id === app.slug)
1853
+ return false;
1833
1854
  const trustedUrl = registeredBuiltins.get(agent.id);
1834
1855
  return Boolean(trustedUrl && matchesPath(agent.url, trustedUrl));
1835
1856
  }
@@ -1839,6 +1860,12 @@ export const syncA2ASecretHandler = defineEventHandler(async (event) => {
1839
1860
  const registeredPath = registeredWorkspacePaths.get(agent.id);
1840
1861
  return Boolean(registeredPath && matchesPath(agent.url, registeredPath));
1841
1862
  });
1863
+ if (!selfAppId && agents.some((agent) => agent.source === "workspace")) {
1864
+ throw createError({
1865
+ statusCode: 503,
1866
+ message: "Cannot sync workspace A2A secrets without a workspace app ID or a unique configured app base path",
1867
+ });
1868
+ }
1842
1869
  const results = [];
1843
1870
  await Promise.all(agents.map(async (agent) => {
1844
1871
  try {
@@ -1848,6 +1875,20 @@ export const syncA2ASecretHandler = defineEventHandler(async (event) => {
1848
1875
  audience: canonicalA2AAudience(agent.url),
1849
1876
  });
1850
1877
  const protectionHeaders = resolveVercelDeploymentProtectionHeaders(target);
1878
+ const recipient = new URL(agent.url);
1879
+ const recipientOrigin = recipient.origin;
1880
+ const loopbackHost = /^https?:\/\/(?:127\.0\.0\.1|\[::1\]|localhost)(?=[:/?#]|$)/i;
1881
+ const configuredLoopback = agent.source === "workspace" &&
1882
+ loopbackHost.test(agent.url) &&
1883
+ workspacePrivateOrigins().some((configured) => loopbackHost.test(configured) &&
1884
+ URL.canParse(configured) &&
1885
+ new URL(configured).origin === recipientOrigin);
1886
+ const allowedLocalhostOrigin = configuredLoopback && recipient.hostname === "localhost"
1887
+ ? recipientOrigin
1888
+ : undefined;
1889
+ const allowedPrivateOrigins = configuredLoopback && !allowedLocalhostOrigin
1890
+ ? [recipientOrigin]
1891
+ : [];
1851
1892
  const res = await ssrfSafeFetch(target, {
1852
1893
  method: "POST",
1853
1894
  headers: {
@@ -1857,10 +1898,11 @@ export const syncA2ASecretHandler = defineEventHandler(async (event) => {
1857
1898
  },
1858
1899
  body: JSON.stringify({ secret, orgDomain }),
1859
1900
  }, {
1860
- maxRedirects: 3,
1861
- ...(protectionHeaders["x-vercel-protection-bypass"]
1862
- ? { followRedirects: false }
1863
- : {}),
1901
+ maxRedirects: 0,
1902
+ followRedirects: false,
1903
+ requireDispatcher: true,
1904
+ allowedPrivateOrigins,
1905
+ ...(allowedLocalhostOrigin ? { allowedLocalhostOrigin } : {}),
1864
1906
  });
1865
1907
  if (!res.ok) {
1866
1908
  const text = await res.text().catch(() => "");
@@ -20,7 +20,10 @@ export interface DiscoveredAgent {
20
20
  export interface DiscoverAgentsOptions {
21
21
  preferLocalUrls?: boolean;
22
22
  requireReadableAgentSources?: boolean;
23
+ requireResolvableWorkspaceUrls?: boolean;
23
24
  includePersonalAgents?: boolean;
25
+ /** Reuse a previously loaded manifest for consistent workspace discovery. */
26
+ workspaceAppsManifest?: WorkspaceAppManifestEntry[] | null;
24
27
  }
25
28
  export interface DiscoverOrgDirectoryAgentsOptions {
26
29
  preferLocalUrls?: boolean;
@@ -998,7 +998,20 @@ async function readWorkspaceAppsFromFilesystem(strict = false) {
998
998
  .readdirSync(appsDir, { withFileTypes: true })
999
999
  .filter((entry) => entry.isDirectory())) {
1000
1000
  const appDir = path.join(appsDir, entry.name);
1001
- const pkg = readJson(path.join(appDir, "package.json"));
1001
+ const packagePath = path.join(appDir, "package.json");
1002
+ if (strict) {
1003
+ try {
1004
+ fs.lstatSync(packagePath);
1005
+ }
1006
+ catch (error) {
1007
+ if (error instanceof Error &&
1008
+ "code" in error &&
1009
+ error.code === "ENOENT")
1010
+ continue;
1011
+ throw error;
1012
+ }
1013
+ }
1014
+ const pkg = readJson(packagePath);
1002
1015
  if (!pkg) {
1003
1016
  if (strict)
1004
1017
  throw new Error(`Invalid workspace package: ${entry.name}`);
@@ -1092,7 +1105,9 @@ function workspaceAppUrl(app, hostedLoopbackFallbackUrl) {
1092
1105
  }
1093
1106
  }
1094
1107
  async function discoverWorkspaceAgents(selfAppId, options, strict = false) {
1095
- const workspaceApps = await loadWorkspaceAppsManifest(strict);
1108
+ const workspaceApps = options?.workspaceAppsManifest !== undefined
1109
+ ? options.workspaceAppsManifest
1110
+ : await loadWorkspaceAppsManifest(strict);
1096
1111
  if (!workspaceApps)
1097
1112
  return [];
1098
1113
  const metadataSettings = await readWorkspaceAppMetadataSettingsInternal(strict);
@@ -1105,7 +1120,10 @@ async function discoverWorkspaceAgents(selfAppId, options, strict = false) {
1105
1120
  const url = options?.preferLocalUrls && builtin
1106
1121
  ? resolveAgentUrl(builtin, true)
1107
1122
  : workspaceAppUrl(withOverride, builtin?.url);
1108
- if (strict && (!url || !isAbsoluteHttpUrl(url))) {
1123
+ if ((strict || options?.requireResolvableWorkspaceUrls) &&
1124
+ (!url ||
1125
+ !isAbsoluteHttpUrl(url) ||
1126
+ (options?.requireResolvableWorkspaceUrls && /[?#]/.test(url)))) {
1109
1127
  throw new Error(`Invalid workspace app URL: ${withOverride.id}`);
1110
1128
  }
1111
1129
  if (!url)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.225.0-nightly-20261105212332",
3
+ "version": "0.225.0",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -760,8 +760,8 @@
760
760
  "yaml": "^2.9.0",
761
761
  "yjs": "^13.6.33",
762
762
  "zod": "^4.3.6",
763
- "@agent-native/recap-cli": "^0.5.96",
764
- "@agent-native/agentkit": "^0.225.0-nightly-20261105212332"
763
+ "@agent-native/agentkit": "^0.225.0",
764
+ "@agent-native/recap-cli": "^0.5.97"
765
765
  },
766
766
  "devDependencies": {
767
767
  "@ai-sdk/anthropic": "^3.0.71",
@@ -833,7 +833,7 @@
833
833
  "xlsx": "^0.18.5"
834
834
  },
835
835
  "peerDependencies": {
836
- "@agent-native/core-corpus": "0.225.0-nightly-20261105212332",
836
+ "@agent-native/core-corpus": ">=0.196.0",
837
837
  "@ai-sdk/anthropic": ">=3",
838
838
  "@ai-sdk/cohere": ">=3",
839
839
  "@ai-sdk/google": ">=3",