@agent-native/core 0.211.0 → 0.212.0-nightly-20261021010049

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 (91) hide show
  1. package/README.md +1 -1
  2. package/dist/app-config/analytics.d.ts +1 -0
  3. package/dist/app-config/analytics.js +7 -0
  4. package/dist/app-config/schema.d.ts +1 -0
  5. package/dist/chat-threads/store.d.ts +28 -2
  6. package/dist/chat-threads/store.js +75 -13
  7. package/dist/cli/template-baseline.js +18 -5
  8. package/dist/cli/template-sync.js +100 -19
  9. package/dist/client/agent-chat/index.d.ts +2 -1
  10. package/dist/client/agent-chat/index.js +2 -1
  11. package/dist/client/chat/agentkit-protocol.js +21 -1
  12. package/dist/client/chat/run-outcome.d.ts +1 -0
  13. package/dist/client/chat/runtime.js +6 -0
  14. package/dist/client/sharing/index.d.ts +2 -1
  15. package/dist/client/sharing/index.js +2 -1
  16. package/dist/client/sharing/share-controller-helpers.d.ts +14 -1
  17. package/dist/client/sharing/share-controller-helpers.js +46 -1
  18. package/dist/client/sharing/useShareButtonController.d.ts +12 -0
  19. package/dist/client/sharing/useShareButtonController.js +76 -57
  20. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  21. package/dist/client/sharing/useShareDialogController.js +77 -49
  22. package/dist/client/sharing/useShareVisibilityTransition.d.ts +68 -0
  23. package/dist/client/sharing/useShareVisibilityTransition.js +224 -0
  24. package/dist/client/use-chat-thread-list.d.ts +40 -0
  25. package/dist/client/use-chat-thread-list.js +210 -0
  26. package/dist/client/use-chat-threads.d.ts +67 -0
  27. package/dist/client/use-chat-threads.js +141 -23
  28. package/dist/client/use-db-sync.d.ts +1 -0
  29. package/dist/client/use-db-sync.js +13 -1
  30. package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
  31. package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/bounded-task-bundle.d.ts +17 -0
  34. package/dist/collab/bounded-task-bundle.js +39 -0
  35. package/dist/collab/bounded-task-source.d.ts +14 -0
  36. package/dist/collab/bounded-task-source.js +8 -0
  37. package/dist/collab/bounded-tasks.d.ts +67 -0
  38. package/dist/collab/bounded-tasks.js +439 -0
  39. package/dist/collab/client.js +2 -1
  40. package/dist/collab/index.d.ts +5 -2
  41. package/dist/collab/index.js +5 -2
  42. package/dist/collab/live-copy-tasks.d.ts +80 -0
  43. package/dist/collab/live-copy-tasks.js +330 -0
  44. package/dist/collab/live-copy.d.ts +188 -0
  45. package/dist/collab/live-copy.js +415 -0
  46. package/dist/collab/routes.d.ts +5 -0
  47. package/dist/collab/routes.js +24 -8
  48. package/dist/collab/storage.d.ts +53 -1
  49. package/dist/collab/storage.js +93 -5
  50. package/dist/collab/struct-routes.d.ts +1 -1
  51. package/dist/collab/ydoc-manager.d.ts +92 -3
  52. package/dist/collab/ydoc-manager.js +447 -38
  53. package/dist/db/client.d.ts +6 -0
  54. package/dist/db/client.js +25 -5
  55. package/dist/deploy/build.js +2 -0
  56. package/dist/extensions/actions.js +1 -0
  57. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  58. package/dist/mcp/agent-connected.js +5 -1
  59. package/dist/mcp/build-server.js +39 -10
  60. package/dist/notifications/routes.d.ts +1 -1
  61. package/dist/observability/routes.d.ts +1 -1
  62. package/dist/org/index.d.ts +2 -2
  63. package/dist/progress/routes.d.ts +1 -1
  64. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  65. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  66. package/dist/server/agent-chat-plugin.js +80 -17
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  69. package/dist/server/analytics.d.ts +1 -0
  70. package/dist/server/analytics.js +18 -1
  71. package/dist/server/collab-plugin.d.ts +10 -1
  72. package/dist/server/collab-plugin.js +89 -20
  73. package/dist/server/index.d.ts +1 -1
  74. package/dist/server/realtime-token.d.ts +1 -1
  75. package/dist/shared/optional-node-builtins.d.ts +1 -0
  76. package/dist/shared/optional-node-builtins.js +3 -0
  77. package/dist/sharing/access.d.ts +1 -0
  78. package/dist/sharing/access.js +113 -95
  79. package/dist/tracking/providers.d.ts +1 -0
  80. package/dist/tracking/providers.js +2 -1
  81. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  82. package/dist/vite/bounded-task-plugin.d.ts +22 -0
  83. package/dist/vite/bounded-task-plugin.js +44 -0
  84. package/dist/vite/client.js +2 -0
  85. package/dist/vitest-config.js +3 -0
  86. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
  87. package/docs/content/deployment-environment-variables.mdx +1 -0
  88. package/docs/content/environment-variables.mdx +1 -0
  89. package/docs/content/sharing.mdx +36 -0
  90. package/docs/content/toolkit-sharing.mdx +98 -12
  91. package/package.json +5 -3
