@agent-native/core 0.205.0 → 0.206.0-nightly-20261009011246

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 (115) hide show
  1. package/dist/a2a/client.d.ts +2 -2
  2. package/dist/agent/engine/anthropic-engine.d.ts +1 -1
  3. package/dist/agent/engine/builder-engine.d.ts +1 -1
  4. package/dist/agent/engine/builder-engine.js +23 -8
  5. package/dist/agent/engine/error-telemetry.d.ts +5 -0
  6. package/dist/agent/engine/error-telemetry.js +41 -0
  7. package/dist/agent/engine/registry.d.ts +1 -6
  8. package/dist/agent/engine/registry.js +2 -31
  9. package/dist/agent/model-config.d.ts +26 -14
  10. package/dist/agent/model-config.js +131 -8
  11. package/dist/agent/model-version.d.ts +19 -0
  12. package/dist/agent/model-version.js +153 -0
  13. package/dist/agent/production-agent.d.ts +1 -0
  14. package/dist/agent/production-agent.js +24 -6
  15. package/dist/agent/provider-model-selection.js +6 -4
  16. package/dist/agent/run-manager.js +5 -11
  17. package/dist/agent/thread-data-builder.js +14 -6
  18. package/dist/authorization/check-action.js +8 -4
  19. package/dist/chat-threads/store.js +22 -9
  20. package/dist/client/analytics.d.ts +1 -0
  21. package/dist/client/analytics.js +10 -2
  22. package/dist/client/chat/runtime.js +18 -0
  23. package/dist/client/chat-model-groups.d.ts +3 -0
  24. package/dist/client/error-format.js +10 -7
  25. package/dist/client/onboarding/use-onboarding.d.ts +12 -0
  26. package/dist/client/onboarding/use-onboarding.js +491 -4
  27. package/dist/client/session-replay-privacy.d.ts +8 -0
  28. package/dist/client/session-replay-privacy.js +8 -0
  29. package/dist/client/session-replay.d.ts +1 -0
  30. package/dist/client/session-replay.js +18 -9
  31. package/dist/client/sse-event-processor.js +10 -6
  32. package/dist/client/use-chat-models.d.ts +5 -0
  33. package/dist/client/use-chat-models.js +51 -5
  34. package/dist/client/use-session.d.ts +8 -1
  35. package/dist/client/use-session.js +20 -1
  36. package/dist/collab/struct-routes.d.ts +1 -1
  37. package/dist/deploy/build.js +353 -3
  38. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  39. package/dist/jobs/backfill-run-history-ownership.d.ts +12 -0
  40. package/dist/jobs/backfill-run-history-ownership.js +87 -0
  41. package/dist/jobs/background-automation-runner.js +3 -9
  42. package/dist/jobs/remote-execution.js +3 -9
  43. package/dist/jobs/run-history-ownership.d.ts +5 -0
  44. package/dist/jobs/run-history-ownership.js +16 -0
  45. package/dist/jobs/run-now.js +2 -3
  46. package/dist/localization/agent-resources-messages.d.ts +3 -0
  47. package/dist/localization/agent-resources-messages.js +33 -0
  48. package/dist/mcp/actions/create-org-service-token.js +5 -7
  49. package/dist/mcp/build-server.d.ts +23 -18
  50. package/dist/mcp/build-server.js +220 -212
  51. package/dist/mcp/connect-route.d.ts +11 -2
  52. package/dist/mcp/connect-route.js +34 -24
  53. package/dist/mcp/connect-store.d.ts +9 -13
  54. package/dist/mcp/connect-store.js +11 -24
  55. package/dist/mcp/credential-membership.js +17 -6
  56. package/dist/mcp/oauth-route.d.ts +11 -1
  57. package/dist/mcp/oauth-route.js +28 -6
  58. package/dist/mcp/oauth-token.d.ts +12 -2
  59. package/dist/mcp/oauth-token.js +15 -4
  60. package/dist/mcp/server.js +9 -5
  61. package/dist/notifications/routes.d.ts +2 -2
  62. package/dist/observability/posthog-ai.d.ts +1 -0
  63. package/dist/observability/posthog-ai.js +20 -8
  64. package/dist/observability/routes.d.ts +6 -6
  65. package/dist/observability/traces.js +61 -39
  66. package/dist/org/app-roles.d.ts +8 -1
  67. package/dist/org/app-roles.js +29 -9
  68. package/dist/org/context.js +24 -8
  69. package/dist/org/index.d.ts +2 -2
  70. package/dist/org/workspace-app-identity.d.ts +1 -0
  71. package/dist/org/workspace-app-identity.js +25 -0
  72. package/dist/progress/routes.d.ts +1 -1
  73. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  74. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  75. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  76. package/dist/scripts/agent-engines/list-agent-engines.js +1 -0
  77. package/dist/server/action-routes.js +8 -1
  78. package/dist/server/agent-chat-plugin.js +1 -0
  79. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  80. package/dist/server/auth.js +33 -17
  81. package/dist/server/bearer-credential-refusal.d.ts +23 -0
  82. package/dist/server/bearer-credential-refusal.js +67 -0
  83. package/dist/server/better-auth-instance.js +13 -6
  84. package/dist/server/builder-browser.d.ts +2 -0
  85. package/dist/server/builder-browser.js +16 -3
  86. package/dist/server/capture-error.d.ts +1 -0
  87. package/dist/server/capture-error.js +28 -4
  88. package/dist/server/complete-text.js +16 -3
  89. package/dist/server/core-routes-plugin.js +2 -0
  90. package/dist/server/dev-action-bridge.js +7 -1
  91. package/dist/server/google-oauth.js +6 -3
  92. package/dist/server/index.d.ts +1 -1
  93. package/dist/server/index.js +1 -1
  94. package/dist/server/realtime-token.d.ts +1 -1
  95. package/dist/server/recap-image-route.js +4 -0
  96. package/dist/server/request-context.d.ts +16 -0
  97. package/dist/server/request-context.js +17 -0
  98. package/dist/shared/agent-chat-context.d.ts +1 -0
  99. package/dist/shared/agent-chat-context.js +26 -0
  100. package/dist/shared/index.d.ts +1 -1
  101. package/dist/shared/index.js +1 -1
  102. package/dist/shared/reasoning-effort.js +10 -1
  103. package/dist/shared/ssr-session-bootstrap.d.ts +7 -0
  104. package/dist/shared/ssr-session-bootstrap.js +21 -0
  105. package/dist/sharing/access.js +86 -5
  106. package/dist/templates/builder-code-starter/server/routes/index.get.ts +2 -0
  107. package/dist/triggers/routes.js +2 -1
  108. package/docs/content/automation-connectors.mdx +5 -3
  109. package/docs/content/automations.mdx +78 -49
  110. package/docs/content/observability.mdx +16 -5
  111. package/docs/content/organizations-teams-permissions.mdx +44 -2
  112. package/docs/content/recurring-jobs.mdx +4 -8
  113. package/docs/content/template-forms.mdx +6 -0
  114. package/package.json +5 -4
  115. package/src/templates/builder-code-starter/server/routes/index.get.ts +2 -0
