@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,
|
|
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
|
-
|
|
250
|
-
|
|
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 (
|
|
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 (!
|
|
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(
|
|
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
|
|
2
|
+
"@agent-native/agentkit": "0.225.0",
|
|
3
3
|
"@agent-native/code-agents-ui": "0.1.75",
|
|
4
|
-
"@agent-native/core": "0.225.0
|
|
5
|
-
"@agent-native/core-corpus": "0.225.0
|
|
6
|
-
"@agent-native/creative-context": "0.8.
|
|
7
|
-
"@agent-native/dispatch": "0.42.1
|
|
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.
|
|
11
|
-
"@agent-native/pinpoint": "0.1.
|
|
12
|
-
"@agent-native/recap-cli": "0.5.
|
|
13
|
-
"@agent-native/scheduling": "0.2.53
|
|
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
|
|
16
|
-
"@agent-native/toolkit": "0.225.0
|
|
15
|
+
"@agent-native/skills": "0.4.1",
|
|
16
|
+
"@agent-native/toolkit": "0.225.0"
|
|
17
17
|
}
|
package/dist/org/handlers.js
CHANGED
|
@@ -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
|
|
1800
|
-
|
|
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:
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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/
|
|
764
|
-
"@agent-native/
|
|
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.
|
|
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",
|