@agent-native/core 0.224.0-nightly-20261104194955 → 0.224.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.
@@ -1,17 +1,17 @@
1
1
  {
2
- "@agent-native/agentkit": "0.224.0-nightly-20261104194955",
2
+ "@agent-native/agentkit": "0.224.0",
3
3
  "@agent-native/code-agents-ui": "0.1.75",
4
- "@agent-native/core": "0.224.0-nightly-20261104194955",
5
- "@agent-native/core-corpus": "0.224.0-nightly-20261104194955",
6
- "@agent-native/creative-context": "0.8.63",
7
- "@agent-native/dispatch": "0.42.0-nightly-20261104194955",
4
+ "@agent-native/core": "0.224.0",
5
+ "@agent-native/core-corpus": "0.224.0",
6
+ "@agent-native/creative-context": "0.8.65",
7
+ "@agent-native/dispatch": "0.42.0",
8
8
  "@agent-native/embedding": "0.2.0",
9
9
  "@agent-native/migrate": "0.0.7",
10
- "@agent-native/otel": "0.1.30",
11
- "@agent-native/pinpoint": "0.1.106",
12
- "@agent-native/recap-cli": "0.5.94",
13
- "@agent-native/scheduling": "0.2.51-nightly-20261104194955",
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.52",
14
14
  "@agent-native/shared-app-config": "0.1.2",
15
- "@agent-native/skills": "0.4.0-nightly-20261104194955",
16
- "@agent-native/toolkit": "0.224.0-nightly-20261104194955"
15
+ "@agent-native/skills": "0.4.0",
16
+ "@agent-native/toolkit": "0.224.0"
17
17
  }
@@ -1,7 +1,7 @@
1
1
  import { getAppConfig } from "../app-config/store.js";
2
2
  import { safeValue } from "../tracking/redaction.js";
3
3
  import { listTrackingProviders, track } from "../tracking/registry.js";
4
- import { DATABASE_FAILURE_MESSAGE, isDatabaseQueryFailure, } from "./embed-redaction.js";
4
+ import { databaseFailureCode, databaseFailureMessage, isDatabaseQueryFailure, } from "./embed-redaction.js";
5
5
  const CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo";
6
6
  const PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