@@ -58,6 +58,11 @@ export interface RequestContext {
58
58
  userEmail?: string;
59
59
  /** Keep data-source credentials within the selected org, not the user. */
60
60
  credentialScope?: "org";
61
+ /** Provenance set only after a stored org service token is verified. */
62
+ verifiedServiceIdentity?: {
63
+ userEmail: string;
64
+ orgId: string;
65
+ };
61
66
  identityAuthenticatedAtMs?: number;
62
67
  identitySessionToken?: string;
63
68
  authUserId?: string;
@@ -129,6 +134,17 @@ export interface RequestContext {
129
134
  };
130
135
  run?: RequestRunContext;
131
136
  }
137
+ type RequestContextEvent = {
138
+ context?: Record<string, unknown>;
139
+ };
140
+ export declare function markVerifiedServiceIdentityForEvent(event: RequestContextEvent, identity: {
141
+ userEmail: string;
142
+ orgId: string;
143
+ }): void;
144
+ export declare function getVerifiedServiceIdentityFromEvent(event: RequestContextEvent): {
145
+ userEmail: string;
146
+ orgId: string;
147
+ } | undefined;
132
148
  export declare function markExplicitPersonalOrgScope(event: {
133
149
  context?: Record<string, unknown>;
134
150
  }): void;
@@ -38,6 +38,23 @@ function processEnv(name) {
38
38
  return process.env?.[name];
39
39
  }
40
40
  const EXPLICIT_PERSONAL_ORG_SCOPE_KEY = "__anExplicitPersonalOrgScope";
41
+ const VERIFIED_SERVICE_IDENTITY_EVENT_KEY = "__anVerifiedServiceIdentity";
42
+ export function markVerifiedServiceIdentityForEvent(event, identity) {
43
+ const context = (event.context ??= {});
44
+ context[VERIFIED_SERVICE_IDENTITY_EVENT_KEY] = identity;
45
+ }
46
+ export function getVerifiedServiceIdentityFromEvent(event) {
47
+ const identity = event.context?.[VERIFIED_SERVICE_IDENTITY_EVENT_KEY];
48
+ const userEmail = identity?.userEmail;
49
+ const orgId = identity?.orgId;
50
+ if (typeof userEmail !== "string" ||
51
+ !userEmail.trim() ||
52
+ typeof orgId !== "string" ||
53
+ !orgId.trim()) {
54
+ return undefined;
55
+ }
56
+ return { userEmail: userEmail.trim(), orgId: orgId.trim() };
57
+ }
41
58
  export function markExplicitPersonalOrgScope(event) {
42
59
  if (event.context) {
43
60
  event.context[EXPLICIT_PERSONAL_ORG_SCOPE_KEY] = true;
@@ -2,3 +2,4 @@ import { splitAgentKitMessageContext } from "@agent-native/agentkit/chat-context
2
2
  export { appendAgentChatContextToMessage } from "@agent-native/agentkit/chat-context";
3
3
  export type { AgentKitMessageParts as AgentChatMessageParts } from "@agent-native/agentkit/chat-context";
4
4
  export declare const splitAgentChatContextFromMessage: typeof splitAgentKitMessageContext;
5
+ export declare function stripAgentChatContextFromMessage(text: string): string;
@@ -1,3 +1,29 @@
1
1
  import { splitAgentKitMessageContext } from "@agent-native/agentkit/chat-context";
2
2
  export { appendAgentChatContextToMessage } from "@agent-native/agentkit/chat-context";
3
3
  export const splitAgentChatContextFromMessage = splitAgentKitMessageContext;
4
+ const ENCODED_AGENT_CHAT_CONTEXT_OPENING = '<context data-agentkit-context-encoding="entities-v1">';
5
+ const LEGACY_CONTEXT_OPEN_PATTERN = /<context(?=[\s>])[^>]*>/gi;
6
+ export function stripAgentChatContextFromMessage(text) {
7
+ LEGACY_CONTEXT_OPEN_PATTERN.lastIndex = 0;
8
+ const opening = LEGACY_CONTEXT_OPEN_PATTERN.exec(text);
9
+ if (!opening)
10
+ return text;
11
+ if (opening[0] === ENCODED_AGENT_CHAT_CONTEXT_OPENING)
12
+ return splitAgentChatContextFromMessage(text).message;
13
+ const closingPattern = /<\/context(?=[\s>])[^>]*>/gi;
14
+ closingPattern.lastIndex = opening.index + opening[0].length;
15
+ let lastClosing = null;
16
+ let closing = closingPattern.exec(text);
17
+ while (closing) {
18
+ lastClosing = closing;
19
+ closing = closingPattern.exec(text);
20
+ }
21
+ if (!lastClosing)
22
+ return text.slice(0, opening.index);
23
+ LEGACY_CONTEXT_OPEN_PATTERN.lastIndex =
24
+ lastClosing.index + lastClosing[0].length;
25
+ if (LEGACY_CONTEXT_OPEN_PATTERN.exec(text))
26
+ return text.slice(0, opening.index);
27
+ return (text.slice(0, opening.index) +
28
+ text.slice(lastClosing.index + lastClosing[0].length));
29
+ }
@@ -1,5 +1,5 @@
1
1
  export { agentChat, type AgentChatMessage, type AgentChatCallOptions, type AgentChatResponse, } from "./agent-chat.js";
2
- export { appendAgentChatContextToMessage, splitAgentChatContextFromMessage, type AgentChatMessageParts, } from "./agent-chat-context.js";
2
+ export { appendAgentChatContextToMessage, splitAgentChatContextFromMessage, stripAgentChatContextFromMessage, type AgentChatMessageParts, } from "./agent-chat-context.js";
3
3
  export { agentEnv, type EnvVar } from "./agent-env.js";
4
4
  export { APP_STATUS, DEFAULT_APP_STATUS, getAppStatus, type AppStatus, } from "./app-status.js";
5
5
  export { extractOAuthStateAppId, extractOAuthStateProvider, } from "./oauth-state.js";
@@ -1,5 +1,5 @@
1
1
  export { agentChat, } from "./agent-chat.js";
2
- export { appendAgentChatContextToMessage, splitAgentChatContextFromMessage, } from "./agent-chat-context.js";
2
+ export { appendAgentChatContextToMessage, splitAgentChatContextFromMessage, stripAgentChatContextFromMessage, } from "./agent-chat-context.js";
3
3
  export { agentEnv } from "./agent-env.js";
4
4
  export { APP_STATUS, DEFAULT_APP_STATUS, getAppStatus, } from "./app-status.js";
5
5
  export { extractOAuthStateAppId, extractOAuthStateProvider, } from "./oauth-state.js";
@@ -116,8 +116,11 @@ function isClaudeReasoningModel(model) {
116
116
  return true;
117
117
  if (id.includes("sonnet-5") || id.includes("sonnet-4-6"))
118
118
  return true;
119
- if (id.includes("haiku-4-5"))
119
+ if (id.includes("haiku-5-5") ||
120
+ id.includes("haiku-5.5") ||
121
+ id.includes("haiku-4-5")) {
120
122
  return true;
123
+ }
121
124
  return claudeOpusAtLeast(id, 4, 6);
122
125
  }
123
126
  export function supportsClaudeAdaptiveThinking(model) {
@@ -126,6 +129,8 @@ export function supportsClaudeAdaptiveThinking(model) {
126
129
  const id = model.toLowerCase().replace(/^anthropic\//, "");
127
130
  if (id.includes("fable-5") || id.includes("mythos-5"))
128
131
  return true;
132
+ if (id.includes("haiku-5-5") || id.includes("haiku-5.5"))
133
+ return true;
129
134
  if (id.includes("sonnet-5") || id.includes("sonnet-4-6"))
130
135
  return true;
131
136
  return claudeOpusAtLeast(id, 4, 6);
@@ -150,6 +155,8 @@ function supportsClaudeXHigh(model) {
150
155
  const id = model.toLowerCase().replace(/^anthropic\//, "");
151
156
  if (id.includes("fable-5"))
152
157
  return true;
158
+ if (id.includes("haiku-5-5") || id.includes("haiku-5.5"))
159
+ return true;
153
160
  if (id.includes("sonnet-5"))
154
161
  return true;
155
162
  return claudeOpusAtLeast(id, 4, 7);
@@ -161,6 +168,8 @@ function claudeAcceptsSamplingParams(model) {
161
168
  const id = model.toLowerCase().replace(/^anthropic\//, "");
162
169
  if (id.includes("fable-5") || id.includes("mythos-5"))
163
170
  return false;
171
+ if (id.includes("haiku-5-5") || id.includes("haiku-5.5"))
172
+ return false;
164
173
  if (id.includes("sonnet-5"))
165
174
  return false;
166
175
  return !claudeOpusAtLeast(id, 4, 7);
@@ -27,4 +27,11 @@ export declare function isSessionNavigationPending(): boolean;
27
27
  export declare function navigateForSession(href: string): boolean;
28
28
  /** Calls `listener` whenever a claimed navigation is released. */
29
29
  export declare function subscribeSessionNavigation(listener: () => void): () => void;
30
+ /**
31
+ * Whether this browser carries the readable cookie the server keeps beside a
32
+ * signed-in session. It says a session is likely, never that one exists: the
33
+ * session read decides. Same rule as the bootstrap script below, so a read
34
+ * started on it starts exactly when the early session read does.
35
+ */
36
+ export declare function hasSessionHint(sessionHintCookieName?: string): boolean;
30
37
  export declare function getSsrSessionBootstrapScriptBody(sessionPath: string, sessionHintCookieName?: string): string;
@@ -50,6 +50,27 @@ export function subscribeSessionNavigation(listener) {
50
50
  window.addEventListener(SESSION_NAVIGATION_RELEASED_EVENT, listener);
51
51
  return () => window.removeEventListener(SESSION_NAVIGATION_RELEASED_EVENT, listener);
52
52
  }
53
+ /**
54
+ * Whether this browser carries the readable cookie the server keeps beside a
55
+ * signed-in session. It says a session is likely, never that one exists: the
56
+ * session read decides. Same rule as the bootstrap script below, so a read
57
+ * started on it starts exactly when the early session read does.
58
+ */
59
+ export function hasSessionHint(sessionHintCookieName) {
60
+ if (typeof document === "undefined")
61
+ return false;
62
+ return document.cookie.split(";").some((cookie) => {
63
+ const entry = cookie.trim();
64
+ const separator = entry.indexOf("=");
65
+ if (separator < 1 || entry.slice(separator + 1) !== "1")
66
+ return false;
67
+ const name = entry.slice(0, separator);
68
+ return sessionHintCookieName
69
+ ? name === sessionHintCookieName
70
+ : name === "an_session_hint" ||
71
+ (name.startsWith("an_session_") && name.endsWith("_hint"));
72
+ });
73
+ }
53
74
  export function getSsrSessionBootstrapScriptBody(sessionPath, sessionHintCookieName) {
54
75
  return `(function __anEarlySessionBootstrap() {
55
76
  if (window.__agentNativeSessionBootstrap) return;
@@ -4,8 +4,8 @@ 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
6
  import { isMissingOrganizationTableError } from "../org/membership.js";
7
- import { orgMembers } from "../org/schema.js";
8
- import { organizations } from "../org/schema.js";
7
+ import { orgMembers, organizations } from "../org/schema.js";
8
+ import { implicitServiceOrgRole } from "../org/service-identity.js";
9
9
  import { getRequestAuthCapability, getRequestContext, getRequestUserEmail, getRequestOrgId, } from "../server/request-context.js";
10
10
  import { workspaceUserGroupsIncludeUser, workspaceUserGroupsTable, } from "../workspace-connections/groups.js";
11
11
  import { listShareableResources, requireShareableResource, } from "./registry.js";
@@ -63,8 +63,86 @@ function normalizeEmailForAccess(email) {
63
63
  function emailColumnMatches(column, email) {
64
64
  return sql `lower(${column}) = ${email}`;
65
65
  }
66
+ function orgMembershipScope(ctx, orgId) {
67
+ const requestContext = getRequestContext();
68
+ const serviceIdentity = requestContext?.verifiedServiceIdentity;
69
+ const normalizedUserEmail = normalizeEmailForAccess(ctx.userEmail);
70
+ if (serviceIdentity) {
71
+ if (normalizeEmailForAccess(serviceIdentity.userEmail) !==
72
+ normalizedUserEmail ||
73
+ serviceIdentity.orgId !== orgId ||
74
+ requestContext.orgId !== orgId ||
75
+ ctx.orgId !== orgId ||
76
+ !implicitServiceOrgRole({
77
+ email: serviceIdentity.userEmail,
78
+ orgId,
79
+ requestOrgId: requestContext.orgId,
80
+ })) {
81
+ return sql `1=0`;
82
+ }
83
+ return sql `exists (
84
+ select 1 from organizations as service_org
85
+ where service_org.id = ${orgId}
86
+ and coalesce(trim(service_org.identity_authority), '') = ''
87
+ and coalesce(trim(service_org.identity_id), '') = ''
88
+ )`;
89
+ }
90
+ if (implicitServiceOrgRole({
91
+ email: ctx.userEmail,
92
+ orgId,
93
+ requestOrgId: ctx.orgId,
94
+ })) {
95
+ const federationGuard = ctx.federationMembershipValidated === true
96
+ ? sql `1=1`
97
+ : sql `not exists (
98
+ select 1 from organizations as federation_org
99
+ where federation_org.id = ${orgId}
100
+ and (
101
+ coalesce(trim(federation_org.identity_authority), '') <> ''
102
+ or coalesce(trim(federation_org.identity_id), '') <> ''
103
+ )
104
+ )`;
105
+ return sql `exists (
106
+ select 1 from org_members as service_alias_member
107
+ where service_alias_member.org_id = ${orgId}
108
+ and lower(service_alias_member.email) = ${normalizedUserEmail}
109
+ and service_alias_member.federation_removal_pending_at is null
110
+ ) and ${federationGuard}`;
111
+ }
112
+ return sql `1=1`;
113
+ }
66
114
  async function isOrgMember(reg, memberOrgId, email, ctx) {
115
+ const requestContext = getRequestContext();
116
+ const verifiedServiceIdentity = requestContext?.verifiedServiceIdentity;
67
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 {
127
+ const [organization] = await db
128
+ .select({
129
+ identityAuthority: organizations.identityAuthority,
130
+ identityId: organizations.identityId,
131
+ })
132
+ .from(organizations)
133
+ .where(eq(organizations.id, memberOrgId))
134
+ .limit(1);
135
+ if (organization &&
136
+ !String(organization.identityAuthority ?? "").trim() &&
137
+ !String(organization.identityId ?? "").trim()) {
138
+ return true;
139
+ }
140
+ }
141
+ catch (error) {
142
+ if (!isMissingOrganizationTableError(error))
143
+ throw error;
144
+ }
145
+ }
68
146
  let rows;
69
147
  try {
70
148
  rows = await db
@@ -123,6 +201,7 @@ export function accessFilter(resourceTable, sharesTable, rawCtx = currentAccess(
123
201
  const publicAllowed = reg?.allowPublic !== false;
124
202
  const includePublic = (options.includePublic ?? false) && publicAllowed;
125
203
  const clauses = [];
204
+ const orgMembership = orgId ? orgMembershipScope(ctx, orgId) : sql `1=1`;
126
205
  if (normalizedUserEmail) {
127
206
  clauses.push(and(emailColumnMatches(resourceTable.ownerEmail, normalizedUserEmail), ownerScopeFilter(reg, resourceTable, ctx)));
128
207
  }
@@ -131,7 +210,7 @@ export function accessFilter(resourceTable, sharesTable, rawCtx = currentAccess(
131
210
  clauses.push(eq(resourceTable.visibility, "public"));
132
211
  }
133
212
  if (orgId) {
134
- clauses.push(and(eq(resourceTable.visibility, "org"), eq(resourceTable.orgId, orgId)));
213
+ clauses.push(and(eq(resourceTable.visibility, "org"), eq(resourceTable.orgId, orgId), orgMembership));
135
214
  }
136
215
  }
137
216
  if (normalizedUserEmail) {
@@ -149,6 +228,7 @@ export function accessFilter(resourceTable, sharesTable, rawCtx = currentAccess(
149
228
  where ${sharesTable.resourceId} = ${resourceTable.id}
150
229
  and ${sharesTable.principalType} = 'org'
151
230
  and ${sharesTable.principalId} = ${orgId}
231
+ and ${orgMembership}
152
232
  and ${shareScope}
153
233
  and ${minRoleSql(minRole)})`);
154
234
  }
@@ -160,8 +240,8 @@ export function accessFilter(resourceTable, sharesTable, rawCtx = currentAccess(
160
240
  select 1 from organizations as federation_org
161
241
  where federation_org.id = ${resourceTable.orgId}
162
242
  and (
163
- federation_org.identity_authority is not null
164
- or federation_org.identity_id is not null
243
+ coalesce(trim(federation_org.identity_authority), '') <> ''
244
+ or coalesce(trim(federation_org.identity_id), '') <> ''
165
245
  )
166
246
  )`;
167
247
  const groupMemberPredicate = sql `exists (
@@ -180,6 +260,7 @@ export function accessFilter(resourceTable, sharesTable, rawCtx = currentAccess(
180
260
  where workspace_group.id = ${sharesTable.principalId}
181
261
  and workspace_group.org_id = ${resourceTable.orgId}
182
262
  and workspace_group.org_id = ${orgId}
263
+ and ${orgMembership}
183
264
  and ${federationGuard}
184
265
  and exists (
185
266
  select 1 from ${orgMembers} as workspace_member
@@ -0,0 +1,2 @@
1
+ // Nitro's [...page] catch-all doesn't match "/", and this starter serves its home page at "/".
2
+ export { default } from "./[...page].get.js";
@@ -8,6 +8,7 @@ import { insertPendingTask, isDuplicateEventError, } from "../integrations/pendi
8
8
  import { RESUME_AUTOMATION_PATCH } from "../jobs/automation-outcome.js";
9
9
  import { nextOccurrence, describeCron, effectiveTimezone, isValidCron, } from "../jobs/cron.js";
10
10
  import { parseJobResource, patchJobFrontmatterFields, } from "../jobs/frontmatter.js";
11
+ import { automationRunOwnership } from "../jobs/run-history-ownership.js";
11
12
  import { getOrgContext } from "../org/context.js";
12
13
  import { organizationIdFromResourceOwner, organizationResourceOwner, resourceGetByPath, resourceListAllOwners, resourcePut, SHARED_OWNER, } from "../resources/store.js";
13
14
  import { getSession } from "../server/auth.js";
@@ -135,7 +136,7 @@ async function resourceToAutomationItem(event, userEmail, resource) {
135
136
  owner: resource.owner,
136
137
  appId: meta.appId,
137
138
  orgId: meta.orgId,
138
- scope: resource.owner === userEmail && !meta.orgId ? "personal" : "organization",
139
+ scope: automationRunOwnership(resource.owner, userEmail, meta.orgId).scope,
139
140
  canUpdate,
140
141
  triggerType: meta.triggerType,
141
142
  event: meta.event,
@@ -22,9 +22,9 @@ An app assigns each workflow a stable ID, a connector ID, input/output schemas,
22
22
 
23
23
  ## Worked example: notify Slack via n8n {#worked-example}
24
24
 
25
- An app that wants to hand a "notify sales" job to an n8n workflow defines it once, then invokes it by ID — never by URL:
25
+ An app that wants to hand a "notify sales" job to an n8n workflow defines it once, then invokes it by ID — never by URL. No first-party template configures a workflow connector yet, so an app opts in by adding an action like this one:
26
26
 
27
- ```ts filename="server/plugins/automation-connectors.ts"
27
+ ```ts filename="actions/invoke-automation-workflow.ts"
28
28
  import {
29
29
  createAutomationRuntime,
30
30
  createInvokeAutomationWorkflowAction,
@@ -57,7 +57,7 @@ const automation = createAutomationRuntime({
57
57
  baseUrl: "https://n8n.example.com",
58
58
  path: "/webhook/notify-sales",
59
59
  method: "POST",
60
- headers: { Authorization: "Bearer ${keys.N8N_WEBHOOK_TOKEN}" },
60
+ headers: { Authorization: "Bearer ${keys.N8N_WEBHOOK_CREDENTIAL}" },
61
61
  timeoutMs: 10_000,
62
62
  },
63
63
  },
@@ -80,6 +80,8 @@ export default createInvokeAutomationWorkflowAction(automation);
80
80
 
81
81
  The agent calls this action with `{ workflowId: "notify-sales-slack", input: { leadEmail: "jane@acme.com" } }` — it never sees `https://n8n.example.com/webhook/notify-sales` or the bearer token. `outbound.baseUrl` + `outbound.path` are static, app-owned config; the runtime rejects any attempt to call a different origin.
82
82
 
83
+ `N8N_WEBHOOK_CREDENTIAL` is the credential name the n8n integration lists. Save the credential as a key with that name, and `${keys.N8N_WEBHOOK_CREDENTIAL}` resolves it server-side. The file name sets the action name, so the agent sees this tool as `invoke-automation-workflow`.
84
+
83
85
  Mount callback handlers only under `/_agent-native/automations/`; all normal app operations remain actions. Keep the callback route minimal: authenticate, claim idempotency, enqueue, and acknowledge — never run an agent loop inside the webhook request itself.
84
86
 
85
87
  The client calls a registered action with `invokeConfiguredAutomationWorkflow()` from `@agent-native/core/client`; it does not hand-write a browser `fetch`.
@@ -5,11 +5,12 @@ description: "Event-triggered and scheduled automations with natural-language co
5
5
 
6
6
  # Automations
7
7
 
8
- An **automation** is an agent-executed task that fires on a schedule or when an
9
- event occurs: _when X happens, do Y_. **Scheduled** and **Event** are the two
10
- trigger types, not separate products. The agent executes the instructions with
11
- the same action and tool lifecycle as interactive chat; connected MCP access is
12
- limited to the automation's explicit `mcpTools` allowlist.
8
+ An **automation** is an agent-executed task that fires on a schedule, when an
9
+ event occurs, or when a webhook is called: _when X happens, do Y_.
10
+ **Scheduled**, **Event**, and **Webhook** are trigger types, not separate
11
+ products. The agent executes the instructions with the same action and tool
12
+ lifecycle as interactive chat; connected MCP access is limited to the
13
+ automation's explicit `mcpTools` allowlist.
13
14
 
14
15
  The framework originally exposed scheduled automations as **Recurring Jobs**.
15
16
  That name, the `jobs/<name>.md` storage path, and the `manage-jobs` tool remain
@@ -29,7 +30,7 @@ schedule-only integration or working with scheduler internals.
29
30
  <div class="diagram-card">
30
31
  <span class="diagram-pill">Condition</span
31
32
  ><small class="diagram-muted"
32
- >Haiku checks: &ldquo;email ends with @builder.io&rdquo;</small
33
+ >Model checks: &ldquo;email ends with @builder.io&rdquo;</small
33
34
  >
34
35
  </div>
35
36
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
@@ -63,14 +64,32 @@ schedule-only integration or working with scheduler internals.
63
64
 
64
65
  </Diagram>
65
66
 
66
- ## Two trigger types {#trigger-types}
67
+ ## Trigger types {#trigger-types}
67
68
 
68
- | Type | Fires when | Key field |
69
- | ---------- | ------------------------------------------------------ | ----------------- |
70
- | `schedule` | A cron expression matches (same as recurring jobs) | `schedule` (cron) |
71
- | `event` | A matching event is emitted on the framework event bus | `event` (name) |
69
+ | Type | Fires when | Key field |
70
+ | ---------- | ------------------------------------------------------------- | ---------------------------------- |
71
+ | `schedule` | A cron expression matches (same as recurring jobs) | `schedule` (cron) |
72
+ | `event` | A matching event is emitted on the framework event bus | `event` (name) |
73
+ | `webhook` | An HTTP `POST` arrives at the automation's secret webhook URL | The URL returned when it's created |
72
74
 
73
- Event triggers can include a `condition` — a natural-language string evaluated by Haiku against the event payload before dispatch. If the condition doesn't match, the automation is silently skipped.
75
+ Event and webhook triggers can include a `condition`: a natural-language string a model checks against the payload before dispatch. If the condition doesn't match, the automation is skipped and records a `skipped` status. If the condition can't be evaluated, the automation doesn't run. See [Conditions](#conditions).
76
+
77
+ ### Webhook triggers {#webhook-triggers}
78
+
79
+ Creating a webhook automation returns a URL path of the form
80
+ `/_agent-native/automations/webhook/<token>`. The token is the secret, so give
81
+ the URL only to the service that calls it. Each `POST` to that URL becomes the
82
+ automation's payload, up to 64 KB. A body sent with a JSON content type is
83
+ parsed, and any other body is passed as text. The endpoint answers `202` and
84
+ runs the automation in the background.
85
+
86
+ Send a stable event ID in an `X-Webhook-Event-Id`, `X-Event-Id`, or
87
+ `X-GitHub-Delivery` header, or as a top-level string `id` in a JSON body. A
88
+ repeated delivery with the same ID isn't queued again and gets a `200` reply
89
+ marked as a duplicate. The ID only stops duplicate deliveries. A failed run can
90
+ be retried, and the retry can repeat side effects the failed run already
91
+ caused. Once a delivery fails for good, its ID is released, and a later
92
+ delivery with that ID is queued as a new one.
74
93
 
75
94
  ## Creating automations {#creating}
76
95
 
@@ -82,10 +101,17 @@ The agent discovers available events, confirms the plan, and writes the automati
82
101
 
83
102
  ### From the Automations UI
84
103
 
85
- Open **Automations** on the full-page Agent surface. It is the primary place to
86
- view scheduled and event-triggered automations, inspect status, pause or resume
87
- them, and delete them. Its compatibility URL remains `/agent#jobs`, so existing
88
- links and bookmarks continue to work.
104
+ Open **Settings › Automations**. It is the primary place to view scheduled,
105
+ event-triggered, and webhook-triggered automations, inspect status, run one
106
+ now, pause or resume them, and delete them. Old `/agent#jobs` links redirect
107
+ there.
108
+
109
+ **Run now** is the quickest way to test an automation. It runs the
110
+ automation's instructions immediately, with their real side effects. It skips
111
+ the trigger and the condition, and it doesn't change the next scheduled run,
112
+ with one exception: if the framework paused the automation, a successful run
113
+ turns it back on, and a scheduled automation gets its next run calculated
114
+ again. The agent can do the same with `manage-automations` and `action=run-now`.
89
115
 
90
116
  <Callout tone="info">
91
117
 
@@ -120,7 +146,7 @@ The third path — writing the `jobs/<name>.md` file by hand via `resourcePut`
120
146
  {
121
147
  lines: "6",
122
148
  label: "Gate",
123
- note: "An optional natural-language `condition`, evaluated by Haiku against the payload before dispatch.",
149
+ note: "An optional natural-language `condition`, checked by a model against the payload before dispatch.",
124
150
  },
125
151
  {
126
152
  lines: "12",
@@ -134,15 +160,15 @@ The third path — writing the `jobs/<name>.md` file by hand via `resourcePut`
134
160
 
135
161
  Automations share every field in the [recurring-jobs frontmatter table](/docs/recurring-jobs#frontmatter). These additional fields control event triggers, conditions, and the execution mode:
136
162
 
137
- | Field | Type | Default | Description |
138
- | ------------- | ------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
139
- | `triggerType` | `"schedule"` \| `"event"` | `"schedule"` | How the automation fires |
140
- | `event` | string | _(optional)_ | Event name to subscribe to (event triggers only) |
141
- | `condition` | string | _(optional)_ | Natural-language condition evaluated before dispatch |
142
- | `mode` | `"agentic"` | `"agentic"` | Full agent loop (the only supported mode). `"deterministic"` was removed — it was never implemented, and automations that set it never fired. |
143
- | `domain` | string | _(optional)_ | Grouping tag (mail, calendar, clips, etc.) |
163
+ | Field | Type | Default | Description |
164
+ | ------------- | ---------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
165
+ | `triggerType` | `"schedule"` \| `"event"` \| `"webhook"` | `"schedule"` | How the automation fires |
166
+ | `event` | string | _(optional)_ | Event name to subscribe to (event triggers only) |
167
+ | `condition` | string | _(optional)_ | Natural-language condition evaluated before dispatch (event and webhook triggers) |
168
+ | `mode` | `"agentic"` | `"agentic"` | Full agent loop (the only supported mode). `"deterministic"` was removed — it was never implemented, and automations that set it never fired. |
169
+ | `domain` | string | _(optional)_ | Grouping tag (mail, calendar, clips, etc.) |
144
170
 
145
- For an event trigger, `schedule` is `""` (empty); for a schedule trigger it carries the cron expression. The dispatcher also writes the same managed `lastRun` / `lastStatus` / `lastError` fields the scheduler does, plus a `"skipped"` status when a condition evaluates to false.
171
+ For an event or webhook trigger, `schedule` is `""` (empty); for a schedule trigger it carries the cron expression. The dispatcher also writes the same managed `lastRun` / `lastStatus` / `lastError` fields the scheduler does, plus a `"skipped"` status when a condition evaluates to false. When a condition can't be evaluated, see [Conditions](#conditions) for what gets recorded.
146
172
 
147
173
  `model` selects a per-automation model. `mcpTools` is an explicit list of MCP
148
174
  tool names such as `mcp__server__tool`. The shared execution runner exposes
@@ -224,15 +250,15 @@ organization event automations both require an exact match with their
224
250
 
225
251
  ## Conditions {#conditions}
226
252
 
227
- Conditions are natural-language strings evaluated by Claude Haiku against the event payload. This is a yes/no classification, not a generation task.
253
+ Conditions are natural-language strings a model checks against the event or webhook payload. This is a yes/no classification, not a generation task.
228
254
 
229
255
  - **Empty or missing condition** = unconditional (always fires).
230
- - Results are memoized (SHA-256 of condition + payload) with a 5-minute TTL and 500-entry LRU cache.
231
- - Payload is truncated to 4000 characters before sending to Haiku.
256
+ - Results are cached for 5 minutes, keyed by a SHA-256 hash of the condition and payload, with at most 500 entries.
257
+ - Payload is truncated to 4000 characters before it is sent to the model.
232
258
 
233
259
  <Callout tone="info">
234
260
 
235
- On API failure, the condition evaluates to `false` (safe default — the automation is skipped rather than run on an unverified condition).
261
+ A condition that can't be evaluated never counts as a match or as a non-match, and the automation doesn't run. A provider error records `lastStatus: error` with the reason in `lastError`, and the event is retried. If no API key is available to evaluate the condition, an event automation records an error for each matching event and pauses itself after three in a row. Once a key is available, a running scheduler turns it back on by itself within about 15 minutes. On a host where no scheduler runs, turn it back on in **Settings › Automations**. A webhook automation in the same state doesn't pause. Its delivery fails and is retried.
236
262
 
237
263
  </Callout>
238
264
 
@@ -283,7 +309,7 @@ Placeholders are resolved **server-side** after the agent emits the tool call
283
309
  Keys are ad-hoc secrets created by users or the agent for automation use (e.g. `SLACK_WEBHOOK`, `HUBSPOT_API_KEY`). They differ from registered secrets (`registerRequiredSecret`) in that they have no template-defined metadata or onboarding step.
284
310
 
285
311
  - Created via the settings UI or the `/_agent-native/secrets/adhoc` API.
286
- - Each key can have a **URL allowlist** that restricts which origins the key can be sent to (origin-level matching).
312
+ - A key created through the API can include a `urlAllowlist` that restricts which origins the key can be sent to (origin-level matching). Keys added in Settings have no allowlist.
287
313
  - The raw value is never exposed to the AI — only `${keys.NAME}` placeholders appear in the agent's context.
288
314
  - Resolution falls back from user scope to workspace scope, so users can override shared keys.
289
315
 
@@ -292,14 +318,16 @@ Keys are ad-hoc secrets created by users or the agent for automation use (e.g. `
292
318
  Trigger-aware personal and organization operations use `manage-automations`
293
319
  with an `action` parameter:
294
320
 
295
- | Action | Purpose |
296
- | ------------- | -------------------------------------------------------------------- |
297
- | `list-events` | Discover all registered events with descriptions and payload schemas |
298
- | `list` | List all automations with status; filter by domain or enabled |
299
- | `define` | Create a new automation (name, trigger type, event, condition, body) |
300
- | `update` | Update an existing automation (enabled, condition, body) |
301
- | `delete` | Delete an automation (always confirms with user first) |
302
- | `fire-test` | Emit a `test.event.fired` event to validate automations |
321
+ | Action | Purpose |
322
+ | ------------- | ---------------------------------------------------------------------------------------- |
323
+ | `list-events` | Discover all registered events with descriptions and payload schemas |
324
+ | `list-hosts` | List paired execution hosts before assigning one to an automation |
325
+ | `list` | List all automations with status; filter by domain or enabled |
326
+ | `define` | Create a new automation (name, trigger type, event, condition, body) |
327
+ | `update` | Update an existing automation (enabled, condition, body) |
328
+ | `delete` | Delete an automation (always confirms with user first) |
329
+ | `run-now` | Run one automation immediately, with real side effects. Skips the trigger and condition. |
330
+ | `fire-test` | Emit a `test.event.fired` event. Only automations subscribed to that event respond. |
303
331
 
304
332
  Additional tool: `web-request` — outbound HTTP with `${keys.NAME}` substitution.
305
333
 
@@ -309,22 +337,23 @@ tool for existing schedule-only integrations and delivery metadata.
309
337
 
310
338
  ## API endpoints {#api}
311
339
 
312
- | Endpoint | Method | Description |
313
- | -------------------------------------- | ------ | ------------------------------- |
314
- | `/_agent-native/automations` | GET | List all automations (parsed) |
315
- | `/_agent-native/automations/fire-test` | POST | Emit a `test.event.fired` event |
316
- | `/_agent-native/secrets/adhoc` | GET | List ad-hoc keys (no values) |
317
- | `/_agent-native/secrets/adhoc` | POST | Create or update an ad-hoc key |
318
- | `/_agent-native/secrets/adhoc/:name` | DELETE | Delete an ad-hoc key |
340
+ | Endpoint | Method | Description |
341
+ | ------------------------------------------- | ------ | ------------------------------- |
342
+ | `/_agent-native/automations` | GET | List all automations (parsed) |
343
+ | `/_agent-native/automations/fire-test` | POST | Emit a `test.event.fired` event |
344
+ | `/_agent-native/automations/webhook/:token` | POST | Trigger a webhook automation |
345
+ | `/_agent-native/secrets/adhoc` | GET | List ad-hoc keys (no values) |
346
+ | `/_agent-native/secrets/adhoc` | POST | Create or update an ad-hoc key |
347
+ | `/_agent-native/secrets/adhoc/:name` | DELETE | Delete an ad-hoc key |
319
348
 
320
- <Endpoint id="doc-block-uyjlfp" title="Fire a test event" method="POST" path="/_agent-native/automations/fire-test" summary="Emit a test.event.fired event to validate event-triggered automations" responses={[
349
+ <Endpoint id="doc-block-uyjlfp" title="Fire a test event" method="POST" path="/_agent-native/automations/fire-test" summary="Emit a test.event.fired event to exercise automations subscribed to it" responses={[
321
350
  {
322
351
  "status": "200",
323
352
  "description": "Event emitted; matching automations are dispatched through the normal condition + ownership path."
324
353
  }
325
354
  ]}>
326
355
 
327
- Confirm an automation's wiring and condition without waiting for a real provider event. Equivalent to the `manage-automations` action `fire-test`.
356
+ Checks the event path (matching, owner scoping, and condition) for an automation subscribed to `test.event.fired`. It emits only that event, so it can't trigger an automation that listens for a real event such as `calendar.booking.created`. To try a real automation, use **Run now**. Equivalent to the `manage-automations` action `fire-test`.
328
357
 
329
358
  </Endpoint>
330
359
 
@@ -354,8 +383,8 @@ Confirm an automation's wiring and condition without waiting for a real provider
354
383
  <div class="diagram-card">
355
384
  <span class="diagram-pill warn">condition</span
356
385
  ><small class="diagram-muted"
357
- >Haiku yes/no on the payload &mdash; false &rarr;
358
- <code>skipped</code></small
386
+ >model yes/no on the payload &mdash; false &rarr; <code>skipped</code>,
387
+ failure &rarr; <code>error</code></small
359
388
  >
360
389
  </div>
361
390
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>