@@ -1,3 +1,4 @@
1
+ import { LiveCopyError, type LiveCopyBinding } from "../collab/live-copy.js";
1
2
  import { type DbExec } from "../db/client.js";
2
3
  type NitroPluginDef = (nitroApp: any) => void | Promise<void>;
3
4
  export type CollabResourceIdResolver = (docId: string) => string | null | Promise<string | null>;
@@ -22,7 +23,14 @@ export interface CollabPluginOptions {
22
23
  idColumn?: string;
23
24
  autoSeed?: boolean;
24
25
  resolveSourceIdFromCollabDocumentId?: (docId: string) => string;
25
- onContentSync?: (docId: string, text: string) => Promise<void>;
26
+ /**
27
+ * Documents this binding covers are live copies: each update commits with
28
+ * its derived text, and the text, JSON and search-replace routes refuse
29
+ * them. Requires `autoSeed: false`; seed with `seedLiveCopy`. Passing the
30
+ * binding here also imports it at server start, so the refusal holds from
31
+ * the first request rather than from whenever an action first loads it.
32
+ */
33
+ liveCopy?: LiveCopyBinding;
26
34
  contentType?: "text" | "json";
27
35
  jsonColumn?: string;
28
36
  access?: CollabAccess;
@@ -48,5 +56,6 @@ export declare function createCollabSourceSeeder(options: {
48
56
  seed: (docId: string, source: string, client?: DbExec) => Promise<void>;
49
57
  withLock?: (docId: string, run: (client?: DbExec) => Promise<void>) => Promise<void>;
50
58
  }): (docId: string) => Promise<void>;
59
+ export declare function liveCopyErrorStatus(error: LiveCopyError): number;
51
60
  export declare function createCollabPlugin(options?: CollabPluginOptions): NitroPluginDef;
52
61
  export {};
@@ -1,6 +1,7 @@
1
1
  import { defineEventHandler, getMethod, setResponseStatus, } from "h3";
2
2
  import { postAwareness, getActiveUsers } from "../collab/awareness.js";
3
3
  import { getCollabEmitter } from "../collab/emitter.js";
4
+ import { LiveCopyError } from "../collab/live-copy.js";
4
5
  import { getCollabState, postCollabUpdate, postCollabText, postCollabSearchReplace, } from "../collab/routes.js";
5
6
  import { hasCollabState } from "../collab/storage.js";
6
7
  import { postCollabJson, getCollabJson, postCollabPatch, } from "../collab/struct-routes.js";
@@ -98,7 +99,67 @@ export function createCollabSourceSeeder(options) {
98
99
  }
99
100
  };
100
101
  }