7
7
  export const MCP_ANALYTICS_EVENTS = {
@@ -206,8 +206,8 @@ export function trackMcpOAuthLoginWallShown(ctx, args) {
206
206
  export function describeMcpError(err) {
207
207
  if (isDatabaseQueryFailure(err)) {
208
208
  return {
209
- errorType: "DrizzleQueryError",
210
- errorMessage: DATABASE_FAILURE_MESSAGE,
209
+ errorType: databaseFailureCode(err) ?? "DrizzleQueryError",
210
+ errorMessage: databaseFailureMessage(err),
211
211
  };
212
212
  }
213
213
  if (err && typeof err === "object") {
@@ -8,7 +8,7 @@ import { getRequestContext, getRequestOrgId, getRequestUserEmail, } from "../ser
8
8
  import { describeMcpError } from "./analytics.js";
9
9
  import { actionApprovalError, actionApprovalReviewResult, buildLinkArtifacts, conciseMcpAppToolText, conciseToolResultText, getMcpDirectoryWidgetResourceUri, hasVerifiedMcpUserIdentity, MCP_ACTION_APPROVAL_INPUT_KEY, mcpAppEmbedOpenLinkMeta, mcpAppOnlyEmbedStartMeta, mcpAppStructuredContent, mcpAppWidgetsEnabledForIdentity, mcpDirectoryWidgetCapabilityForTool, mcpDirectoryWidgetSourceMeta, mcpResponseStructuredContent, mcpResultHasContent, metadataObject, modelVisibleToolResponse, openAiToolResultMeta, PRESERVE_MCP_OBJECT_RESULT, resolveMcpAppResourceSafely, returnsQueryPayload, textPagingInputs, upstreamStatusText, withheldWriteResponse, withoutMcpAppEmbedTicket, withServerMintedMcpAppEmbedStart, } from "./build-server.js";
10
10
  import { mcpCallAttribution } from "./call-attribution.js";
11
- import { DATABASE_FAILURE_MESSAGE, describeThrownForLog, HIDDEN_EMBED_URL, isDatabaseQueryFailure, isEmbedStartUrl, redactEmbedText, } from "./embed-redaction.js";
11
+ import { databaseFailureMessage, describeThrownForLog, HIDDEN_EMBED_URL, isDatabaseQueryFailure, isEmbedStartUrl, redactEmbedText, } from "./embed-redaction.js";
12
12
  import { embedCredentialSources, mcpActionErrorCode, redactEmbedCredentials, returnedResult, runMcpActionCallUnredacted, toJsonData, UnreadableResultError, } from "./execute-action-call.js";
13
13
  import { mcpResponseOutputCheck } from "./output-contract.js";
14
14
  export function createDirectActionCall(scope) {
@@ -45,7 +45,7 @@ export function createDirectActionCall(scope) {
45
45
  : callableActions[name]?.mcpOutputContract;
46
46
  const errorText = (err) => {
47
47
  if (isDatabaseQueryFailure(err))
48
- return DATABASE_FAILURE_MESSAGE;
48
+ return databaseFailureMessage(err);
49
49
  const projectedError = directoryCatalog && config.directoryProfile?.projectResult
50
50
  ? config.directoryProfile.projectResult(name, err.message)
51
51
  : err.message;
@@ -13,6 +13,14 @@ export declare const HIDDEN_EMBED_URL = "[hidden embed URL]";
13
13
  export declare const DATABASE_FAILURE_MESSAGE = "The database rejected the request.";
14
14
  /** Drizzle puts a failed query's SQL and bound parameters in its message. */
15
15
  export declare function isDatabaseQueryFailure(error: unknown): boolean;
16
+ export declare function databaseFailureCode(error: unknown): string | undefined;
17
+ /**
18
+ * The client-safe text for a failure `isDatabaseQueryFailure` recognizes. It
19
+ * names the code without saying what it means, because one code has several
20
+ * causes: 57014 is any cancellation, and CONNECT_TIMEOUT is `withDbTimeout` or
21
+ * the driver's wait for a connection.
22
+ */
23
+ export declare function databaseFailureMessage(error: unknown): string;
16
24
  /** Replaces a value already being serialized higher up the same result. */
17
25
  export declare const CIRCULAR_RESULT = "[circular result]";
18
26
  /** Keys removed from every result object, whatever their value. */
@@ -16,6 +16,27 @@ export const DATABASE_FAILURE_MESSAGE = "The database rejected the request.";
16
16
  export function isDatabaseQueryFailure(error) {
17
17
  return error instanceof Error && error.message.startsWith("Failed query:");
18
18
  }
19
+ // A driver's error code names the failure without any of the query's values.
20
+ // Anything else in that field stays hidden along with the message.
21
+ const DATABASE_ERROR_CODE = /^[A-Z0-9_]{1,40}$/;
22
+ export function databaseFailureCode(error) {
23
+ const code = error.cause?.code;
24
+ return typeof code === "string" && DATABASE_ERROR_CODE.test(code)
25
+ ? code
26
+ : undefined;
27
+ }
28
+ /**
29
+ * The client-safe text for a failure `isDatabaseQueryFailure` recognizes. It
30
+ * names the code without saying what it means, because one code has several
31
+ * causes: 57014 is any cancellation, and CONNECT_TIMEOUT is `withDbTimeout` or
32
+ * the driver's wait for a connection.
33
+ */
34
+ export function databaseFailureMessage(error) {
35
+ const code = databaseFailureCode(error);
36
+ return code
37
+ ? `The database request failed (${code}).`
38
+ : DATABASE_FAILURE_MESSAGE;
39
+ }
19
40
  /** Replaces a value already being serialized higher up the same result. */
20
41
  export const CIRCULAR_RESULT = "[circular result]";
21
42
  /** Keys removed from every result object, whatever their value. */
@@ -6,7 +6,7 @@ import { MCP_ACTION_RESULT_MARKER, isMcpActionResult, mcpActionResultText, } fro
6
6
  import { assertServicePrincipalMayCall, assertServicePrincipalMayRun, recordServicePrincipalDenial, ServicePrincipalRefusedError, } from "../org/service-principal-guard.js";
7
7
  import { getRequestOrgId, getRequestUserEmail, } from "../server/request-context.js";
8
8
  import { answerMcpApprovalPrompt, consumeMcpApprovalGrant, consumeReviewedMcpApprovalGrant, createMcpApprovalGrant, findRecentMcpApprovalRun, storedApprovalArguments, recordMcpApprovalRun, } from "./approval-store.js";
9
- import { CIRCULAR_RESULT, EMBED_RESULT_SENSITIVE_KEYS, DATABASE_FAILURE_MESSAGE, HIDDEN_EMBED_URL, IMAGE_BYTES, isDatabaseQueryFailure, isEmbedCredentialKey, readString, redactEmbedText, } from "./embed-redaction.js";
9
+ import { CIRCULAR_RESULT, EMBED_RESULT_SENSITIVE_KEYS, databaseFailureMessage, HIDDEN_EMBED_URL, IMAGE_BYTES, isDatabaseQueryFailure, isEmbedCredentialKey, readString, redactEmbedText, } from "./embed-redaction.js";
10
10
  import { hasMcpOAuthScope } from "./oauth-token.js";
11
11
  /**
12
12
  * The authorization, approval, execution and result-sanitizing steps of an
@@ -504,7 +504,7 @@ function failedOutcome(stage, error) {
504
504
  let errorCode;
505
505
  try {
506
506
  const raw = isDatabaseQueryFailure(error)
507
- ? DATABASE_FAILURE_MESSAGE
507
+ ? databaseFailureMessage(error)
508
508
  : error instanceof Error
509
509
  ? error.message
510
510
  : error;
@@ -1447,9 +1447,11 @@ function webMcpManifestEtag(manifest) {
1447
1447
  function ifNoneMatchIncludes(header, etag) {
1448
1448
  if (!header)
1449
1449
  return false;
1450
+ // A compressed response reaches the browser with "-df" on its tag (seen on
1451
+ // our Netlify hosts), and the browser sends that tag back as it received it.
1450
1452
  return header
1451
1453
  .split(",")
1452
- .some((tag) => tag.trim().replace(/^W\//, "") === etag);
1454
+ .some((tag) => tag.trim().replace(/^W\//, "").replace(/-df"$/, '"') === etag);
1453
1455
  }
1454
1456
  export function mountWebMcpActionRoutes(nitroApp, actions, options) {
1455
1457
  const eligible = Object.fromEntries(Object.entries(actions).filter(([name, entry]) => /^[A-Za-z0-9_.-]{1,128}$/.test(name) &&
@@ -56,6 +56,20 @@ export declare function resolveAccess(resourceType: string, resourceId: string,
56
56
  export declare function resolveAccess(resourceType: string, resourceId: string, rawCtx: AccessContext | undefined, options: {
57
57
  skipResourceBody: true;
58
58
  }): Promise<ResolvedAccessProjected | null>;
59
+ /**
60
+ * {@link resolveAccess} for a list of resources of one type: the ids the
61
+ * viewer can open, mapped to their access. Each decision is the same one
62
+ * `resolveAccess` makes. What changes is the reads: one query loads the rows,
63
+ * one loads their shares, and org membership, group membership, and a
64
+ * registration's `canManageAccessKey` answers are looked up once, not once per
65
+ * resource. Ids that do not exist or that the viewer cannot open are left out.
66
+ */
67
+ export declare function resolveAccessMany(resourceType: string, resourceIds: string[], rawCtx?: AccessContext, options?: {
68
+ skipResourceBody?: false;
69
+ }): Promise<Map<string, ResolvedAccess>>;
70
+ export declare function resolveAccessMany(resourceType: string, resourceIds: string[], rawCtx: AccessContext | undefined, options: {
71
+ skipResourceBody: true;
72
+ }): Promise<Map<string, ResolvedAccessProjected>>;
59
73
  /**
60
74
  * Host hook: for resources that live inside another product (a design bound
61
75
  * to a Builder project), the host is the authority on who may open them. The
@@ -1,4 +1,4 @@
1
- import { and, eq, isNull, or, sql } from "drizzle-orm";
1
+ import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
2
2
  import { drizzle as drizzleProxy } from "drizzle-orm/pg-proxy";
3
3
  import { getScopedDbExec, withDbExec } from "../db/client.js";
4
4
  import { evaluateFeatureFlagStrict } from "../feature-flags/store.js";
@@ -389,7 +389,7 @@ export function isResourceAvailable(reg, resource) {
389
389
  function hasDynamicPublicAccessRoleResolver(reg) {
390
390
  return typeof reg.publicAccessRole === "function";
391
391
  }
392
- async function loadResourceForAccess(reg, resourceId, options = {}) {
392
+ async function loadRowsForAccess(reg, where, options = {}) {
393
393
  const db = reg.getDb();
394
394
  // Hooks that receive the row may read any column, so they get all of it.
395
395
  const useProjection = options.skipResourceBody === true &&
@@ -402,10 +402,7 @@ async function loadResourceForAccess(reg, resourceId, options = {}) {
402
402
  const query = !projectedColumns && omittedColumnNames.size === 0
403
403
  ? db.select()
404
404
  : db.select(selectExistingColumns(projectedColumns ?? reg.resourceTable, omittedColumnNames));
405
- const [resource] = await query
406
- .from(reg.resourceTable)
407
- .where(eq(reg.resourceTable.id, resourceId));
408
- return resource ?? null;
405
+ return await query.from(reg.resourceTable).where(where);
409
406
  }
410
407
  catch (err) {
411
408
  const missing = missingColumnName(err);
@@ -415,7 +412,7 @@ async function loadResourceForAccess(reg, resourceId, options = {}) {
415
412
  console.warn(`[sharing] ${reg.type} access lookup omitted missing column ${missing}`);
416
413
  }
417
414
  }
418
- throw new Error(`Could not load ${reg.type} ${resourceId}: too many missing resource columns`);
415
+ throw new Error(`Could not load ${reg.type} rows: too many missing resource columns`);
419
416
  }
420
417
  export async function resolveAccess(resourceType, resourceId, rawCtx = currentAccess(), options = {}) {
421
418
  const { access } = await inAccessTransaction(rawCtx, () => loadAndResolveAccess(resourceType, resourceId, rawCtx, options));
@@ -424,7 +421,7 @@ export async function resolveAccess(resourceType, resourceId, rawCtx = currentAc
424
421
  function inAccessTransaction(ctx, run) {
425
422
  return ctx.transaction ? withDbExec(ctx.transaction, run) : run();
426
423
  }
427
- async function loadAndResolveAccess(resourceType, resourceId, rawCtx, options) {
424
+ function scopeAccess(resourceType, rawCtx) {
428
425
  const registered = requireShareableResource(resourceType);
429
426
  const transaction = rawCtx.transaction ?? getScopedDbExec();
430
427
  const transactionDb = transaction
@@ -437,8 +434,11 @@ async function loadAndResolveAccess(resourceType, resourceId, rawCtx, options) {
437
434
  const reg = transactionDb
438
435
  ? { ...registered, getDb: () => transactionDb }
439
436
  : registered;
440
- const ctx = resolveRegisteredAccessContext(reg, transactionCtx);
441
- const resource = await loadResourceForAccess(reg, resourceId, options);
437
+ return { reg, ctx: resolveRegisteredAccessContext(reg, transactionCtx) };
438
+ }
439
+ async function loadAndResolveAccess(resourceType, resourceId, rawCtx, options) {
440
+ const { reg, ctx } = scopeAccess(resourceType, rawCtx);
441
+ const [resource] = await loadRowsForAccess(reg, eq(reg.resourceTable.id, resourceId), options);
442
442
  if (!resource)
443
443
  return { access: null, resource: null, reg };
444
444
  return {
@@ -447,6 +447,76 @@ async function loadAndResolveAccess(resourceType, resourceId, rawCtx, options) {
447
447
  reg,
448
448
  };
449
449
  }
450
+ const RESOLVE_MANY_ROW_CHUNK = 500;
451
+ const RESOLVE_MANY_CONCURRENCY = 8;
452
+ export async function resolveAccessMany(resourceType, resourceIds, rawCtx = currentAccess(), options = {}) {
453
+ return inAccessTransaction(rawCtx, async () => {
454
+ const { reg, ctx } = scopeAccess(resourceType, rawCtx);
455
+ const ids = [...new Set(resourceIds)];
456
+ const resources = [];
457
+ for (let start = 0; start < ids.length; start += RESOLVE_MANY_ROW_CHUNK) {
458
+ resources.push(...(await loadRowsForAccess(reg, inArray(reg.resourceTable.id, ids.slice(start, start + RESOLVE_MANY_ROW_CHUNK)), options)));
459
+ }
460
+ const batch = {
461
+ memberships: new Map(),
462
+ groupMemberships: new Map(),
463
+ managers: new Map(),
464
+ shares: await loadSharesForAccess(reg, resources.map((resource) => resource.id), ctx),
465
+ };
466
+ const access = new Map();
467
+ for (let start = 0; start < resources.length; start += RESOLVE_MANY_CONCURRENCY) {
468
+ await Promise.all(resources
469
+ .slice(start, start + RESOLVE_MANY_CONCURRENCY)
470
+ .map(async (resource) => {
471
+ const resolved = await accessToResource(reg, resource.id, resource, ctx, batch);
472
+ if (resolved)
473
+ access.set(resource.id, resolved);
474
+ }));
475
+ }
476
+ return access;
477
+ });
478
+ }
479
+ /**
480
+ * The grants {@link highestShareRole} reads, for many resources at once: the
481
+ * viewer's own user grants, their org's grants, and every group grant.
482
+ */
483
+ async function loadSharesForAccess(reg, resourceIds, ctx) {
484
+ const byResource = new Map();
485
+ const normalizedUserEmail = normalizeEmailForAccess(ctx.userEmail);
486
+ const principalClauses = principalShareClauses(reg, ctx);
487
+ if (reg.supportsGroupShares && normalizedUserEmail) {
488
+ principalClauses.push(eq(reg.sharesTable.principalType, "group"));
489
+ }
490
+ if (resourceIds.length === 0 || principalClauses.length === 0) {
491
+ return byResource;
492
+ }
493
+ const db = reg.getDb();
494
+ for (let start = 0; start < resourceIds.length; start += RESOLVE_MANY_ROW_CHUNK) {
495
+ const rows = (await db
496
+ .select({
497
+ resourceId: reg.sharesTable.resourceId,
498
+ principalType: reg.sharesTable.principalType,
499
+ principalId: reg.sharesTable.principalId,
500
+ role: reg.sharesTable.role,
501
+ })
502
+ .from(reg.sharesTable)
503
+ .where(and(inArray(reg.sharesTable.resourceId, resourceIds.slice(start, start + RESOLVE_MANY_ROW_CHUNK)), or(...principalClauses))));
504
+ for (const row of rows) {
505
+ const list = byResource.get(row.resourceId) ?? [];
506
+ list.push(row);
507
+ byResource.set(row.resourceId, list);
508
+ }
509
+ }
510
+ return byResource;
511
+ }
512
+ function memoized(cache, key, load) {
513
+ let value = cache.get(key);
514
+ if (!value) {
515
+ value = load();
516
+ cache.set(key, value);
517
+ }
518
+ return value;
519
+ }
450
520
  const hostAccessResolvers = new Map();
451
521
  export function registerHostAccessResolver(resourceType, resolver, options) {
452
522
  if (resolver) {
@@ -459,21 +529,27 @@ export function registerHostAccessResolver(resourceType, resolver, options) {
459
529
  hostAccessResolvers.delete(resourceType);
460
530
  }
461
531
  }
462
- async function ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail) {
532
+ async function ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail, batch) {
463
533
  if (normalizedUserEmail &&
464
534
  normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&
465
535
  ownerMatchesActiveScope(reg, resource, ctx)) {
466
536
  return "owner";
467
537
  }
468
- if (reg.canManageAccess && (await reg.canManageAccess(resource, ctx))) {
469
- return "admin";
538
+ if (reg.canManageAccess) {
539
+ const manage = reg.canManageAccess;
540
+ const key = batch && reg.canManageAccessKey?.(resource, ctx);
541
+ const manages = batch && key != null
542
+ ? await memoized(batch.managers, key, async () => Boolean(await manage(resource, ctx)))
543
+ : await manage(resource, ctx);
544
+ if (manages)
545
+ return "admin";
470
546
  }
471
547
  return undefined;
472
548
  }
473
- async function accessToResource(reg, resourceId, resource, ctx) {
549
+ async function accessToResource(reg, resourceId, resource, ctx, batch) {
474
550
  const { userEmail } = ctx;
475
551
  const normalizedUserEmail = normalizeEmailForAccess(userEmail);
476
- const memberships = new Map();
552
+ const memberships = batch?.memberships ?? new Map();
477
553
  const isOrgMember = (orgId) => {
478
554
  let membership = memberships.get(orgId);
479
555
  if (!membership) {
@@ -492,12 +568,12 @@ async function accessToResource(reg, resourceId, resource, ctx) {
492
568
  : undefined;
493
569
  if (hosted === null)
494
570
  return null;
495
- const managingRole = await ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail);
571
+ const managingRole = await ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail, batch);
496
572
  if (managingRole)
497
573
  return { role: managingRole, resource };
498
574
  if (hosted !== undefined)
499
575
  return { role: hosted, resource };
500
- const role = await highestShareRole(reg, resourceId, ctx, resource, isOrgMember);
576
+ const role = await highestShareRole(reg, resourceId, ctx, resource, isOrgMember, batch);
501
577
  const shareRole = role === "admin" && reg.canManageAccess ? "editor" : role;
502
578
  if (resource.visibility === "public" && reg.allowPublic !== false) {
503
579
  const publicRole = await publicAccessRoleForResource(reg, resource, ctx);
@@ -545,7 +621,18 @@ export async function resolveAccessStatus(resourceType, resourceId, ctx = curren
545
621
  return { state: available ? "allowed" : "trashed", role: access.role };
546
622
  });
547
623
  }
548
- async function highestShareRole(reg, resourceId, ctx, resource, isOrgMember) {
624
+ function principalShareClauses(reg, ctx) {
625
+ const normalizedUserEmail = normalizeEmailForAccess(ctx.userEmail);
626
+ const clauses = [];
627
+ if (normalizedUserEmail) {
628
+ clauses.push(and(eq(reg.sharesTable.principalType, "user"), emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail)));
629
+ }
630
+ if (ctx.orgId) {
631
+ clauses.push(and(eq(reg.sharesTable.principalType, "org"), eq(reg.sharesTable.principalId, ctx.orgId)));
632
+ }
633
+ return clauses;
634
+ }
635
+ async function highestShareRole(reg, resourceId, ctx, resource, isOrgMember, batch) {
549
636
  const { userEmail, orgId } = ctx;
550
637
  const normalizedUserEmail = normalizeEmailForAccess(userEmail);
551
638
  if (!normalizedUserEmail && !orgId)
@@ -553,23 +640,20 @@ async function highestShareRole(reg, resourceId, ctx, resource, isOrgMember) {
553
640
  if (!explicitSharesAllowedForResource(reg, resource, ctx))
554
641
  return null;
555
642
  const db = reg.getDb();
556
- const principalClauses = [];
557
- if (normalizedUserEmail) {
558
- principalClauses.push(and(eq(reg.sharesTable.principalType, "user"), emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail)));
559
- }
560
- if (orgId) {
561
- principalClauses.push(and(eq(reg.sharesTable.principalType, "org"), eq(reg.sharesTable.principalId, orgId)));
562
- }
643
+ const principalClauses = principalShareClauses(reg, ctx);
563
644
  if (principalClauses.length === 0)
564
645
  return null;
565
- const rows = await db
566
- .select({
567
- role: reg.sharesTable.role,
568
- principalType: reg.sharesTable.principalType,
569
- })
570
- .from(reg.sharesTable)
571
- .where(and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)))
572
- .limit(10);
646
+ const prefetched = batch?.shares.get(resourceId) ?? [];
647
+ const rows = batch
648
+ ? prefetched.filter((row) => row.principalType !== "group")
649
+ : await db
650
+ .select({
651
+ role: reg.sharesTable.role,
652
+ principalType: reg.sharesTable.principalType,
653
+ })
654
+ .from(reg.sharesTable)
655
+ .where(and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)))
656
+ .limit(10);
573
657
  let best = null;
574
658
  const grantRows = rows;
575
659
  for (const row of grantRows) {
@@ -588,18 +672,23 @@ async function highestShareRole(reg, resourceId, ctx, resource, isOrgMember) {
588
672
  }
589
673
  }
590
674
  if (reg.supportsGroupShares && normalizedUserEmail && resource.orgId) {
591
- const groupRows = await db
592
- .select({
593
- principalId: reg.sharesTable.principalId,
594
- role: reg.sharesTable.role,
595
- })
596
- .from(reg.sharesTable)
597
- .where(and(eq(reg.sharesTable.resourceId, resourceId), eq(reg.sharesTable.principalType, "group")));
675
+ const groupRows = batch
676
+ ? prefetched.filter((row) => row.principalType === "group")
677
+ : await db
678
+ .select({
679
+ principalId: reg.sharesTable.principalId,
680
+ role: reg.sharesTable.role,
681
+ })
682
+ .from(reg.sharesTable)
683
+ .where(and(eq(reg.sharesTable.resourceId, resourceId), eq(reg.sharesTable.principalType, "group")));
598
684
  if (groupRows.length > 0 && (await isOrgMember(resource.orgId))) {
599
685
  for (const row of groupRows) {
600
- if (await workspaceUserGroupsIncludeUser(resource.orgId, [row.principalId], normalizedUserEmail)) {
686
+ const includesUser = () => workspaceUserGroupsIncludeUser(resource.orgId, [row.principalId], normalizedUserEmail);
687
+ const member = batch
688
+ ? await memoized(batch.groupMemberships, `${resource.orgId}\u0000${row.principalId}`, includesUser)
689
+ : await includesUser();
690
+ if (member)
601
691
  best = higherShareRole(row.role, best);
602
- }
603
692
  }
604
693
  }
605
694
  }
@@ -1,5 +1,5 @@
1
1
  export { ownableColumns, createSharesTable, roleSatisfies, ROLE_RANK, type Visibility, type ShareRole, type PrincipalType, } from "./schema.js";
2
2
  export { registerShareableResource, getShareableResource, requireShareableResource, listShareableResources, type ShareableResourceRegistration, type ShareEmailExtras, } from "./registry.js";
3
- export { accessFilter, resolveAccess, assertAccess, currentAccess, ForbiddenError, isResourceAvailable, resolveAccessStatus, registerHostAccessResolver, type HostAccessResolver, type AccessContext, type ResolvedAccess, type ResourceAccessState, type ResourceAccessStatus, } from "./access.js";
3
+ export { accessFilter, resolveAccess, resolveAccessMany, assertAccess, currentAccess, ForbiddenError, isResourceAvailable, resolveAccessStatus, registerHostAccessResolver, type HostAccessResolver, type AccessContext, type ResolvedAccess, type ResourceAccessState, type ResourceAccessStatus, } from "./access.js";
4
4
  export { ACCESS_REQUEST_NOTE_MAX_LENGTH, accessRequestReviewPath, approveAccessRequest, declineAccessRequest, getAccessRequestReview, listResourceAccessRequests, requestResourceAccess, resolveLinkStatus, type AccessGrantedEmail, type AccessRequestReview, type RequestResourceAccessResult, type ResourceAccessRequestList, type ResourceLinkStatus, type ViewerAccessRequest, } from "./access-requests.js";
5
5
  export { filterRecipientsByResourceAccess, type FilterRecipientsInput, } from "./recipients.js";
@@ -1,5 +1,5 @@
1
1
  export { ownableColumns, createSharesTable, roleSatisfies, ROLE_RANK, } from "./schema.js";
2
2
  export { registerShareableResource, getShareableResource, requireShareableResource, listShareableResources, } from "./registry.js";
3
- export { accessFilter, resolveAccess, assertAccess, currentAccess, ForbiddenError, isResourceAvailable, resolveAccessStatus, registerHostAccessResolver, } from "./access.js";
3
+ export { accessFilter, resolveAccess, resolveAccessMany, assertAccess, currentAccess, ForbiddenError, isResourceAvailable, resolveAccessStatus, registerHostAccessResolver, } from "./access.js";
4
4
  export { ACCESS_REQUEST_NOTE_MAX_LENGTH, accessRequestReviewPath, approveAccessRequest, declineAccessRequest, getAccessRequestReview, listResourceAccessRequests, requestResourceAccess, resolveLinkStatus, } from "./access-requests.js";
5
5
  export { filterRecipientsByResourceAccess, } from "./recipients.js";
@@ -86,6 +86,17 @@ export interface ShareableResourceRegistration {
86
86
  userEmail?: string;
87
87
  orgId?: string;
88
88
  }) => boolean | Promise<boolean>;
89
+ /**
90
+ * Resources that return the same key get the same `canManageAccess` answer
91
+ * for the same viewer, so `resolveAccessMany` asks once per key. Return
92
+ * `undefined` for a resource whose answer depends on more than the key.
93
+ */
94
+ canManageAccessKey?: (resource: {
95
+ orgId?: string | null;
96
+ }, ctx: {
97
+ userEmail?: string;
98
+ orgId?: string;
99
+ }) => string | undefined;
89
100
  resolveAccessContext?: (ctx: {
90
101
  userEmail?: string;
91
102
  orgId?: string;
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
174
174
 
175
175
  `authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
176
176
 
177
+ ## Check many resources at once
178
+
179
+ Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
180
+
177
181
  ## Create actions must set owner
178
182
 
179
183
  When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
174
174
 
175
175
  `authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
176
176
 
177
+ ## Check many resources at once
178
+
179
+ Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
180
+
177
181
  ## Create actions must set owner
178
182
 
179
183
  When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
174
174
 
175
175
  `authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
176
176
 
177
+ ## Check many resources at once
178
+
179
+ Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
180
+
177
181
  ## Create actions must set owner
178
182
 
179
183
  When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
174
174
 
175
175
  `authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
176
176
 
177
+ ## Check many resources at once
178
+
179
+ Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
180
+
177
181
  ## Create actions must set owner
178
182
 
179
183
  When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.224.0-nightly-20261104194955",
3
+ "version": "0.224.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/agentkit": "^0.224.0-nightly-20261104194955",
764
- "@agent-native/recap-cli": "^0.5.94"
763
+ "@agent-native/recap-cli": "^0.5.96",
764
+ "@agent-native/agentkit": "^0.224.0"
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.224.0-nightly-20261104194955",
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",
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
174
174
 
175
175
  `authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
176
176
 
177
+ ## Check many resources at once
178
+
179
+ Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
180
+
177
181
  ## Create actions must set owner
178
182
 
179
183
  When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
174
174
 
175
175
  `authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
176
176
 
177
+ ## Check many resources at once
178
+
179
+ Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
180
+
177
181
  ## Create actions must set owner
178
182
 
179
183
  When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
174
174
 
175
175
  `authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
176
176
 
177
+ ## Check many resources at once
178
+
179
+ Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
180
+
177
181
  ## Create actions must set owner
178
182
 
179
183
  When inserting a new row, fill `ownerEmail` and `orgId` from the request context:
@@ -174,6 +174,10 @@ For delete actions use `"admin"` (or fold in `"owner"` to require the real owner
174
174
 
175
175
  `authorize` is a different axis, not an alternative: it gates whether the caller may run the operation at all, while `assertAccess` scopes which row they may touch. A write action restricted to some teammates needs both — `authorize: appAccess.requireAny(...)` on the action, `assertAccess` inside `run`.
176
176
 
177
+ ## Check many resources at once
178
+
179
+ Never call `resolveAccess` in a loop over a list: each call runs several queries, so a page that checks every row pays that per row. When rows come from somewhere `accessFilter` cannot reach (a manifest, a gateway response), use `resolveAccessMany("deck", ids, ctx, { skipResourceBody: true })`. It returns a `Map` of the ids the viewer can open, makes the same decision as `resolveAccess`, and reads rows, shares, org membership, and group membership once. Give a registration that uses `canManageAccess` a `canManageAccessKey` (for example `resource.orgId`) so the manager check is asked once per key, not once per row.
180
+
177
181
  ## Create actions must set owner
178
182
 
179
183
  When inserting a new row, fill `ownerEmail` and `orgId` from the request context: