@agent-native/core 0.209.1 → 0.209.2-nightly-20261016005836

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 (97) hide show
  1. package/dist/agent/actions/list-model-providers.js +15 -2
  2. package/dist/agent/app-model-defaults.d.ts +9 -13
  3. package/dist/agent/app-model-defaults.js +23 -5
  4. package/dist/agent/default-agent-engine.d.ts +5 -3
  5. package/dist/agent/default-agent-engine.js +38 -8
  6. package/dist/agent/durable-background.d.ts +2 -0
  7. package/dist/agent/durable-background.js +2 -0
  8. package/dist/agent/engine/registry.js +6 -0
  9. package/dist/agent/production-agent.js +58 -5
  10. package/dist/app-config/clips.d.ts +43 -0
  11. package/dist/app-config/clips.js +91 -0
  12. package/dist/app-config/index.d.ts +1 -0
  13. package/dist/app-config/index.js +1 -0
  14. package/dist/app-config/schema.d.ts +17 -0
  15. package/dist/app-config/schema.js +2 -0
  16. package/dist/automations/service.d.ts +2 -0
  17. package/dist/automations/service.js +4 -1
  18. package/dist/client/analytics.js +48 -1
  19. package/dist/client/application-state.js +7 -2
  20. package/dist/client/chat/agentkit-protocol.js +12 -3
  21. package/dist/client/client-bootstrap.js +2 -0
  22. package/dist/client/problem-report.d.ts +11 -0
  23. package/dist/client/problem-report.js +19 -0
  24. package/dist/client/recorder.d.ts +88 -0
  25. package/dist/client/recorder.js +585 -0
  26. package/dist/client/route-chunk-recovery.js +4 -1
  27. package/dist/client/sign-out.js +2 -0
  28. package/dist/client/use-action.js +3 -0
  29. package/dist/deploy/workspace-deploy.js +77 -3
  30. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  31. package/dist/integrations/google-docs-poller.js +12 -5
  32. package/dist/integrations/plugin.js +86 -19
  33. package/dist/integrations/request-scope.d.ts +16 -0
  34. package/dist/integrations/request-scope.js +27 -0
  35. package/dist/integrations/webhook-handler.d.ts +2 -0
  36. package/dist/integrations/webhook-handler.js +38 -18
  37. package/dist/jobs/background-automation-runner.d.ts +2 -0
  38. package/dist/jobs/background-automation-runner.js +25 -2
  39. package/dist/jobs/scheduler.js +36 -22
  40. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  41. package/dist/notifications/routes.d.ts +2 -2
  42. package/dist/observability/routes.d.ts +1 -1
  43. package/dist/org/context.d.ts +12 -0
  44. package/dist/org/context.js +26 -11
  45. package/dist/org/index.d.ts +1 -1
  46. package/dist/problem-report/actions/read-problem-report.d.ts +27 -0
  47. package/dist/problem-report/actions/read-problem-report.js +103 -0
  48. package/dist/problem-report/actions/save-problem-report.d.ts +35 -0
  49. package/dist/problem-report/actions/save-problem-report.js +36 -0
  50. package/dist/problem-report/format.d.ts +7 -0
  51. package/dist/problem-report/format.js +110 -0
  52. package/dist/problem-report/schema.d.ts +41 -0
  53. package/dist/problem-report/schema.js +34 -0
  54. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  55. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  56. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  57. package/dist/scripts/agent-engines/manage-agent-engine.js +9 -4
  58. package/dist/scripts/agent-engines/set-agent-engine.js +7 -1
  59. package/dist/server/action-discovery.js +11 -0
  60. package/dist/server/action-routes.js +2 -0
  61. package/dist/server/agent-chat-ai-setup.d.ts +2 -3
  62. package/dist/server/agent-chat-ai-setup.js +18 -4
  63. package/dist/server/agent-chat-plugin.js +97 -22
  64. package/dist/server/agent-chat-stream.d.ts +17 -5
  65. package/dist/server/agent-chat-stream.js +15 -0
  66. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  67. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  68. package/dist/server/agent-engine-default-model-route.js +39 -6
  69. package/dist/server/agent-engine-status-cache.d.ts +4 -0
  70. package/dist/server/agent-engine-status-cache.js +2 -0
  71. package/dist/server/agent-run-context.d.ts +9 -0
  72. package/dist/server/agent-run-context.js +82 -17
  73. package/dist/server/auth.js +6 -1
  74. package/dist/server/builder-browser.js +18 -3
  75. package/dist/server/chatgpt-subscription-oauth.js +12 -17
  76. package/dist/server/core-routes-plugin.d.ts +1 -0
  77. package/dist/server/core-routes-plugin.js +46 -9
  78. package/dist/server/index.d.ts +1 -1
  79. package/dist/server/index.js +1 -1
  80. package/dist/server/loopback.d.ts +1 -0
  81. package/dist/server/loopback.js +30 -0
  82. package/dist/server/origin-allowlist.d.ts +2 -0
  83. package/dist/server/origin-allowlist.js +13 -1
  84. package/dist/server/realtime-token.d.ts +1 -1
  85. package/dist/server/request-context.d.ts +4 -0
  86. package/dist/templates/chat/app/components/layout/Sidebar.tsx +11 -1
  87. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +2 -2
  88. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +31 -0
  89. package/dist/triggers/dispatcher.js +117 -31
  90. package/docs/content/recorder.mdx +259 -0
  91. package/docs/content/template-clips-developers.mdx +9 -8
  92. package/docs/content/template-clips-features.mdx +8 -7
  93. package/package.json +7 -4
  94. package/src/templates/chat/app/components/layout/Sidebar.tsx +11 -1
  95. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +2 -2
  96. package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +31 -0
  97. package/third-party-licenses/TABLER-ICONS.txt +21 -0
@@ -11,7 +11,7 @@ import { readDeployCredentialEnv } from "./credential-provider.js";
11
11
  import { getWorkspaceA2ADerivedSecret } from "./derived-secret.js";
12
12
  import { publicFrameworkPath } from "./framework-route-prefix.js";
13
13
  import { getAppBasePath, getOrigin, getOAuthStateSigningKey, isAllowedOAuthRedirectUri, isConfiguredAppOrigin, } from "./google-oauth.js";
14
- import { isLoopbackOrigin } from "./origin-allowlist.js";
14
+ import { getAppDeclaredOrigins, isLoopbackOrigin } from "./origin-allowlist.js";
15
15
  import { getRequestOrgId, getRequestUserEmail } from "./request-context.js";
16
16
  const DEFAULT_BUILDER_APP_HOST = "https://builder.io";
17
17
  const DEFAULT_BUILDER_API_HOST = "https://api.builder.io";
@@ -981,7 +981,7 @@ function getBuilderConnectCallbackOrigin(event, originOverride) {
981
981
  if (isBuilderCloudRequestHost(headerHost)) {
982
982
  return getBuilderBrowserOriginForEvent(event);
983
983
  }
984
- const configuredOrigin = getOrigin(event, { useForwardedHost: false });
984
+ const configuredOrigin = getBuilderAppOrigin(event);
985
985
  if (!isLoopbackOrigin(configuredOrigin))
986
986
  return configuredOrigin;
987
987
  if (!isLoopbackBuilderRequestHost(headerHost) &&
@@ -992,6 +992,21 @@ function getBuilderConnectCallbackOrigin(event, originOverride) {
992
992
  }
993
993
  return configuredOrigin;
994
994
  }
995
+ /**
996
+ * A proxy that rewrites `Host` (Builder Cloud -> `*.netlify.app`) hands us an
997
+ * origin the browser never visited, and the origin lock 404s it. When the
998
+ * inbound origin is not one the app declared, send the browser to the first
999
+ * declared origin instead.
1000
+ */
1001
+ function getBuilderAppOrigin(event) {
1002
+ const inbound = getOrigin(event, { useForwardedHost: false });
1003
+ if (process.env.NODE_ENV !== "production")
1004
+ return inbound;
1005
+ const declared = getAppDeclaredOrigins().filter((origin) => !isLoopbackOrigin(origin));
1006
+ if (declared.length === 0 || declared.includes(inbound))
1007
+ return inbound;
1008
+ return declared[0];
1009
+ }
995
1010
  function isAllowedBuilderConnectOrigin(origin) {
996
1011
  let url;
997
1012
  try {
@@ -1126,7 +1141,7 @@ export function getBuilderBrowserOriginForEvent(event) {
1126
1141
  return getConfiguredBuilderFallbackOrigin(event) ?? "";
1127
1142
  }
1128
1143
  if (!isTrustedBuilderRequestHost(headerHost)) {
1129
- return getOrigin(event, { useForwardedHost: false });
1144
+ return getBuilderAppOrigin(event);
1130
1145
  }
1131
1146
  if (isLoopbackBuilderRequestHost(headerHost)) {
1132
1147
  const publicPreviewOrigin = firstPublicBuilderPreviewOriginFromEnv();
@@ -10,9 +10,10 @@ import { listOAuthAccountsByOwner, markOAuthReconnectRequired, readOAuthCredenti
10
10
  import { encryptSecretValue, decryptSecretValue } from "../secrets/crypto.js";
11
11
  import { getSetting, mutateSetting } from "../settings/store.js";
12
12
  import { getUserSetting, mutateUserSetting, putUserSetting, deleteUserSetting, } from "../settings/user-settings.js";
13
- import { getSession, isLoopbackRequest, redirectWithStagedCookies, } from "./auth.js";
13
+ import { getSession, redirectWithStagedCookies } from "./auth.js";
14
14
  import { oauthErrorPage } from "./google-oauth.js";
15
- import { getRequestContext } from "./request-context.js";
15
+ import { isDirectLoopbackRequest } from "./loopback.js";
16
+ import { getRequestContext, getRequestRunContext } from "./request-context.js";
16
17
  export const CHATGPT_SUBSCRIPTION_OAUTH_PROVIDER = CHATGPT_SUBSCRIPTION_PROVIDER;
17
18
  const CHATGPT_OAUTH_FLOW_COOKIE = "an_chatgpt_subscription_oauth";
18
19
  const CHATGPT_OAUTH_FLOW_TTL_SECONDS = 10 * 60;
@@ -146,23 +147,10 @@ export function isChatGPTSubscriptionLoopbackCallbackUri(value) {
146
147
  url.port !== "0" &&
147
148
  url.href === value);
148
149
  }
149
- function exactLoopbackSocketAddress(address) {
150
- return address === "127.0.0.1" || address === "::ffff:127.0.0.1";
151
- }
152
150
  function callbackUriForEvent(event) {
153
151
  const host = getRequestHeader(event, "host");
154
- const request = event.node?.req;
155
- const socket = request?.socket;
156
- if (!host ||
157
- host.trim() !== host ||
158
- getRequestHeader(event, "x-forwarded-host") ||
159
- getRequestHeader(event, "x-forwarded-proto") ||
160
- !isLoopbackRequest(event) ||
161
- !socket ||
162
- ("encrypted" in socket && socket.encrypted) ||
163
- !exactLoopbackSocketAddress(socket.localAddress)) {
152
+ if (!host || !isDirectLoopbackRequest(event))
164
153
  return null;
165
- }
166
154
  const originValue = `http://${host}`;
167
155
  if (!URL.canParse(originValue))
168
156
  return null;
@@ -181,9 +169,16 @@ function callbackUriForEvent(event) {
181
169
  : null;
182
170
  }
183
171
  function currentRequestIsLocalLoopback() {
172
+ if (getRequestRunContext()?.isBackgroundWorker === true)
173
+ return false;
184
174
  const context = getRequestContext();
185
- if (!context?.isLoopbackRequest || !context.requestOrigin)
175
+ if (!context?.isLoopbackRequest ||
176
+ !context.isDirectLoopbackRequest ||
177
+ !context.requestOrigin ||
178
+ context.orgId ||
179
+ context.orgScope !== "personal") {
186
180
  return false;
181
+ }
187
182
  if (!URL.canParse(context.requestOrigin))
188
183
  return false;
189
184
  const origin = new URL(context.requestOrigin);
@@ -31,6 +31,7 @@ export declare const LEGACY_FRAMEWORK_EVENTS_ROUTE = "/_agent-native/poll-events
31
31
  export declare function resolveAgentEngineStatusIdentity(event: H3Event): Promise<{
32
32
  userEmail: string | undefined;
33
33
  orgId: string | undefined;
34
+ orgScope?: "personal";
34
35
  }>;
35
36
  /**
36
37
  * Shared Builder grants stay org-scoped, but connect initiation can come from
@@ -28,7 +28,7 @@ import { handleMcpOAuth, handleMcpOAuthAuthorizationServerMetadata, handleMcpOAu
28
28
  import { MCP_ROUTE_PREFIXES } from "../mcp/route-paths.js";
29
29
  import { isSlackWebhookConfigured, registerBuiltinNotificationChannels, } from "../notifications/channels.js";
30
30
  import { createNotificationsHandler } from "../notifications/routes.js";
31
- import { getOrgContext } from "../org/context.js";
31
+ import { getOrgContext, hasOrgScopeResolutionUnavailable, resolveOrgScopeForEmail, } from "../org/context.js";
32
32
  import { isMissingOrganizationTableError } from "../org/membership.js";
33
33
  import { createProgressHandler } from "../progress/routes.js";
34
34
  import { REALTIME_POLL_LIVE_QUERY_PARAM } from "../realtime-protocol.js";
@@ -84,6 +84,7 @@ import { getAppBasePath, getOrigin } from "./google-oauth.js";
84
84
  import { createGoogleRealtimeSessionHandler } from "./google-realtime-session.js";
85
85
  import { readBody, readBodyWithSizeLimit, DEFAULT_UPLOAD_MAX_FILE_BYTES, isAllowedUploadMimeType, } from "./h3-helpers.js";
86
86
  import { handleIdentitySso } from "./identity-sso.js";
87
+ import { isDirectLoopbackRequest, isLoopbackRequest } from "./loopback.js";
87
88
  import { createOpenRouteHandler } from "./open-route.js";
88
89
  import { PERSONAL_PROVIDER_KEYS_RESTRICTED_ERROR_CODE, PERSONAL_PROVIDER_KEYS_RESTRICTED_MESSAGE, readOrgMemberRole, } from "./personal-provider-key-policy.js";
89
90
  import { createPollEventsHandler, validateSseMaxDurationMs, } from "./poll-events.js";
@@ -146,7 +147,13 @@ function requestAgentEngineStatusDeps() {
146
147
  const appId = normalizeAgentAppModelDefaultAppId(app.id ?? app.template ?? app.slug);
147
148
  if (!appId)
148
149
  return null;
149
- const settings = await readAgentAppModelDefaultSettings({ userEmail: getRequestUserEmail(), orgId: getRequestOrgId() }, appId);
150
+ const settings = await readAgentAppModelDefaultSettings({
151
+ userEmail: getRequestUserEmail(),
152
+ orgId: getRequestOrgId(),
153
+ ...(getRequestContext()?.orgScope === "personal"
154
+ ? { orgScope: "personal" }
155
+ : {}),
156
+ }, appId);
150
157
  return settings.engine && settings.model
151
158
  ? { engine: settings.engine, model: settings.model }
152
159
  : null;
@@ -177,7 +184,21 @@ export async function resolveAgentEngineStatusIdentity(event) {
177
184
  return { userEmail: undefined, orgId: undefined };
178
185
  try {
179
186
  const orgCtx = await getOrgContext(event);
180
- return { userEmail, orgId: orgCtx.orgId ?? undefined };
187
+ if (orgCtx.orgId)
188
+ return { userEmail, orgId: orgCtx.orgId };
189
+ if (hasOrgScopeResolutionUnavailable(event)) {
190
+ return { userEmail, orgId: undefined };
191
+ }
192
+ if (orgCtx.email.trim().toLowerCase() === userEmail.trim().toLowerCase()) {
193
+ return { userEmail, orgId: undefined, orgScope: "personal" };
194
+ }
195
+ const scope = await resolveOrgScopeForEmail(userEmail);
196
+ if (scope.status === "organization") {
197
+ return { userEmail, orgId: scope.orgId };
198
+ }
199
+ return scope.status === "personal"
200
+ ? { userEmail, orgId: undefined, orgScope: "personal" }
201
+ : { userEmail, orgId: undefined };
181
202
  }
182
203
  catch (error) {
183
204
  /* org module not present in this template */
@@ -1007,10 +1028,10 @@ export function resolveAvatarEmailParam(pathname, appBasePath = "") {
1007
1028
  }
1008
1029
  async function detectUsageEngineName(event, userEmail) {
1009
1030
  try {
1010
- const orgId = userEmail
1011
- ? (await resolveAgentEngineStatusIdentity(event)).orgId
1012
- : undefined;
1013
- const status = await runWithRequestContext({ userEmail, orgId }, () => resolveAgentEngineStatus({
1031
+ const identity = userEmail
1032
+ ? await resolveAgentEngineStatusIdentity(event)
1033
+ : { userEmail, orgId: undefined };
1034
+ const status = await runWithRequestContext(identity, () => resolveAgentEngineStatus({
1014
1035
  ...requestAgentEngineStatusDeps(),
1015
1036
  // Tracking only needs the engine name; skip the base-URL secret read.
1016
1037
  readOpenAiBaseUrlConfigured: () => false,
@@ -3990,8 +4011,24 @@ export function createCoreRoutesPlugin(options = {}) {
3990
4011
  getH3App(nitroApp).use(`${P}/agent-engine/status`, defineEventHandler(async (event) => {
3991
4012
  try {
3992
4013
  const statusStartedAt = Date.now();
3993
- const { userEmail, orgId } = await resolveAgentEngineStatusIdentity(event);
3994
- const status = await memoizeAgentEngineStatus({ userEmail, orgId }, async () => await runWithRequestContext({ userEmail, orgId }, async () => {
4014
+ const { userEmail, orgId, orgScope } = await resolveAgentEngineStatusIdentity(event);
4015
+ const directLoopbackRequest = isDirectLoopbackRequest(event);
4016
+ const requestOrigin = directLoopbackRequest
4017
+ ? `http://${getHeader(event, "host")}`
4018
+ : undefined;
4019
+ const status = await memoizeAgentEngineStatus({
4020
+ userEmail,
4021
+ orgId,
4022
+ orgScope,
4023
+ isDirectLoopbackRequest: directLoopbackRequest,
4024
+ }, async () => await runWithRequestContext({
4025
+ userEmail,
4026
+ orgId,
4027
+ ...(orgScope ? { orgScope } : {}),
4028
+ isLoopbackRequest: isLoopbackRequest(event),
4029
+ isDirectLoopbackRequest: directLoopbackRequest,
4030
+ ...(requestOrigin ? { requestOrigin } : {}),
4031
+ }, async () => {
3995
4032
  const deps = requestAgentEngineStatusDeps();
3996
4033
  const engineStatus = await resolveAgentEngineStatus(deps);
3997
4034
  const chatEligible = await isAgentChatAiSetupReady({
@@ -1,6 +1,6 @@
1
1
  export { readComposerWebsiteSource, type ComposerWebsiteExtraction, } from "./composer-website-source.js";
2
2
  export { isAgentKitFigmaSourceAvailable, listAgentKitCapabilities, readAgentKitIntegrationIntent, type AgentKitCapabilityAppId, type AgentKitCapabilityCatalog, type AgentKitIntegrationCapability, } from "../agentkit/capabilities.js";
3
- export { defineAppConfig, getAppConfig, resolveAppHomePath, resetAppConfigForTests, appConfigSchema, type AppConfig, type AppConfigInput, } from "../app-config/index.js";
3
+ export { defineAppConfig, getAppConfig, resolveAppHomePath, resetAppConfigForTests, resolveClipsHostedStorageConfig, appConfigSchema, type AppConfig, type AppConfigInput, type ClipsHostedStorageConfig, type ResolvedClipsHostedStorageConfig, } from "../app-config/index.js";
4
4
  export { platformWaitUntil } from "./background-work.js";
5
5
  export { resolveDeployEnvironment } from "./deploy-environment.js";
6
6
  export { isTestIdentity, testIdentitySql } from "./test-identity.js";
@@ -1,6 +1,6 @@
1
1
  export { readComposerWebsiteSource, } from "./composer-website-source.js";
2
2
  export { isAgentKitFigmaSourceAvailable, listAgentKitCapabilities, readAgentKitIntegrationIntent, } from "../agentkit/capabilities.js";
3
- export { defineAppConfig, getAppConfig, resolveAppHomePath, resetAppConfigForTests, appConfigSchema, } from "../app-config/index.js";
3
+ export { defineAppConfig, getAppConfig, resolveAppHomePath, resetAppConfigForTests, resolveClipsHostedStorageConfig, appConfigSchema, } from "../app-config/index.js";
4
4
  export { platformWaitUntil } from "./background-work.js";
5
5
  export { resolveDeployEnvironment } from "./deploy-environment.js";
6
6
  export { isTestIdentity, testIdentitySql } from "./test-identity.js";
@@ -1,3 +1,4 @@
1
1
  import type { H3Event } from "h3";
2
2
  export declare function isLoopbackAddress(ip: string | undefined): boolean;
3
3
  export declare function isLoopbackRequest(event: H3Event): boolean;
4
+ export declare function isDirectLoopbackRequest(event: H3Event): boolean;
@@ -16,3 +16,33 @@ export function isLoopbackRequest(event) {
16
16
  }
17
17
  return isLoopbackAddress(ip);
18
18
  }
19
+ export function isDirectLoopbackRequest(event) {
20
+ const request = event.node?.req;
21
+ const headers = request?.headers;
22
+ const host = headers?.host;
23
+ const socket = request?.socket;
24
+ const hasForwardedHeader = Object.keys(headers ?? {}).some((name) => {
25
+ const normalizedName = name.toLowerCase();
26
+ return (normalizedName === "forwarded" ||
27
+ normalizedName.startsWith("x-forwarded-"));
28
+ });
29
+ if (!isLoopbackRequest(event) ||
30
+ typeof host !== "string" ||
31
+ !/^127\.0\.0\.1(?::\d{1,5})?$/.test(host) ||
32
+ hasForwardedHeader ||
33
+ !socket ||
34
+ ("encrypted" in socket && socket.encrypted) ||
35
+ (socket.localAddress !== "127.0.0.1" &&
36
+ socket.localAddress !== "::ffff:127.0.0.1")) {
37
+ return false;
38
+ }
39
+ const originValue = `http://${host}`;
40
+ if (!URL.canParse(originValue))
41
+ return false;
42
+ const origin = new URL(originValue);
43
+ return (origin.hostname === "127.0.0.1" &&
44
+ origin.port !== "0" &&
45
+ origin.pathname === "/" &&
46
+ origin.search === "" &&
47
+ origin.hash === "");
48
+ }
@@ -7,4 +7,6 @@ export declare function firstOriginFromEnv(keys: readonly string[], options: {
7
7
  allowLoopback: boolean;
8
8
  }): string | undefined;
9
9
  export declare function getConfiguredOriginAllowlist(): Set<string>;
10
+ /** The allowlist minus platform-assigned origins, in declaration order. */
11
+ export declare function getAppDeclaredOrigins(): string[];
10
12
  export declare function requestForwardedOrigin(request: Request | undefined): string | undefined;
@@ -72,9 +72,14 @@ export function firstOriginFromEnv(keys, options) {
72
72
  }
73
73
  return undefined;
74
74
  }
75
- export function getConfiguredOriginAllowlist() {
75
+ // Netlify fills these with its own hostnames. A site served through a proxy
76
+ // (Builder Cloud) is never browsed at them, so they do not declare where the app lives.
77
+ const PLATFORM_ORIGIN_ENV_KEYS = new Set(["URL", "DEPLOY_URL"]);
78
+ function collectConfiguredOrigins(options) {
76
79
  const out = new Set();
77
80
  for (const key of EXPLICIT_PUBLIC_ORIGIN_ENV_KEYS) {
81
+ if (!options.includePlatform && PLATFORM_ORIGIN_ENV_KEYS.has(key))
82
+ continue;
78
83
  addNormalizedOrigin(out, process.env[key], { allowLoopback: true });
79
84
  }
80
85
  for (const origin of (process.env[BETTER_AUTH_TRUSTED_ORIGINS_ENV_KEY] ?? "").split(",")) {
@@ -85,6 +90,13 @@ export function getConfiguredOriginAllowlist() {
85
90
  }
86
91
  return out;
87
92
  }
93
+ export function getConfiguredOriginAllowlist() {
94
+ return collectConfiguredOrigins({ includePlatform: true });
95
+ }
96
+ /** The allowlist minus platform-assigned origins, in declaration order. */
97
+ export function getAppDeclaredOrigins() {
98
+ return [...collectConfiguredOrigins({ includePlatform: false })];
99
+ }
88
100
  export function requestForwardedOrigin(request) {
89
101
  if (!request)
90
102
  return undefined;
@@ -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
  }>>;
@@ -77,6 +77,8 @@ export interface RequestContext {
77
77
  userName?: string;
78
78
  orgId?: string;
79
79
  orgScope?: "personal";
80
+ /** Explicitly selects deployment-wide agent defaults for shared no-org jobs. */
81
+ defaultAgentEngineScope?: "deployment";
80
82
  /**
81
83
  * Narrow authorization capability verified from an embed session. This is
82
84
  * deliberately separate from user identity: capability-only sessions must
@@ -113,6 +115,8 @@ export interface RequestContext {
113
115
  * radius (a resource that is itself local-only, NODE_ENV, etc.).
114
116
  */
115
117
  isLoopbackRequest?: boolean;
118
+ /** True only when this server received the request directly on 127.0.0.1. */
119
+ isDirectLoopbackRequest?: boolean;
116
120
  isIntegrationCaller?: boolean;
117
121
  integration?: {
118
122
  taskId: string;
@@ -5,6 +5,7 @@ import {
5
5
  useChatThreads,
6
6
  type ChatThreadSummary,
7
7
  } from "@agent-native/toolkit/app/chat/agentkit-chat/rail";
8
+ import { ReportProblemButton } from "@agent-native/toolkit/app/feedback";
8
9
  import { OrgSwitcher } from "@agent-native/toolkit/app/org";
9
10
  import { openCommandMenu } from "@agent-native/toolkit/app/shared";
10
11
  import { AgentNativeIcon } from "@agent-native/toolkit/app/shared/AgentNativeIcon";
@@ -567,7 +568,16 @@ export function Sidebar({
567
568
  <ChatThreadsSection collapsed={collapsed} />
568
569
  </nav>
569
570
 
570
- <div className="mt-auto shrink-0 border-t border-sidebar-border p-2">
571
+ <div
572
+ className={cn(
573
+ "mt-auto shrink-0 px-2 pb-1",
574
+ collapsed && "flex justify-center",
575
+ )}
576
+ >
577
+ <ReportProblemButton variant={collapsed ? "icon" : "sidebar"} />
578
+ </div>
579
+
580
+ <div className="shrink-0 border-t border-sidebar-border p-2">
571
581
  <div
572
582
  className={cn(
573
583
  collapsed
@@ -126,8 +126,8 @@ record source, target, owner, exact action, and verification. Don't reopen
126
126
  closed fixes.
127
127
  **Clustered** closes one row but retains it.
128
128
 
129
- Enumerate channel parents newest backward through `next_cursor` until older
130
- than 5 days. Check reactions first; skip marked messages.
129
+ Enumerate channel parents newest-first for five days. For a permalink plus
130
+ newer scope, follow the [checked inventory procedure](references/channel-inventory.md).
131
131
 
132
132
  **`slack_search` can rank and truncate.** Use channel reads to enumerate parents
133
133
  and put their count in the recap. Sort targeted searches oldest-first and follow
@@ -0,0 +1,31 @@
1
+ # Channel parent inventory
2
+
3
+ Use this procedure before investigating, claiming, or using a CI rollup in
4
+ place of Slack source messages.
5
+
6
+ - Read each scoped channel newest-first, following `next_cursor` until the
7
+ five-day boundary, the inclusive permalink cutoff, or Slack exhaustion.
8
+ - For “this permalink and newer,” use the parent message's `ts` as the inclusive
9
+ `cutoffTs` and `permalinkTs`. Slack's `oldest` parameter is exclusive, so
10
+ query from at least one second earlier and filter locally by `ts >= cutoff`.
11
+ - Record every top-level parent returned in every fetched page before filtering
12
+ or triage. This includes duplicates, owned items, bots, and boundary overlap.
13
+ Reactions and ownership determine what to investigate, not what to inventory.
14
+ - Save `.tmp/feedback-inventory.json` with `cutoffTs`, optional `permalinkTs`,
15
+ and `channels`. Set `permalinkTs` when a supplied message anchors the scan. A channel
16
+ has `channelId`, `oldestQuery`, and `pages`. A page has its requested `cursor`,
17
+ returned `nextCursor`, `exhausted`, the full Slack `parentCount`, the complete
18
+ `sourceTimestamps` list copied from that response, and `parents`. Each parent
19
+ records `ts`, `reactionChecked: true`, `reactionSummary` (`none` if empty),
20
+ and `disposition`.
21
+ - Keep the source manifest separate from disposition rows. Copy every timestamp
22
+ and the count from the full Slack response before filtering; the checker
23
+ compares both lists, catches missing parents and requires the requested
24
+ permalink timestamp to appear. Continue until a fetched page contains a
25
+ parent older than cutoff or Slack reports exhaustion.
26
+ - Run `pnpm feedback:inventory-check -- .tmp/feedback-inventory.json`. It checks
27
+ boundary overlap, page/cursor order, count parity, timestamps, reaction state,
28
+ and dispositions. Include each channel's parent and page counts in the recap.
29
+
30
+ A grouped CI report or attachment is a discovery index, never a replacement for
31
+ the channel-parent inventory.
@@ -3,7 +3,7 @@ import { isProductionServerlessFunctionRuntime } from "../db/client.js";
3
3
  import { subscribeAll, unsubscribe } from "../event-bus/index.js";
4
4
  import { countAutomationCredentialState, trackAutomationPaused, } from "../jobs/automation-events.js";
5
5
  import { applyAutomationFailure, classifyAutomationFailure, CLEAR_FAILURE_STATE, CONFIG_INVALID_ERROR_CODE, OWNER_MISSING_ERROR_CODE, pauseNow, } from "../jobs/automation-outcome.js";
6
- import { BackgroundAutomationRunError, checkBackgroundAutomationCredentials, isBackgroundAutomationRunActive, runBackgroundAutomation, } from "../jobs/background-automation-runner.js";
6
+ import { BackgroundAutomationRunError, checkBackgroundAutomationCredentials, isBackgroundAutomationRunActive, runBackgroundAutomation, validateAutomationRunIdentity, } from "../jobs/background-automation-runner.js";
7
7
  import { buildJobResourceContent, jobBelongsToApp, parseJobResource, patchJobFrontmatterFields, } from "../jobs/frontmatter.js";
8
8
  import { registerRecurringSweepHandler, } from "../jobs/sweep-hooks.js";
9
9
  import { resourceFingerprintAllOwners, resourceGetByPath, resourceListAllOwnersWithFingerprint, resourcePutIfCurrent, } from "../resources/store.js";
@@ -853,12 +853,54 @@ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
853
853
  return "retry";
854
854
  let identity;
855
855
  if (resource.owner === "__shared__") {
856
- const userEmail = meta.createdBy || resource.owner;
856
+ const userEmail = meta.createdBy?.trim().toLowerCase();
857
+ if (!userEmail) {
858
+ await recordTriggerExecutionOutcome(resource, { lastCheck: new Date().toISOString() }, {
859
+ failure: {
860
+ code: CONFIG_INVALID_ERROR_CODE,
861
+ message: "Shared automation has no creator identity.",
862
+ precondition: true,
863
+ },
864
+ pauseImmediately: true,
865
+ eventId: queued.eventId,
866
+ });
867
+ return "completed";
868
+ }
869
+ const orgId = meta.orgId?.trim() || undefined;
870
+ let validation;
871
+ try {
872
+ validation = await validateAutomationRunIdentity(userEmail, orgId);
873
+ }
874
+ catch (error) {
875
+ await recordTriggerSkip(resource, "error", "Could not verify the automation execution identity.", "owner_unverifiable");
876
+ throw error;
877
+ }
878
+ if (!validation.ok) {
879
+ if (validation.code === OWNER_MISSING_ERROR_CODE ||
880
+ validation.code === CONFIG_INVALID_ERROR_CODE) {
881
+ await recordTriggerExecutionOutcome(resource, { lastCheck: new Date().toISOString() }, {
882
+ failure: {
883
+ code: validation.code,
884
+ message: validation.reason,
885
+ precondition: true,
886
+ },
887
+ pauseImmediately: true,
888
+ eventId: queued.eventId,
889
+ });
890
+ return "completed";
891
+ }
892
+ await recordTriggerSkip(resource, "error", validation.reason, validation.code);
893
+ return "completed";
894
+ }
857
895
  identity = {
858
896
  userEmail,
859
- orgId: meta.orgId,
897
+ orgId,
898
+ ...(!orgId ? { defaultAgentEngineScope: "deployment" } : {}),
860
899
  eventOwner: userEmail.toLowerCase(),
861
900
  };
901
+ if (!automationMatchesEventOwner(identity, queued.eventOwner)) {
902
+ return "completed";
903
+ }
862
904
  }
863
905
  else {
864
906
  let resolved;
@@ -900,7 +942,16 @@ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
900
942
  let classifierEngine;
901
943
  let classifierModel;
902
944
  if (meta.condition?.trim()) {
903
- const credentialCheck = await checkBackgroundAutomationCredentials({ ownerEmail: identity.userEmail, orgId: identity.orgId }, deps, meta.model);
945
+ const credentialCheck = await checkBackgroundAutomationCredentials({
946
+ ownerEmail: identity.userEmail,
947
+ orgId: identity.orgId,
948
+ ...(identity.orgScope === "personal"
949
+ ? { orgScope: "personal" }
950
+ : {}),
951
+ ...(identity.defaultAgentEngineScope === "deployment"
952
+ ? { defaultAgentEngineScope: "deployment" }
953
+ : {}),
954
+ }, deps, meta.model);
904
955
  if (!credentialCheck.ok) {
905
956
  await recordTriggerExecutionOutcome(resource, { lastCheck: new Date().toISOString() }, { failure: credentialCheck.failure });
906
957
  return "completed";
@@ -910,7 +961,16 @@ async function dispatchQueuedAutomationEvent(queued, deps, hardDeadlineAt) {
910
961
  }
911
962
  let matches;
912
963
  try {
913
- matches = await runWithRequestContext({ userEmail: identity.userEmail, orgId: identity.orgId }, () => evaluateCondition(meta.condition, queued.payload, {
964
+ matches = await runWithRequestContext({
965
+ userEmail: identity.userEmail,
966
+ orgId: identity.orgId,
967
+ ...(identity.orgScope === "personal"
968
+ ? { orgScope: "personal" }
969
+ : {}),
970
+ ...(identity.defaultAgentEngineScope === "deployment"
971
+ ? { defaultAgentEngineScope: "deployment" }
972
+ : {}),
973
+ }, () => evaluateCondition(meta.condition, queued.payload, {
914
974
  userEmail: identity.userEmail,
915
975
  orgId: identity.orgId,
916
976
  appId: deps.appId,
@@ -979,7 +1039,16 @@ export async function dispatchAutomationWebhookTask(task) {
979
1039
  let classifierEngine;
980
1040
  let classifierModel;
981
1041
  if (meta.condition?.trim()) {
982
- const credentialCheck = await checkBackgroundAutomationCredentials({ ownerEmail: identity.userEmail, orgId: identity.orgId }, deps, meta.model);
1042
+ const credentialCheck = await checkBackgroundAutomationCredentials({
1043
+ ownerEmail: identity.userEmail,
1044
+ orgId: identity.orgId,
1045
+ ...(identity.orgScope === "personal"
1046
+ ? { orgScope: "personal" }
1047
+ : {}),
1048
+ ...(identity.defaultAgentEngineScope === "deployment"
1049
+ ? { defaultAgentEngineScope: "deployment" }
1050
+ : {}),
1051
+ }, deps, meta.model);
983
1052
  if (!credentialCheck.ok) {
984
1053
  throw new BackgroundAutomationRunError(credentialCheck.failure.message, credentialCheck.failure.code);
985
1054
  }
@@ -991,7 +1060,16 @@ export async function dispatchAutomationWebhookTask(task) {
991
1060
  }
992
1061
  let matches;
993
1062
  try {
994
- matches = await runWithRequestContext({ userEmail: identity.userEmail, orgId: identity.orgId }, () => evaluateCondition(meta.condition, task.payload, {
1063
+ matches = await runWithRequestContext({
1064
+ userEmail: identity.userEmail,
1065
+ orgId: identity.orgId,
1066
+ ...(identity.orgScope === "personal"
1067
+ ? { orgScope: "personal" }
1068
+ : {}),
1069
+ ...(identity.defaultAgentEngineScope === "deployment"
1070
+ ? { defaultAgentEngineScope: "deployment" }
1071
+ : {}),
1072
+ }, () => evaluateCondition(meta.condition, task.payload, {
995
1073
  userEmail: identity.userEmail,
996
1074
  orgId: identity.orgId,
997
1075
  appId: deps.appId,
@@ -1083,32 +1161,40 @@ async function dispatchAgentic(resource, payload, eventMeta, identity, hardDeadl
1083
1161
  body: latestTrigger.body,
1084
1162
  resource: claimed,
1085
1163
  };
1086
- const requestContext = runningMeta.originScopeId &&
1087
- runningMeta.deliveryPlatform &&
1088
- runningMeta.deliveryDestination
1089
- ? {
1090
- isIntegrationCaller: true,
1091
- integration: {
1092
- taskId: `automation:${triggerName}:${eventMeta.eventId}`,
1093
- scopeId: runningMeta.originScopeId,
1094
- principalType: "service",
1095
- incoming: {
1096
- platform: runningMeta.deliveryPlatform,
1097
- externalThreadId: `${runningMeta.deliveryTenantId || "unknown"}:${runningMeta.deliveryDestination}:${runningMeta.deliveryThreadRef || "root"}`,
1098
- text: "",
1099
- tenantId: runningMeta.deliveryTenantId,
1100
- integrationScopeId: runningMeta.originScopeId,
1101
- platformContext: {
1102
- channelId: runningMeta.deliveryDestination,
1103
- threadTs: runningMeta.deliveryThreadRef,
1104
- teamId: runningMeta.deliveryTenantId,
1164
+ const requestContext = {
1165
+ ...(identity.orgScope === "personal"
1166
+ ? { orgScope: "personal" }
1167
+ : {}),
1168
+ ...(identity.defaultAgentEngineScope === "deployment"
1169
+ ? { defaultAgentEngineScope: "deployment" }
1170
+ : {}),
1171
+ ...(runningMeta.originScopeId &&
1172
+ runningMeta.deliveryPlatform &&
1173
+ runningMeta.deliveryDestination
1174
+ ? {
1175
+ isIntegrationCaller: true,
1176
+ integration: {
1177
+ taskId: `automation:${triggerName}:${eventMeta.eventId}`,
1178
+ scopeId: runningMeta.originScopeId,
1179
+ principalType: "service",
1180
+ incoming: {
1181
+ platform: runningMeta.deliveryPlatform,
1182
+ externalThreadId: `${runningMeta.deliveryTenantId || "unknown"}:${runningMeta.deliveryDestination}:${runningMeta.deliveryThreadRef || "root"}`,
1183
+ text: "",
1184
+ tenantId: runningMeta.deliveryTenantId,
1185
+ integrationScopeId: runningMeta.originScopeId,
1186
+ platformContext: {
1187
+ channelId: runningMeta.deliveryDestination,
1188
+ threadTs: runningMeta.deliveryThreadRef,
1189
+ teamId: runningMeta.deliveryTenantId,
1190
+ },
1191
+ threadRef: runningMeta.deliveryThreadRef,
1192
+ timestamp: now.getTime(),
1105
1193
  },
1106
- threadRef: runningMeta.deliveryThreadRef,
1107
- timestamp: now.getTime(),
1108
1194
  },
1109
- },
1110
- }
1111
- : undefined;
1195
+ }
1196
+ : {}),
1197
+ };
1112
1198
  try {
1113
1199
  const result = await runBackgroundAutomation({
1114
1200
  automation,