102
+ // Worker and capacity failures are the server's, so a client may retry them;
103
+ // a task that broke its contract is a server bug, not a bad edit.
104
+ const RETRYABLE_TASK_FAILURES = new Set([
105
+ "unavailable",
106
+ "closed",
107
+ "queue_full",
108
+ "queue_timeout",
109
+ "deadline_exceeded",
110
+ "memory_exceeded",
111
+ "worker_crashed",
112
+ "startup_failed",
113
+ ]);
114
+ export function liveCopyErrorStatus(error) {
115
+ switch (error.code) {
116
+ case "write_refused":
117
+ case "read_refused":
118
+ case "not_live_copy":
119
+ case "not_seeded":
120
+ case "version_conflict":
121
+ return 409;
122
+ case "state_too_large":
123
+ return 413;
124
+ case "malformed_state":
125
+ case "unsupported_content":
126
+ case "derive_refused":
127
+ case "build_refused":
128
+ return 422;
129
+ case "derive_failed":
130
+ case "build_failed":
131
+ if (RETRYABLE_TASK_FAILURES.has(error.reason ?? ""))
132
+ return 503;
133
+ if (error.reason === "input_too_large" ||
134
+ error.reason === "output_too_large") {
135
+ return 413;
136
+ }
137
+ return 500;
138
+ }
139
+ }
140
+ function collabErrorResponse(event, error) {
141
+ if (error instanceof LiveCopyError) {
142
+ setResponseStatus(event, liveCopyErrorStatus(error));
143
+ return {
144
+ error: error.message,
145
+ code: `live_copy_${error.code}`,
146
+ ...(error.reason !== undefined ? { reason: error.reason } : {}),
147
+ };
148
+ }
149
+ return null;
150
+ }
101
151
  export function createCollabPlugin(options = {}) {
152
+ if ("onContentSync" in options) {
153
+ throw new Error("createCollabPlugin no longer accepts onContentSync; it was never called. Use liveCopy (defineLiveCopy) to derive a document's text when its live copy changes.");
154
+ }
155
+ if (options.liveCopy) {
156
+ if (options.autoSeed !== false) {
157
+ throw new Error("createCollabPlugin liveCopy requires autoSeed: false; seed live copies with seedLiveCopy.");
158
+ }
159
+ if (options.contentType === "json") {
160
+ throw new Error('createCollabPlugin liveCopy derives text from an XML fragment and cannot be combined with contentType: "json".');
161
+ }
162
+ }
102
163
  const normalizedAccess = normalizeCollabAccess(options);
103
164
  const { table = "documents", contentColumn = "content", idColumn = "id", autoSeed = true, maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES, } = options;
104
165
  const resolveSourceIdFromCollabDocumentId = options.resolveSourceIdFromCollabDocumentId ?? ((docId) => docId);
@@ -355,26 +416,34 @@ export function createCollabPlugin(options = {}) {
355
416
  (action === "search-replace" && method === "POST") ||
356
417
  (action === "json" && (method === "GET" || method === "POST")) ||
357
418
  (action === "patch" && method === "POST");
358
- if (needsSeed)
359
- await ensureDocumentSeeded(docId);
360
- if (action === "state" && method === "GET")
361
- return getCollabState(event);
362
- if (action === "update" && method === "POST")
363
- return postCollabUpdate(event);
364
- if (action === "text" && method === "POST")
365
- return postCollabText(event);
366
- if (action === "search-replace" && method === "POST")
367
- return postCollabSearchReplace(event);
368
- if (action === "json" && method === "POST")
369
- return postCollabJson(event);
370
- if (action === "json" && method === "GET")
371
- return getCollabJson(event);
372
- if (action === "patch" && method === "POST")
373
- return postCollabPatch(event);
374
- if (action === "awareness" && method === "POST")
375
- return postAwareness(event);
376
- if (action === "users" && method === "GET")
377
- return getActiveUsers(event);
419
+ try {
420
+ if (needsSeed)
421
+ await ensureDocumentSeeded(docId);
422
+ if (action === "state" && method === "GET")
423
+ return await getCollabState(event);
424
+ if (action === "update" && method === "POST")
425
+ return await postCollabUpdate(event);
426
+ if (action === "text" && method === "POST")
427
+ return await postCollabText(event);
428
+ if (action === "search-replace" && method === "POST")
429
+ return await postCollabSearchReplace(event);
430
+ if (action === "json" && method === "POST")
431
+ return await postCollabJson(event);
432
+ if (action === "json" && method === "GET")
433
+ return await getCollabJson(event);
434
+ if (action === "patch" && method === "POST")
435
+ return await postCollabPatch(event);
436
+ if (action === "awareness" && method === "POST")
437
+ return await postAwareness(event);
438
+ if (action === "users" && method === "GET")
439
+ return await getActiveUsers(event);
440
+ }
441
+ catch (error) {
442
+ const response = collabErrorResponse(event, error);
443
+ if (response)
444
+ return response;
445
+ throw error;
446
+ }
378
447
  setResponseStatus(event, 404);
379
448
  return { error: "Not found" };
380
449
  });
@@ -58,7 +58,7 @@ export { refreshMcpManagerForPrincipal } from "./agent-chat/mcp-glue.js";
58
58
  export { AGENT_CHAT_STREAM_PATH, AGENT_CHAT_STREAM_TOKEN_SUFFIX, AGENT_CHAT_STREAM_TOKEN_TTL_SECONDS, createAgentChatStreamToken, isAgentChatStreamingRuntime, readAgentChatStreamBearerToken, verifyAgentChatStreamToken, type AgentChatStreamPrincipal, } from "./agent-chat-stream.js";
59
59
  export type { AgentChatMcpIcon, AgentChatMcpOptions, } from "./agent-chat/mcp-options.js";
60
60
  export { configureAgentNativeEmbeddedEnvironment, createAgentNativeEmbeddedAuthOptions, createAgentNativeEmbeddedPlugin, mountAgentNativeEmbedded, normalizeAgentNativeEmbeddedSession, type AgentNativeEmbeddedAuthOptions, type AgentNativeEmbeddedGetSession, type AgentNativeEmbeddedHostSession, type AgentNativeEmbeddedPluginOptions, } from "./embedded.js";
61
- export { createThread, getThread, listThreads, updateThreadData, deleteThread, setThreadArchived, setThreadPinned, setThreadScope, type ChatThread, type ChatThreadScope, type ChatThreadSummary, type ListThreadsOptions, } from "../chat-threads/store.js";
61
+ export { createThread, getThread, listThreads, updateThreadData, deleteThread, setThreadArchived, setThreadPinned, setThreadScope, type ChatThread, type ChatThreadAccessBasis, type ChatThreadOwnership, type ChatThreadScope, type ChatThreadSummary, type ChatThreadListRow, type ListThreadsOptions, } from "../chat-threads/store.js";
62
62
  export { createResourcesPlugin, defaultResourcesPlugin, } from "./resources-plugin.js";
63
63
  export { createCoreRoutesPlugin, defaultCoreRoutesPlugin, FRAMEWORK_ROUTE_PREFIX, type CoreRoutesPluginOptions, } from "./core-routes-plugin.js";
64
64
  export type { CoreRoutesMcpOptions } from "./core-routes/mcp-connect-options.js";
@@ -54,8 +54,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
54
54
  expiresAt?: undefined;
55
55
  ttlSeconds?: undefined;
56
56
  } | {
57
+ error?: undefined;
57
58
  token: string;
58
59
  expiresAt: string;
59
60
  ttlSeconds: number;
60
- error?: undefined;
61
61
  }>>;
@@ -2,4 +2,5 @@ import type { AsyncLocalStorage } from "node:async_hooks";
2
2
  import type { EventEmitter } from "node:events";
3
3
  export type AsyncLocalStorageCtor = new <T>() => AsyncLocalStorage<T>;
4
4
  export declare function getAsyncLocalStorageCtor(): AsyncLocalStorageCtor | undefined;
5
+ export declare function getWorkerThreads(): typeof import("node:worker_threads") | undefined;
5
6
  export declare function createEventEmitter(): EventEmitter;
@@ -11,6 +11,9 @@ function nodeBuiltin(name) {
11
11
  export function getAsyncLocalStorageCtor() {
12
12
  return nodeBuiltin("node:async_hooks")?.AsyncLocalStorage;
13
13
  }
14
+ export function getWorkerThreads() {
15
+ return nodeBuiltin("node:worker_threads");
16
+ }
14
17
  export function createEventEmitter() {
15
18
  const Ctor = nodeBuiltin("node:events")?.EventEmitter;
16
19
  if (Ctor)
@@ -15,6 +15,7 @@ export interface AccessContext {
15
15
  }
16
16
  export declare function currentAccess(): AccessContext;
17
17
  export declare function resolveRegisteredAccessContext(reg: ShareableResourceRegistration | undefined, ctx: AccessContext): AccessContext;
18
+ export declare function hasCurrentOrgMembership(reg: Pick<ShareableResourceRegistration, "getDb">, memberOrgId: string, email: string, ctx: AccessContext): Promise<boolean>;
18
19
  export declare function accessFilter(resourceTable: any, sharesTable: any, rawCtx?: AccessContext, minRole?: ShareRole, options?: {
19
20
  includePublic?: boolean;
20
21
  }): SQL;
@@ -3,7 +3,6 @@ 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";
5
5
  import { CROSS_APP_ORG_FEDERATION_FLAG } from "../org/feature-flags.js";
6
- import { isMissingOrganizationTableError } from "../org/membership.js";
7
6
  import { orgMembers, organizations } from "../org/schema.js";
8
7
  import { implicitServiceOrgRole } from "../org/service-identity.js";
9
8
  import { getRequestAuthCapability, getRequestContext, getRequestUserEmail, getRequestOrgId, } from "../server/request-context.js";
@@ -111,19 +110,34 @@ function orgMembershipScope(ctx, orgId) {
111
110
  }
112
111
  return sql `1=1`;
113
112
  }
114
- async function isOrgMember(reg, memberOrgId, email, ctx) {
115
- const requestContext = getRequestContext();
116
- const verifiedServiceIdentity = requestContext?.verifiedServiceIdentity;
117
- const db = reg.getDb();
118
- if (normalizeEmailForAccess(requestContext?.userEmail) === email &&
119
- normalizeEmailForAccess(verifiedServiceIdentity?.userEmail) === email &&
120
- implicitServiceOrgRole({
121
- email: verifiedServiceIdentity?.userEmail,
122
- orgId: memberOrgId,
123
- requestOrgId: requestContext?.orgId,
124
- }) &&
125
- verifiedServiceIdentity?.orgId === memberOrgId) {
126
- try {
113
+ export async function hasCurrentOrgMembership(reg, memberOrgId, email, ctx) {
114
+ const normalizedEmail = normalizeEmailForAccess(email);
115
+ if (!memberOrgId || !normalizedEmail)
116
+ return false;
117
+ const transaction = ctx.transaction ?? getScopedDbExec();
118
+ const membershipContext = transaction ? { ...ctx, transaction } : ctx;
119
+ return inAccessTransaction(membershipContext, async () => {
120
+ const requestContext = getRequestContext();
121
+ const verifiedServiceIdentity = requestContext?.verifiedServiceIdentity;
122
+ const db = transaction
123
+ ? drizzleProxy(async (query, params) => {
124
+ const result = await transaction.execute({
125
+ sql: query,
126
+ args: params,
127
+ });
128
+ return { rows: result.rows.map((row) => Object.values(row)) };
129
+ })
130
+ : reg.getDb();
131
+ const verifiedLocalService = normalizeEmailForAccess(requestContext?.userEmail) === normalizedEmail &&
132
+ normalizeEmailForAccess(verifiedServiceIdentity?.userEmail) ===
133
+ normalizedEmail &&
134
+ implicitServiceOrgRole({
135
+ email: verifiedServiceIdentity?.userEmail,
136
+ orgId: memberOrgId,
137
+ requestOrgId: requestContext?.orgId,
138
+ }) &&
139
+ verifiedServiceIdentity?.orgId === memberOrgId;
140
+ const loadOrganization = async () => {
127
141
  const [organization] = await db
128
142
  .select({
129
143
  identityAuthority: organizations.identityAuthority,
@@ -132,66 +146,46 @@ async function isOrgMember(reg, memberOrgId, email, ctx) {
132
146
  .from(organizations)
133
147
  .where(eq(organizations.id, memberOrgId))
134
148
  .limit(1);
135
- if (organization &&
136
- !String(organization.identityAuthority ?? "").trim() &&
137
- !String(organization.identityId ?? "").trim()) {
149
+ if (!organization) {
150
+ throw new Error("Organization metadata is unavailable for access validation");
151
+ }
152
+ return organization;
153
+ };
154
+ let organization;
155
+ if (verifiedLocalService) {
156
+ organization = await loadOrganization();
157
+ if (!organization.identityAuthority?.trim() &&
158
+ !organization.identityId?.trim()) {
138
159
  return true;
139
160
  }
140
161
  }
141
- catch (error) {
142
- if (!isMissingOrganizationTableError(error))
143
- throw error;
144
- }
145
- }
146
- let rows;
147
- try {
148
- rows = await db
162
+ const rows = await db
149
163
  .select({ id: orgMembers.id })
150
164
  .from(orgMembers)
151
- .where(and(eq(orgMembers.orgId, memberOrgId), emailColumnMatches(orgMembers.email, email), isNull(orgMembers.federationRemovalPendingAt)))
165
+ .where(and(eq(orgMembers.orgId, memberOrgId), emailColumnMatches(orgMembers.email, normalizedEmail), isNull(orgMembers.federationRemovalPendingAt)))
152
166
  .limit(1);
153
- }
154
- catch (error) {
155
- if (!isMissingOrganizationTableError(error))
156
- throw error;
157
- return false;
158
- }
159
- if (rows.length === 0)
160
- return false;
161
- let organization = null;
162
- try {
163
- [organization] = await db
164
- .select({
165
- identityAuthority: organizations.identityAuthority,
166
- identityId: organizations.identityId,
167
- })
168
- .from(organizations)
169
- .where(eq(organizations.id, memberOrgId))
170
- .limit(1);
171
- }
172
- catch (error) {
173
- if (!isMissingOrganizationTableError(error))
174
- throw error;
175
- return true;
176
- }
177
- const linked = String(organization?.identityAuthority ?? "").trim() ||
178
- String(organization?.identityId ?? "").trim();
179
- if (!linked)
180
- return true;
181
- if (!(await evaluateFeatureFlagStrict(CROSS_APP_ORG_FEDERATION_FLAG.key, {
182
- userEmail: email,
183
- userKey: email,
184
- orgId: memberOrgId,
185
- transaction: ctx.transaction,
186
- }))) {
187
- return true;
188
- }
189
- const { validateFederatedOrganizationMembershipForCurrentRequest } = await import("../org/federation.js");
190
- const validation = await validateFederatedOrganizationMembershipForCurrentRequest({
191
- orgId: memberOrgId,
192
- email,
167
+ if (rows.length === 0)
168
+ return false;
169
+ organization ??= await loadOrganization();
170
+ if (!organization.identityAuthority?.trim() &&
171
+ !organization.identityId?.trim()) {
172
+ return true;
173
+ }
174
+ if (!(await evaluateFeatureFlagStrict(CROSS_APP_ORG_FEDERATION_FLAG.key, {
175
+ userEmail: normalizedEmail,
176
+ userKey: normalizedEmail,
177
+ orgId: memberOrgId,
178
+ transaction,
179
+ }))) {
180
+ return true;
181
+ }
182
+ const { validateFederatedOrganizationMembershipForCurrentRequest } = await import("../org/federation.js");
183
+ const validation = await validateFederatedOrganizationMembershipForCurrentRequest({
184
+ orgId: memberOrgId,
185
+ email: normalizedEmail,
186
+ });
187
+ return validation.active;
193
188
  });
194
- return validation.active;
195
189
  }
196
190
  export function accessFilter(resourceTable, sharesTable, rawCtx = currentAccess(), minRole = "viewer", options = {}) {
197
191
  const reg = findRegistrationByTable(resourceTable);
@@ -469,6 +463,17 @@ async function ownerOrAdminRole(reg, resource, ctx, normalizedUserEmail) {
469
463
  async function accessToResource(reg, resourceId, resource, ctx) {
470
464
  const { userEmail } = ctx;
471
465
  const normalizedUserEmail = normalizeEmailForAccess(userEmail);
466
+ const memberships = new Map();
467
+ const isOrgMember = (orgId) => {
468
+ let membership = memberships.get(orgId);
469
+ if (!membership) {
470
+ membership = normalizedUserEmail
471
+ ? hasCurrentOrgMembership(reg, orgId, normalizedUserEmail, ctx)
472
+ : Promise.resolve(false);
473
+ memberships.set(orgId, membership);
474
+ }
475
+ return membership;
476
+ };
472
477
  // A host can deny outright. When it grants access, the resource's owner
473
478
  // and admins keep their own roles; the host's role covers everyone else.
474
479
  const hostResolver = hostAccessResolvers.get(reg.type);
@@ -482,21 +487,19 @@ async function accessToResource(reg, resourceId, resource, ctx) {
482
487
  return { role: managingRole, resource };
483
488
  if (hosted !== undefined)
484
489
  return { role: hosted, resource };
490
+ const role = await highestShareRole(reg, resourceId, ctx, resource, isOrgMember);
485
491
  if (resource.visibility === "public" && reg.allowPublic !== false) {
486
492
  const publicRole = await publicAccessRoleForResource(reg, resource, ctx);
487
- const role = await highestShareRole(reg, resourceId, ctx, resource);
488
493
  return { role: higherShareRole(publicRole, role), resource };
489
494
  }
495
+ if (role)
496
+ return { role, resource };
490
497
  if (resource.visibility === "org" &&
491
498
  resource.orgId &&
492
499
  normalizedUserEmail &&
493
- (await isOrgMember(reg, resource.orgId, normalizedUserEmail, ctx))) {
494
- const role = await highestShareRole(reg, resourceId, ctx, resource);
495
- return { role: role ?? "viewer", resource };
500
+ (await isOrgMember(resource.orgId))) {
501
+ return { role: "viewer", resource };
496
502
  }
497
- const role = await highestShareRole(reg, resourceId, ctx, resource);
498
- if (role)
499
- return { role, resource };
500
503
  return null;
501
504
  }
502
505
  /**
@@ -530,7 +533,7 @@ export async function resolveAccessStatus(resourceType, resourceId, ctx = curren
530
533
  return { state: available ? "allowed" : "trashed", role: access.role };
531
534
  });
532
535
  }
533
- async function highestShareRole(reg, resourceId, ctx, resource) {
536
+ async function highestShareRole(reg, resourceId, ctx, resource, isOrgMember) {
534
537
  const { userEmail, orgId } = ctx;
535
538
  const normalizedUserEmail = normalizeEmailForAccess(userEmail);
536
539
  if (!normalizedUserEmail && !orgId)
@@ -545,34 +548,49 @@ async function highestShareRole(reg, resourceId, ctx, resource) {
545
548
  if (orgId) {
546
549
  principalClauses.push(and(eq(reg.sharesTable.principalType, "org"), eq(reg.sharesTable.principalId, orgId)));
547
550
  }
551
+ if (principalClauses.length === 0)
552
+ return null;
553
+ const rows = await db
554
+ .select({
555
+ role: reg.sharesTable.role,
556
+ principalType: reg.sharesTable.principalType,
557
+ })
558
+ .from(reg.sharesTable)
559
+ .where(and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)))
560
+ .limit(10);
548
561
  let best = null;
562
+ const grantRows = rows;
563
+ for (const row of grantRows) {
564
+ if (row.principalType === "user")
565
+ best = higherShareRole(row.role, best);
566
+ }
567
+ // Direct admin grants are already maximal and do not depend on organization health.
568
+ if (best === "admin")
569
+ return best;
570
+ if (orgId &&
571
+ grantRows.some((row) => row.principalType === "org") &&
572
+ (await isOrgMember(orgId))) {
573
+ for (const row of grantRows) {
574
+ if (row.principalType === "org")
575
+ best = higherShareRole(row.role, best);
576
+ }
577
+ }
549
578
  if (reg.supportsGroupShares && normalizedUserEmail && resource.orgId) {
550
- if (await isOrgMember(reg, resource.orgId, normalizedUserEmail, ctx)) {
551
- const groupRows = await db
552
- .select({
553
- principalId: reg.sharesTable.principalId,
554
- role: reg.sharesTable.role,
555
- })
556
- .from(reg.sharesTable)
557
- .where(and(eq(reg.sharesTable.resourceId, resourceId), eq(reg.sharesTable.principalType, "group")));
579
+ const groupRows = await db
580
+ .select({
581
+ principalId: reg.sharesTable.principalId,
582
+ role: reg.sharesTable.role,
583
+ })
584
+ .from(reg.sharesTable)
585
+ .where(and(eq(reg.sharesTable.resourceId, resourceId), eq(reg.sharesTable.principalType, "group")));
586
+ if (groupRows.length > 0 && (await isOrgMember(resource.orgId))) {
558
587
  for (const row of groupRows) {
559
588
  if (await workspaceUserGroupsIncludeUser(resource.orgId, [row.principalId], normalizedUserEmail)) {
560
- if (!best || ROLE_RANK[row.role] > ROLE_RANK[best]) {
561
- best = row.role;
562
- }
589
+ best = higherShareRole(row.role, best);
563
590
  }
564
591
  }
565
592
  }
566
593
  }
567
- const rows = await db
568
- .select({ role: reg.sharesTable.role })
569
- .from(reg.sharesTable)
570
- .where(and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)))
571
- .limit(10);
572
- for (const r of rows) {
573
- if (!best || ROLE_RANK[r.role] > ROLE_RANK[best])
574
- best = r.role;
575
- }
576
594
  return best;
577
595
  }
578
596
  export async function assertAccess(resourceType, resourceId, minRole = "viewer", ctx = currentAccess(), options = {}) {
@@ -1,2 +1,3 @@
1
+ export declare const AGENT_NATIVE_ANALYTICS_PROVIDER = "agent-native-analytics";
1
2
  export declare function sendPostHogEvent(name: string, properties: Record<string, unknown>, distinctId: string): boolean;
2
3
  export declare function registerBuiltinProviders(): void;
@@ -6,6 +6,7 @@ import { reshapeTrackedExceptionProperties } from "./posthog-exception.js";
6
6
  import { registerTrackingProvider } from "./registry.js";
7
7
  const POSTHOG_DEFAULT_HOST = "https://us.i.posthog.com";
8
8
  const AGENT_NATIVE_ANALYTICS_DEFAULT_ENDPOINT = "https://analytics.agent-native.com/track";
9
+ export const AGENT_NATIVE_ANALYTICS_PROVIDER = "agent-native-analytics";
9
10
  const BATCH_INTERVAL_MS = 10_000;
10
11
  const MAX_BATCH_SIZE = 50;
11
12
  const QUEUE_KEY = Symbol.for("@agent-native/core/tracking.queue");
@@ -323,7 +324,7 @@ function createWebhookProvider(url, authHeader) {
323
324
  function createAgentNativeAnalyticsProvider(publicKey, endpoint) {
324
325
  const flushImmediately = agentNativeAnalyticsFlushesImmediately();
325
326
  return {
326
- name: "agent-native-analytics",
327
+ name: AGENT_NATIVE_ANALYTICS_PROVIDER,
327
328
  acceptsTestIdentityExceptions: true,
328
329
  track(event) {
329
330
  enqueue(endpoint, JSON.stringify({
@@ -13,7 +13,6 @@ declare const _default: import("../../action.js").ActionDefinition<{
13
13
  executionEngine?: string;
14
14
  executionCwd?: string;
15
15
  }, {
16
- deleted?: undefined;
17
16
  created: boolean;
18
17
  name: string;
19
18
  scope: "organization" | "personal";
@@ -24,6 +23,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
24
23
  reasoningEffort: "auto" | "high" | "low" | "max" | "medium" | "minimal" | "none" | "xhigh";
25
24
  webhookPath: string;
26
25
  nextRun: string;
26
+ deleted?: undefined;
27
27
  enabled?: undefined;
28
28
  executionHostId?: undefined;
29
29
  executionEngine?: undefined;
@@ -45,11 +45,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
45
45
  nextRun?: undefined;
46
46
  webhookPath?: undefined;
47
47
  } | {
48
- deleted?: undefined;
49
48
  created?: undefined;
50
49
  scope?: undefined;
51
50
  triggerType?: undefined;
52
51
  event?: undefined;
52
+ deleted?: undefined;
53
53
  name: string;
54
54
  enabled: boolean;
55
55
  schedule: string;
@@ -0,0 +1,22 @@
1
+ interface PluginContext {
2
+ resolve(source: string, importer?: string, options?: {
3
+ skipSelf?: boolean;
4
+ }): Promise<{
5
+ id: string;
6
+ external?: boolean | string;
7
+ } | null>;
8
+ addWatchFile(file: string): void;
9
+ }
10
+ /**
11
+ * `import source from "./tasks.ts?bounded-task"` yields the module bundled
12
+ * into a `BoundedTaskSource` at build time, so deployed servers never run
13
+ * esbuild. Registered for Vite (dev and client builds) and the production
14
+ * Nitro build.
15
+ */
16
+ export declare function boundedTaskSourcePlugin(): {
17
+ name: string;
18
+ enforce: "pre";
19
+ resolveId(this: PluginContext, source: string, importer?: string): Promise<string>;
20
+ load(this: PluginContext, id: string): Promise<string>;
21
+ };
22
+ export {};
@@ -0,0 +1,44 @@
1
+ import path from "node:path";
2
+ import { bundleBoundedTaskSource } from "../collab/bounded-task-bundle.js";
3
+ const QUERY = "?bounded-task";
4
+ const RESOLVED_PREFIX = "\0agent-native-bounded-task:";
5
+ /**
6
+ * `import source from "./tasks.ts?bounded-task"` yields the module bundled
7
+ * into a `BoundedTaskSource` at build time, so deployed servers never run
8
+ * esbuild. Registered for Vite (dev and client builds) and the production
9
+ * Nitro build.
10
+ */
11
+ export function boundedTaskSourcePlugin() {
12
+ return {
13
+ name: "agent-native-bounded-task-source",
14
+ enforce: "pre",
15
+ async resolveId(source, importer) {
16
+ if (!source.endsWith(QUERY))
17
+ return null;
18
+ const request = source.slice(0, -QUERY.length);
19
+ const resolved = await this.resolve(request, importer, {
20
+ skipSelf: true,
21
+ });
22
+ if (!resolved || resolved.external) {
23
+ throw new Error(`Cannot resolve bounded task module ${request}${importer ? ` from ${importer}` : ""}`);
24
+ }
25
+ return RESOLVED_PREFIX + resolved.id.split("?")[0];
26
+ },
27
+ async load(id) {
28
+ if (!id.startsWith(RESOLVED_PREFIX))
29
+ return null;
30
+ const entry = id.slice(RESOLVED_PREFIX.length);
31
+ const bundled = await bundleBoundedTaskSource(entry, {
32
+ name: path.basename(entry).replace(/\.[^.]+$/, ""),
33
+ });
34
+ for (const input of bundled.inputs)
35
+ this.addWatchFile(input);
36
+ const source = {
37
+ kind: bundled.kind,
38
+ name: bundled.name,
39
+ code: bundled.code,
40
+ };
41
+ return `export default Object.freeze(${JSON.stringify(source)});\n`;
42
+ },
43
+ };
44
+ }
@@ -29,6 +29,7 @@ import { AGENT_NATIVE_TYPEGEN_ENV, formatRuntimeConfigReport, getRuntimeConfigRe
29
29
  import { actionTypesPlugin } from "./action-types-plugin.js";
30
30
  import { createAgentNativeConfigContext, loadAgentNativeConfigFile, loadWorkspaceAgentNativeConfigFile, readAgentNativeJsonConfig, resolveFirstRunOnboardingBuildReplacement, resolveHarnessBuildReplacement, writeAgentNativeBuildConfigMarker, } from "./agent-native-config-loader.js";
31
31
  import { agentsBundlePlugin } from "./agents-bundle-plugin.js";
32
+ import { boundedTaskSourcePlugin } from "./bounded-task-plugin.js";
32
33
  import { migrationDiagnosticPlugin } from "./migration-diagnostic-plugin.js";
33
34
  import { motionSourceProvenancePlugin } from "./motion-source-provenance-plugin.js";
34
35
  import { resolveAgentNativePackageVersions } from "./package-versions.js";
@@ -3657,6 +3658,7 @@ function createAgentNativePlugins(options, { command, includeReactTransform, use
3657
3658
  appChangelogRawPlugin(),
3658
3659
  actionTypesPlugin(),
3659
3660
  agentsBundlePlugin({ agentNativeConfig: options.agentNativeConfig }),
3661
+ boundedTaskSourcePlugin(),
3660
3662
  authClientAssetPlugin(cwd),
3661
3663
  autoReloadOnOptimizeDep(),
3662
3664
  fullReloadOnOptimizeDep504(),
@@ -1,3 +1,4 @@
1
+ import { boundedTaskSourcePlugin } from "./vite/bounded-task-plugin.js";
1
2
  const DEFAULT_MAX_WORKERS = "25%";
2
3
  const ENV_KEYS = ["VITEST_CONCURRENCY", "AGENT_NATIVE_VITEST_CONCURRENCY"];
3
4
  export function resolveMaxWorkers(env = process.env, fallback = DEFAULT_MAX_WORKERS) {
@@ -25,6 +26,8 @@ export function resolveMaxWorkers(env = process.env, fallback = DEFAULT_MAX_WORK
25
26
  return count;
26
27
  }
27
28
  const vitestBaseConfig = {
29
+ // App tests import server modules that load `?bounded-task` sources.
30
+ plugins: [boundedTaskSourcePlugin()],
28
31
  test: {
29
32
  maxWorkers: resolveMaxWorkers(),
30
33
  // Vitest's 5 s default only holds while a worker has a core to itself.
@@ -1,10 +1,10 @@
1
1
  declare const _default: import("../../action.js").ActionDefinition<Record<string, never>, {
2
+ memberEmails: any[];
2
3
  id: string;
3
4
  orgId: string;
4
5
  name: string;
5
6
  createdByEmail: string;
6
7
  createdAt: string;
7
8
  updatedAt: string;
8
- memberEmails: any[];
9
9
  }[]>;
10
10
  export default _default;