@agent-native/core 0.149.6 → 0.151.1

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 (173) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  3. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +83 -45
  4. package/corpus/templates/analytics/changelog/2026-08-11-analytics-dashboards-keep-the-selected-tab-when-reopened.md +6 -0
  5. package/corpus/templates/assets/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  6. package/corpus/templates/brain/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  7. package/corpus/templates/calendar/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  8. package/corpus/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  9. package/corpus/templates/clips/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  10. package/corpus/templates/clips/changelog/2026-08-11-clips-recovers-from-transient-updates-and-avoids-macos-priva.md +6 -0
  11. package/corpus/templates/clips/desktop/src/app.tsx +32 -176
  12. package/corpus/templates/clips/desktop/src/lib/updater.ts +52 -37
  13. package/corpus/templates/clips/desktop/src/styles.css +1 -166
  14. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -0
  15. package/corpus/templates/clips/desktop/src-tauri/Info.plist +4 -0
  16. package/corpus/templates/clips/desktop/src-tauri/src/eventkit.rs +28 -1
  17. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  18. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +48 -1
  19. package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -0
  20. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +5 -0
  21. package/corpus/templates/clips/server/plugins/auth.ts +3 -0
  22. package/corpus/templates/content/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  23. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +2 -1
  24. package/corpus/templates/design/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  25. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +5 -1
  26. package/corpus/templates/design/changelog/2026-08-11-design-agent-prompts-copy-successfully-in-the-desktop-app.md +6 -0
  27. package/corpus/templates/dispatch/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  28. package/corpus/templates/dispatch/app/global.css +2 -2
  29. package/corpus/templates/dispatch/changelog/2026-08-10-desktop-workspace-sign-in.md +6 -0
  30. package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-shows-the-generic-chat-starter-among-defa.md +6 -0
  31. package/corpus/templates/dispatch/changelog/2026-08-11-integrations-and-scheduled-work-open-as-first-class-electron-pages.md +6 -0
  32. package/corpus/templates/dispatch/changelog/2026-08-11-sidebar-text-is-slightly-easier-to-read-in-light-and-dark-mo.md +6 -0
  33. package/corpus/templates/dispatch/server/lib/identity-sso.ts +57 -31
  34. package/corpus/templates/dispatch/server/plugins/feature-flags.ts +5 -0
  35. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +82 -33
  36. package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +7 -15
  37. package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +9 -1
  38. package/corpus/templates/dispatch/shared/feature-flags.ts +15 -0
  39. package/corpus/templates/factory/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  40. package/corpus/templates/forms/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  41. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +8 -7
  42. package/corpus/templates/forms/app/hooks/use-forms.ts +8 -1
  43. package/corpus/templates/forms/changelog/2026-08-11-fixed-public-forms-so-conditional-questions-stay-hidden-unti.md +6 -0
  44. package/corpus/templates/forms/server/handlers/submissions.ts +2 -1
  45. package/corpus/templates/forms/server/lib/public-form-ssr.ts +37 -2
  46. package/corpus/templates/forms/shared/page-url.ts +54 -0
  47. package/corpus/templates/forms/shared/types.ts +4 -4
  48. package/corpus/templates/mail/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  49. package/corpus/templates/plan/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  50. package/corpus/templates/slides/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  51. package/corpus/templates/slides/actions/add-slide.ts +0 -27
  52. package/corpus/templates/slides/actions/import-file.ts +28 -9
  53. package/corpus/templates/slides/actions/view-screen.ts +4 -4
  54. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +8 -1
  55. package/corpus/templates/slides/app/components/editor/AiEditingMarker.tsx +25 -0
  56. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +43 -48
  57. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +9 -6
  58. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -9
  59. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +2 -0
  60. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +0 -26
  61. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +904 -217
  62. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
  63. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +11 -0
  64. package/corpus/templates/slides/app/components/editor/inline-edit-session.ts +16 -0
  65. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +22 -6
  66. package/corpus/templates/slides/app/components/editor/style-clipboard.ts +61 -0
  67. package/corpus/templates/slides/app/components/layout/Layout.tsx +26 -2
  68. package/corpus/templates/slides/app/components/ui/context-menu.tsx +1 -0
  69. package/corpus/templates/slides/app/context/DeckContext.tsx +75 -25
  70. package/corpus/templates/slides/app/global.css +1 -1
  71. package/corpus/templates/slides/app/i18n/en-US.ts +6 -1
  72. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -1
  73. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +12 -12
  74. package/corpus/templates/slides/app/lib/slide-agent-context.ts +52 -0
  75. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +61 -0
  76. package/corpus/templates/slides/app/pages/DeckEditor.tsx +55 -116
  77. package/corpus/templates/slides/app/pages/Index.tsx +28 -3
  78. package/corpus/templates/slides/changelog/2026-08-11-agent-chat-now-follows-the-current-slide-and-selection.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-08-11-deck-titles-now-use-slide-content-instead-of-placeholder-filenames.md +6 -0
  80. package/corpus/templates/slides/changelog/2026-08-11-history-now-opens-from-the-deck-overflow-menu.md +6 -0
  81. package/corpus/templates/slides/changelog/2026-08-11-images-can-be-dropped-directly-onto-slides-and-resized-or-re.md +6 -0
  82. package/corpus/templates/slides/changelog/2026-08-11-new-decks-now-transition-directly-to-a-full-page-editor-load.md +6 -0
  83. package/corpus/templates/slides/changelog/2026-08-11-selected-slide-layers-can-be-nudged-precisely-with-arrow-key.md +6 -0
  84. package/corpus/templates/slides/changelog/2026-08-11-slides-can-copy-and-paste-element-styles-with-cmd-option-c-v.md +6 -0
  85. package/corpus/templates/slides/changelog/2026-08-11-slides-shows-one-compact-ai-marker-and-keeps-your-current-sl.md +6 -0
  86. package/corpus/templates/slides/changelog/2026-08-11-text-edits-preserve-slide-layout-when-no-changes-are-made.md +6 -0
  87. package/corpus/templates/slides/changelog/2026-08-11-the-style-toolbar-now-uses-the-full-available-width.md +6 -0
  88. package/corpus/templates/slides/changelog/2026-08-11-undo-and-redo-now-move-one-visible-slide-state-at-a-time.md +6 -0
  89. package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -2
  90. package/corpus/templates/slides/shared/deck-title.ts +56 -0
  91. package/corpus/templates/tasks/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  92. package/dist/agent/run-store.js +6 -1
  93. package/dist/agent/thread-data-builder.js +19 -44
  94. package/dist/agent/types.d.ts +13 -0
  95. package/dist/catalog.json +1 -0
  96. package/dist/cli/create.js +1 -1
  97. package/dist/client/AssistantChat.js +58 -10
  98. package/dist/client/FileStorageSetupCard.d.ts +6 -0
  99. package/dist/client/FileStorageSetupCard.js +18 -0
  100. package/dist/client/NewWorkspaceAppFlow.js +10 -0
  101. package/dist/client/agent-chat-adapter.js +87 -11
  102. package/dist/client/agent-chat.d.ts +1 -1
  103. package/dist/client/agent-chat.js +8 -3
  104. package/dist/client/chat/attachment-adapters.js +10 -0
  105. package/dist/client/chat/message-components.d.ts +4 -1
  106. package/dist/client/chat/message-components.js +37 -13
  107. package/dist/client/chat/tool-call-display.js +14 -0
  108. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  109. package/dist/client/chat-first/apps-rail.js +6 -6
  110. package/dist/client/chat-first/copy.js +1 -0
  111. package/dist/client/chat-first/types.d.ts +1 -0
  112. package/dist/client/extensions/EmbeddedExtension.js +1 -1
  113. package/dist/client/extensions/ExtensionViewer.js +1 -1
  114. package/dist/client/extensions/InlineExtensionFrame.js +1 -1
  115. package/dist/client/onboarding/FirstRunOnboarding.js +17 -2
  116. package/dist/client/settings/FileStorageSettingsForm.d.ts +2 -0
  117. package/dist/client/settings/FileStorageSettingsForm.js +113 -0
  118. package/dist/client/settings/SettingsPanel.js +2 -1
  119. package/dist/client/sse-event-processor.js +21 -0
  120. package/dist/collab/struct-routes.d.ts +1 -1
  121. package/dist/extensions/actions.js +2 -2
  122. package/dist/extensions/html-shell.js +3 -1
  123. package/dist/file-upload/pre-upload-attachments.d.ts +15 -10
  124. package/dist/file-upload/pre-upload-attachments.js +68 -24
  125. package/dist/file-upload/registry.d.ts +4 -5
  126. package/dist/file-upload/registry.js +7 -14
  127. package/dist/file-upload/s3.d.ts +9 -0
  128. package/dist/file-upload/s3.js +168 -0
  129. package/dist/file-upload/types.d.ts +3 -3
  130. package/dist/file-upload/types.js +2 -2
  131. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  132. package/dist/notifications/routes.d.ts +3 -3
  133. package/dist/onboarding/app-profile.js +23 -1
  134. package/dist/onboarding/default-steps.js +69 -0
  135. package/dist/onboarding/types.d.ts +2 -0
  136. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  137. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  138. package/dist/secrets/routes.d.ts +3 -3
  139. package/dist/server/agent-chat/browser-team-tools.js +20 -0
  140. package/dist/server/agent-chat-plugin.js +12 -3
  141. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  142. package/dist/server/auth.js +45 -21
  143. package/dist/server/better-auth-instance.d.ts +2 -0
  144. package/dist/server/better-auth-instance.js +2 -2
  145. package/dist/server/core-routes-plugin.d.ts +1 -0
  146. package/dist/server/core-routes-plugin.js +26 -16
  147. package/dist/server/identity-sso-store.d.ts +3 -0
  148. package/dist/server/identity-sso-store.js +45 -0
  149. package/dist/server/identity-sso.d.ts +20 -10
  150. package/dist/server/identity-sso.js +79 -31
  151. package/dist/server/index.d.ts +1 -1
  152. package/dist/server/index.js +1 -1
  153. package/dist/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  154. package/dist/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  155. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
  156. package/dist/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  157. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
  158. package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
  159. package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
  160. package/docs/content/authentication.mdx +27 -0
  161. package/docs/content/cross-app-sso.mdx +38 -8
  162. package/docs/content/extensions.mdx +15 -15
  163. package/docs/content/file-uploads.mdx +10 -12
  164. package/docs/content/generative-ui.mdx +6 -1
  165. package/docs/content/key-concepts.mdx +141 -83
  166. package/package.json +2 -2
  167. package/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  168. package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  169. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
  170. package/src/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
  171. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
  172. package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
  173. package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ Desktop workspace sign-in can now be rolled out gradually while ordinary app sign-in remains unchanged by default.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Dispatch no longer shows the generic Chat starter among default app launchers
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Integrations and scheduled work open as first-class Electron pages without a duplicate Dispatch navigation shell.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Sidebar text is slightly easier to read in light and dark mode.
@@ -43,26 +43,30 @@ export const IDENTITY_TOKEN_TTL = "2m";
43
43
  /** JWT `scope` claim marking this as an identity (login) token, not A2A/MCP. */
44
44
  export const IDENTITY_SCOPE = "identity";
45
45
 
46
- /**
47
- * Host suffixes that a `redirect_uri` may belong to. This is the core
48
- * open-redirect / token-theft guard: an attacker-supplied `redirect_uri`
49
- * pointing anywhere else would exfiltrate the identity token.
50
- *
51
- * `.agent-native.com` covers every first-party hosted app
52
- * (mail.agent-native.com, calendar.agent-native.com, …) — the shared
53
- * first-party prod-URL registry is exactly the `*.agent-native.com`
54
- * subdomain space, so a suffix check is both sufficient and the least
55
- * footgun-prone (no per-app list to keep in sync).
56
- *
57
- * These are matched as DOT-prefixed suffixes against the parsed URL
58
- * hostname, so `agent-native.com.evil.com` does NOT match
59
- * `.agent-native.com` (it ends in `.evil.com`), and a bare
60
- * `agent-native.com` apex is intentionally NOT matched (all first-party
61
- * apps are subdomains; the apex is the marketing site).
62
- */
63
- export const DEFAULT_ALLOWED_HOST_SUFFIXES: readonly string[] = [
64
- ".agent-native.com",
65
- ];
46
+ /** Exact first-party clients trusted to receive identity tokens. */
47
+ export const CANONICAL_IDENTITY_SSO_APP_ORIGINS = {
48
+ analytics: "https://analytics.agent-native.com",
49
+ assets: "https://assets.agent-native.com",
50
+ brain: "https://brain.agent-native.com",
51
+ calendar: "https://calendar.agent-native.com",
52
+ chat: "https://chat.agent-native.com",
53
+ clips: "https://clips.agent-native.com",
54
+ content: "https://content.agent-native.com",
55
+ crm: "https://crm.agent-native.com",
56
+ design: "https://design.agent-native.com",
57
+ dispatch: "https://dispatch.agent-native.com",
58
+ forms: "https://forms.agent-native.com",
59
+ macros: "https://macros.agent-native.com",
60
+ mail: "https://mail.agent-native.com",
61
+ plan: "https://plan.agent-native.com",
62
+ slides: "https://slides.agent-native.com",
63
+ tasks: "https://tasks.agent-native.com",
64
+ } as const;
65
+
66
+ export const DEFAULT_ALLOWED_ORIGINS: readonly string[] = Object.values(
67
+ CANONICAL_IDENTITY_SSO_APP_ORIGINS,
68
+ );
69
+ const IDENTITY_SSO_CALLBACK_PATH = "/_agent-native/identity/callback";
66
70
 
67
71
  /** Loopback hosts allowed for local development of the client side. */
68
72
  const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
@@ -102,14 +106,11 @@ export function getConfiguredHostSuffixes(
102
106
  * `http:` is also allowed (TLS is not available on localhost).
103
107
  * 3. It has no embedded credentials (`user:pass@host`) — those are an
104
108
  * open-redirect obfuscation vector.
105
- * 4. Its hostname is either a loopback host, OR ends in one of the
106
- * allowed host suffixes (default `.agent-native.com`, plus any
107
- * configured via env). The suffix is matched against the FULL parsed
108
- * hostname with a leading dot, so `agent-native.com.evil.com` is
109
- * rejected (ends in `.evil.com`) and substring spoofs cannot pass.
109
+ * 4. Its origin is an exact registered first-party app, a loopback host,
110
+ * or an operator-configured extra host suffix.
110
111
  *
111
- * Everything else is rejected. Fragments/queries on the URI are allowed
112
- * (the client may have its own) — we append our params safely.
112
+ * Everything else is rejected. The authorize endpoint adds a second check
113
+ * that binds the app id to the exact origin and callback path.
113
114
  */
114
115
  export function isAllowedRedirectUri(
115
116
  rawRedirectUri: unknown,
@@ -145,10 +146,9 @@ export function isAllowedRedirectUri(
145
146
 
146
147
  if (isLoopback) return true;
147
148
 
148
- const suffixes = [
149
- ...DEFAULT_ALLOWED_HOST_SUFFIXES,
150
- ...(options.allowedHostSuffixes ?? getConfiguredHostSuffixes()),
151
- ];
149
+ if (DEFAULT_ALLOWED_ORIGINS.includes(url.origin)) return true;
150
+
151
+ const suffixes = options.allowedHostSuffixes ?? getConfiguredHostSuffixes();
152
152
 
153
153
  // Suffix match against the full hostname, leading-dot anchored so a
154
154
  // sibling/parent host can never satisfy a child's suffix:
@@ -158,6 +158,32 @@ export function isAllowedRedirectUri(
158
158
  return suffixes.some((suffix) => hostname.endsWith(suffix));
159
159
  }
160
160
 
161
+ const APP_ID = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
162
+
163
+ export function isAllowedIdentityRedirect(
164
+ appId: unknown,
165
+ rawRedirectUri: unknown,
166
+ options: { allowedHostSuffixes?: readonly string[] } = {},
167
+ ): boolean {
168
+ if (typeof appId !== "string" || !APP_ID.test(appId)) return false;
169
+ if (!isAllowedRedirectUri(rawRedirectUri, options)) return false;
170
+
171
+ const url = new URL(rawRedirectUri as string);
172
+ if (url.pathname !== IDENTITY_SSO_CALLBACK_PATH || url.search || url.hash) {
173
+ return false;
174
+ }
175
+
176
+ if (DEFAULT_ALLOWED_ORIGINS.includes(url.origin)) {
177
+ return (
178
+ CANONICAL_IDENTITY_SSO_APP_ORIGINS[
179
+ appId as keyof typeof CANONICAL_IDENTITY_SSO_APP_ORIGINS
180
+ ] === url.origin
181
+ );
182
+ }
183
+
184
+ return true;
185
+ }
186
+
161
187
  export interface IdentityClaims {
162
188
  /** Stable user identifier — the Dispatch account email. */
163
189
  sub: string;
@@ -0,0 +1,5 @@
1
+ import { createFeatureFlagsPlugin } from "@agent-native/core/server";
2
+
3
+ import { DISPATCH_FEATURE_FLAGS } from "../../shared/feature-flags.js";
4
+
5
+ export default createFeatureFlagsPlugin({ flags: DISPATCH_FEATURE_FLAGS });
@@ -8,8 +8,8 @@
8
8
  * unauthenticated visitor here with:
9
9
  * ?app=<id>&redirect_uri=<https url>&state=<opaque>
10
10
  *
11
- * 2. We validate `redirect_uri` against the strict allowlist
12
- * (`isAllowedRedirectUri`). An invalid/forbidden value is rejected
11
+ * 2. We bind `app` and `redirect_uri` against the strict allowlist
12
+ * (`isAllowedIdentityRedirect`). An invalid/forbidden pair is rejected
13
13
  * with 400 BEFORE any session work — an attacker-controlled
14
14
  * `redirect_uri` must never receive a token. This is the single most
15
15
  * important control on this endpoint.
@@ -49,42 +49,40 @@
49
49
  * This is documented as the chosen trade-off.
50
50
  *
51
51
  * Auth-guard reachability:
52
- * `/_agent-native/*` is 401'd by the core auth guard when there is no
53
- * session, which would break the logged-OUT bounce. So this plugin
54
- * registers the exact path `/_agent-native/identity/authorize` as a
55
- * `publicPath` via a second `createAuthPlugin({ publicPaths })` call.
56
- * When two `createAuthPlugin` calls run in the same server boot on the
57
- * same Nitro app, the framework APPENDS publicPaths to the live guard
58
- * config (it does not clobber Dispatch's googleOnly/marketing/onboarding
59
- * — verified in packages/core/src/server/auth.ts). The path is matched
60
- * exactly (or as a `/`-segment prefix), so ONLY the authorize endpoint
61
- * becomes public; any future `/_agent-native/identity/*` route stays
62
- * protected. The handler then resolves the session ITSELF (exactly the
63
- * `/_agent-native/open` pattern) — public-path only means "guard does not
64
- * pre-empt", not "no auth": logged-out users are still bounced to login,
65
- * and a token is only minted for a real session.
52
+ * Dispatch's primary auth plugin receives the exact public paths from
53
+ * `setupDispatch`. These handlers still resolve the session themselves;
54
+ * public-path only means the guard does not pre-empt the logged-out bounce.
66
55
  */
67
56
 
68
57
  import { signA2AToken } from "@agent-native/core/a2a";
69
- import { getOrgDomain } from "@agent-native/core/org";
70
58
  import {
71
- createAuthPlugin,
72
- getH3App,
73
- getSession,
74
- } from "@agent-native/core/server";
59
+ getFeatureFlagRules,
60
+ isFeatureFlagEnabled,
61
+ normalizeFeatureFlagRules,
62
+ } from "@agent-native/core/feature-flags";
63
+ import { getOrgDomain } from "@agent-native/core/org";
64
+ import { getH3App, getSession } from "@agent-native/core/server";
75
65
  import { signInJourney } from "@agent-native/core/shared";
76
- import { defineEventHandler, getMethod } from "h3";
66
+ import { defineEventHandler, getHeader, getMethod } from "h3";
77
67
  import type { H3Event } from "h3";
78
68
 
69
+ import { DESKTOP_WORKSPACE_SSO_FLAG } from "../../shared/feature-flags.js";
79
70
  import {
80
71
  IDENTITY_TOKEN_TTL,
81
72
  buildIdentityClaims,
82
73
  buildRedirectLocation,
83
- isAllowedRedirectUri,
74
+ isAllowedIdentityRedirect,
84
75
  } from "../lib/identity-sso.js";
85
76
 
86
- /** Exact path. Registered as a publicPath so the guard does not 401 it. */
77
+ const AVAILABILITY_PATH = "/_agent-native/identity/availability";
87
78
  const AUTHORIZE_PATH = "/_agent-native/identity/authorize";
79
+ const DESKTOP_SSO_CANARY_USER_AGENT = /AgentNativeDesktopSsoCanary\//i;
80
+
81
+ export function isDesktopWorkspaceSsoRequest(
82
+ userAgent: string | undefined,
83
+ ): boolean {
84
+ return DESKTOP_SSO_CANARY_USER_AGENT.test(userAgent ?? "");
85
+ }
88
86
 
89
87
  function getRequestUrl(event: H3Event): string {
90
88
  return (event as any).node?.req?.url ?? (event as any).path ?? "/";
@@ -93,7 +91,10 @@ function getRequestUrl(event: H3Event): string {
93
91
  function jsonResponse(body: unknown, status: number): Response {
94
92
  return new Response(JSON.stringify(body), {
95
93
  status,
96
- headers: { "Content-Type": "application/json" },
94
+ headers: {
95
+ "Cache-Control": "private, no-store",
96
+ "Content-Type": "application/json",
97
+ },
97
98
  });
98
99
  }
99
100
 
@@ -118,6 +119,45 @@ async function resolveOrgDomain(
118
119
  }
119
120
  }
120
121
 
122
+ export async function canAttemptWorkspaceSso(): Promise<boolean> {
123
+ const stored = await getFeatureFlagRules(
124
+ DESKTOP_WORKSPACE_SSO_FLAG.key,
125
+ {},
126
+ ).catch(() => null);
127
+ if (!stored) return false;
128
+ const rules = normalizeFeatureFlagRules(stored);
129
+ if (rules.mode === "off") return false;
130
+ if (rules.mode === "on") return true;
131
+ return (
132
+ rules.emails.length > 0 || rules.orgIds.length > 0 || rules.percentage > 0
133
+ );
134
+ }
135
+
136
+ export async function isWorkspaceSsoEnabledForSession(
137
+ session: Awaited<ReturnType<typeof getSession>>,
138
+ ): Promise<boolean> {
139
+ if (!session?.email) return false;
140
+ return isFeatureFlagEnabled(DESKTOP_WORKSPACE_SSO_FLAG, {
141
+ userEmail: session.email,
142
+ userKey: session.email,
143
+ orgId: session.orgId,
144
+ }).catch(() => false);
145
+ }
146
+
147
+ const availabilityHandler = defineEventHandler(
148
+ async (event: H3Event): Promise<Response> => {
149
+ const method = getMethod(event);
150
+ if (method !== "GET" && method !== "HEAD") {
151
+ return jsonResponse({ error: "Method not allowed" }, 405);
152
+ }
153
+ const session = await getSession(event).catch(() => null);
154
+ const available = session?.email
155
+ ? await isWorkspaceSsoEnabledForSession(session)
156
+ : await canAttemptWorkspaceSso();
157
+ return jsonResponse({ available }, 200);
158
+ },
159
+ );
160
+
121
161
  const authorizeHandler = defineEventHandler(
122
162
  async (event: H3Event): Promise<Response> => {
123
163
  const method = getMethod(event);
@@ -134,11 +174,15 @@ const authorizeHandler = defineEventHandler(
134
174
  }
135
175
 
136
176
  const redirectUri = search.get("redirect_uri");
177
+ const appId = search.get("app");
137
178
  const state = search.get("state");
179
+ const isDesktopRequest = isDesktopWorkspaceSsoRequest(
180
+ getHeader(event, "user-agent"),
181
+ );
138
182
 
139
183
  // ---- Control 1: redirect_uri allowlist (BEFORE any session work) ----
140
184
  // An attacker-supplied redirect_uri must never reach the mint path.
141
- if (!isAllowedRedirectUri(redirectUri)) {
185
+ if (!isAllowedIdentityRedirect(appId, redirectUri)) {
142
186
  return jsonResponse(
143
187
  {
144
188
  error: "invalid_redirect_uri",
@@ -150,9 +194,13 @@ const authorizeHandler = defineEventHandler(
150
194
  400,
151
195
  );
152
196
  }
153
- // Narrowed to string by isAllowedRedirectUri.
197
+ // Narrowed to string by isAllowedIdentityRedirect.
154
198
  const safeRedirectUri = redirectUri as string;
155
199
 
200
+ if (isDesktopRequest && !(await canAttemptWorkspaceSso())) {
201
+ return jsonResponse({ error: "not_found" }, 404);
202
+ }
203
+
156
204
  // ---- Resolve the EXISTING Dispatch session --------------------------
157
205
  const session = await getSession(event).catch(() => null);
158
206
 
@@ -182,6 +230,10 @@ const authorizeHandler = defineEventHandler(
182
230
  return redirect(signInHref);
183
231
  }
184
232
 
233
+ if (isDesktopRequest && !(await isWorkspaceSsoEnabledForSession(session))) {
234
+ return jsonResponse({ error: "not_found" }, 404);
235
+ }
236
+
185
237
  // ---- Mint the short-lived identity token ----------------------------
186
238
  if (!process.env.A2A_SECRET) {
187
239
  // Without a shared secret, no first-party app could verify the token.
@@ -238,13 +290,10 @@ const authorizeHandler = defineEventHandler(
238
290
  );
239
291
 
240
292
  /**
241
- * Dispatch identity-SSO plugin. Mounts the authorize route and registers
242
- * its exact path as a public path so the core auth guard does not 401 the
243
- * logged-out bounce. The `createAuthPlugin({ publicPaths })` call is
244
- * additive — it appends to the live guard config without disturbing the
245
- * primary Dispatch auth plugin's googleOnly/marketing/onboarding config.
293
+ * Dispatch identity-SSO plugin. The primary Dispatch auth plugin owns the
294
+ * exact public-path registration; this plugin owns only these handlers.
246
295
  */
247
296
  export default async (nitroApp: any) => {
297
+ getH3App(nitroApp).use(AVAILABILITY_PATH, availabilityHandler);
248
298
  getH3App(nitroApp).use(AUTHORIZE_PATH, authorizeHandler);
249
- return createAuthPlugin({ publicPaths: [AUTHORIZE_PATH] })(nitroApp);
250
299
  };
@@ -37,9 +37,9 @@
37
37
  * Auth-guard reachability: `/_agent-native/*` is 401'd by the core auth
38
38
  * guard when there is no session. This route is authenticated by the A2A
39
39
  * JWT, not a cookie, so — exactly like the identity-sso plugin's authorize
40
- * route — it registers its exact path as a `publicPath` via a second
41
- * additive `createAuthPlugin({ publicPaths })` call. "Public path" only
42
- * means "the guard does not pre-empt with a 401"; the handler still performs
40
+ * route — Dispatch's primary auth plugin receives its exact `publicPath`
41
+ * from `setupDispatch`. "Public path" only means "the guard does not pre-empt
42
+ * with a 401"; the handler still performs
43
43
  * the full A2A JWT + same-org check itself (defense in depth). The path is
44
44
  * matched exactly, so no other `/_agent-native/org/*` route is affected.
45
45
  *
@@ -51,11 +51,7 @@ import {
51
51
  getOrgDomain,
52
52
  resolveOrgByDomain,
53
53
  } from "@agent-native/core/org";
54
- import {
55
- createAuthPlugin,
56
- getH3App,
57
- runWithRequestContext,
58
- } from "@agent-native/core/server";
54
+ import { getH3App, runWithRequestContext } from "@agent-native/core/server";
59
55
  import { discoverAgents } from "@agent-native/core/server/agent-discovery";
60
56
  import { defineEventHandler, getMethod, getRequestHeader } from "h3";
61
57
  import type { H3Event } from "h3";
@@ -181,14 +177,10 @@ const orgAppsHandler = defineEventHandler(
181
177
  );
182
178
 
183
179
  /**
184
- * Dispatch org-app-directory plugin. Mounts the directory route and
185
- * registers its exact path as a public path so the core auth guard does not
186
- * 401 the A2A peer call before our own JWT + same-org check runs. The
187
- * `createAuthPlugin({ publicPaths })` call is additive — it appends to the
188
- * live guard config without disturbing Dispatch's primary auth plugin
189
- * (same mechanism the identity-sso plugin relies on).
180
+ * Dispatch org-app-directory plugin. The primary Dispatch auth plugin owns
181
+ * the exact public-path registration so this handler can perform its own JWT
182
+ * and same-org checks without racing a second auth initializer.
190
183
  */
191
184
  export default async (nitroApp: any) => {
192
185
  getH3App(nitroApp).use(ORG_APPS_PATH, orgAppsHandler);
193
- return createAuthPlugin({ publicPaths: [ORG_APPS_PATH] })(nitroApp);
194
186
  };
@@ -1,3 +1,11 @@
1
1
  import { setupDispatch } from "@agent-native/dispatch/server";
2
2
 
3
- export default setupDispatch();
3
+ export default setupDispatch({
4
+ auth: {
5
+ publicPaths: [
6
+ "/_agent-native/identity/availability",
7
+ "/_agent-native/identity/authorize",
8
+ "/_agent-native/org/apps",
9
+ ],
10
+ },
11
+ });
@@ -0,0 +1,15 @@
1
+ import {
2
+ defineFeatureFlag,
3
+ defineFeatureFlags,
4
+ } from "@agent-native/core/feature-flags";
5
+
6
+ export const DESKTOP_WORKSPACE_SSO_FLAG = defineFeatureFlag({
7
+ key: "desktop.workspace-sso",
8
+ displayName: "Desktop workspace sign-in",
9
+ description:
10
+ "Let the signed Agent Native Desktop broker workspace identity across first-party apps.",
11
+ });
12
+
13
+ export const DISPATCH_FEATURE_FLAGS = defineFeatureFlags([
14
+ DESKTOP_WORKSPACE_SSO_FLAG,
15
+ ]);
@@ -49,6 +49,34 @@ sendToAgentChat({
49
49
  });
50
50
  ```
51
51
 
52
+ ### Keep user text separate from injected context
53
+
54
+ Treat the visible `message` as the user's request or the shortest clear
55
+ description of an app-initiated operation. Put context the user did not type —
56
+ IDs, URLs, filenames, current selection or screen state, serialized records,
57
+ upload instructions, and bounded source excerpts — in the `context` field.
58
+ `context` is model input carried through the agent turn; it is stripped from
59
+ the rendered user message, so do not concatenate it into `message` with labels,
60
+ blank-line sections, or `<context>` tags yourself.
61
+
62
+ Use the related surface for each kind of supporting input:
63
+
64
+ | Surface | Use for |
65
+ | --- | --- |
66
+ | `message` | User-authored intent or a concise app action description |
67
+ | `context` | Derived metadata and bounded text needed to carry out that intent |
68
+ | `setAgentChatContextItem` | Context staged for a later user-submitted prompt; keep it keyed so updates replace stale context |
69
+ | `images`, `referenceImagePaths`, attachments | Binary or visual inputs; describe only the handling instructions in `context` |
70
+
71
+ If an app builds a prompt from a form, selection, upload, or editor state, keep
72
+ the visible message short and pass the assembled details as `context`. Preserve
73
+ the user's actual freeform text in `message` when it is the request; do not
74
+ restate it as metadata in `context` unless the agent needs a structured copy.
75
+
76
+ This boundary applies to auto-submitted turns and prefills. A hidden context
77
+ field is not a license to send unbounded records or secrets: cap excerpts,
78
+ prefer stable IDs and URLs, and use an action or resource lookup for full data.
79
+
52
80
  **From the UI, in the background:**
53
81
 
54
82
  ```ts
@@ -49,6 +49,34 @@ sendToAgentChat({
49
49
  });
50
50
  ```
51
51
 
52
+ ### Keep user text separate from injected context
53
+
54
+ Treat the visible `message` as the user's request or the shortest clear
55
+ description of an app-initiated operation. Put context the user did not type —
56
+ IDs, URLs, filenames, current selection or screen state, serialized records,
57
+ upload instructions, and bounded source excerpts — in the `context` field.
58
+ `context` is model input carried through the agent turn; it is stripped from
59
+ the rendered user message, so do not concatenate it into `message` with labels,
60
+ blank-line sections, or `<context>` tags yourself.
61
+
62
+ Use the related surface for each kind of supporting input:
63
+
64
+ | Surface | Use for |
65
+ | --- | --- |
66
+ | `message` | User-authored intent or a concise app action description |
67
+ | `context` | Derived metadata and bounded text needed to carry out that intent |
68
+ | `setAgentChatContextItem` | Context staged for a later user-submitted prompt; keep it keyed so updates replace stale context |
69
+ | `images`, `referenceImagePaths`, attachments | Binary or visual inputs; describe only the handling instructions in `context` |
70
+
71
+ If an app builds a prompt from a form, selection, upload, or editor state, keep
72
+ the visible message short and pass the assembled details as `context`. Preserve
73
+ the user's actual freeform text in `message` when it is the request; do not
74
+ restate it as metadata in `context` unless the agent needs a structured copy.
75
+
76
+ This boundary applies to auto-submitted turns and prefills. A hidden context
77
+ field is not a license to send unbounded records or secrets: cap excerpts,
78
+ prefer stable IDs and URLs, and use an action or resource lookup for full data.
79
+
52
80
  **From the UI, in the background:**
53
81
 
54
82
  ```ts
@@ -80,13 +80,14 @@ Each response is stored in the `responses` SQL table:
80
80
  `submitterEmail` may come from the logged-in Forms session or from trusted
81
81
  feedback clients that pass the logged-in user email as submission metadata.
82
82
 
83
- `pageUrl` and `clientSurface` are hidden pass-through fields: trusted embeds
84
- (e.g. the framework FeedbackButton) forward the URL of the page the respondent
85
- was on and the runtime shell they were in (`web`, `electron`, or `tauri`) as
86
- submission metadata, so owners can see which screen and which app feedback came
87
- from. Both are null for direct fills that send no context, and `clientSurface`
88
- is allowlisted server-side (unknown values are dropped). The responses table
89
- surfaces them as "Page" and "Source" columns when any response carries them.
83
+ `pageUrl` and `clientSurface` are hidden pass-through fields. Direct public
84
+ fills record the current form URL, while trusted embeds (e.g. the framework
85
+ FeedbackButton) forward the respondent's source page and runtime shell (`web`,
86
+ `electron`, or `tauri`) so owners can see which screen and app feedback came
87
+ from. Sensitive URL query keys are scrubbed before persistence, `clientSurface`
88
+ is allowlisted server-side (unknown values are dropped), and anonymous forms
89
+ suppress both fields. The responses table surfaces them as "Page" and "Source"
90
+ columns when any response carries them.
90
91
 
91
92
  The `data` JSON maps field IDs to values:
92
93
 
@@ -4,6 +4,7 @@ import {
4
4
  useActionMutation,
5
5
  } from "@agent-native/core/client/hooks";
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
+ import { scrubPageUrl } from "@shared/page-url";
7
8
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8
9
  import { toast } from "sonner";
9
10
 
@@ -189,7 +190,13 @@ export function useSubmitForm() {
189
190
  fetch(appApiPath(`/api/submit/${formId}`), {
190
191
  method: "POST",
191
192
  headers: { "Content-Type": "application/json" },
192
- body: JSON.stringify({ data, captchaToken, _hp, _t }),
193
+ body: JSON.stringify({
194
+ data,
195
+ captchaToken,
196
+ _hp,
197
+ _t,
198
+ _meta: { pageUrl: scrubPageUrl(window.location.href) },
199
+ }),
193
200
  }).then((r) => {
194
201
  if (!r.ok) return r.json().then((e: any) => Promise.reject(e));
195
202
  return r.json();
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Fixed public forms so conditional questions stay hidden until relevant and submissions retain a safe page link.
@@ -21,6 +21,7 @@ import {
21
21
  isConditionalFieldVisible,
22
22
  sanitizeConditionalValues as sanitizeVisibleValues,
23
23
  } from "../../shared/conditional.js";
24
+ import { scrubPageUrl } from "../../shared/page-url.js";
24
25
  import {
25
26
  cleanSubmitterEmail,
26
27
  publicSubmitterEmail,
@@ -226,7 +227,7 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
226
227
  meta?.chatSessionIds,
227
228
  ]);
228
229
  const activeRunId = cleanMetaText(meta?.activeRunId);
229
- const pageUrl = cleanMetaText(meta?.pageUrl);
230
+ const pageUrl = scrubPageUrl(meta?.pageUrl);
230
231
  const clientSurface = cleanClientSurface(meta?.clientSurface);
231
232
 
232
233
  await db.insert(schema.responses).values({
@@ -10,6 +10,8 @@ import {
10
10
  import { eq } from "drizzle-orm";
11
11
  import { getMethod, getRequestURL, type H3Event } from "h3";
12
12
 
13
+ import { isConditionalFieldVisible } from "../../shared/conditional.js";
14
+ import { SENSITIVE_QUERY_PARAMS } from "../../shared/page-url.js";
13
15
  import {
14
16
  toPublicFormSettings,
15
17
  type FormField,
@@ -200,6 +202,10 @@ function renderField(field: FormField): string {
200
202
  ? ` data-cond-field="${escapeHtml(field.conditional.fieldId)}" data-cond-op="${escapeHtml(field.conditional.operator)}" data-cond-val="${escapeHtml(field.conditional.value)}"`
201
203
  : "";
202
204
  const widthClass = field.width === "half" ? " field-half" : "";
205
+ const initiallyVisible = isConditionalFieldVisible(field, {});
206
+ const initialVisibility = initiallyVisible
207
+ ? ""
208
+ : ' style="display:none" data-hidden="1"';
203
209
 
204
210
  let input = "";
205
211
 
@@ -265,7 +271,7 @@ function renderField(field: FormField): string {
265
271
  break;
266
272
  }
267
273
 
268
- return `<div class="field${widthClass}" data-field-id="${id}"${cond}>
274
+ return `<div class="field${widthClass}" data-field-id="${id}"${cond}${initialVisibility}>
269
275
  <label class="field-label">${escapeHtml(field.label)}${field.required ? '<span class="req">*</span>' : ""}</label>
270
276
  ${desc}${input}</div>`;
271
277
  }
@@ -435,6 +441,34 @@ function renderFormPage(
435
441
  var REDIRECT = ${JSON.stringify(safeRedirectUrl(settings.redirectUrl))};
436
442
  var TURNSTILE_KEY = ${JSON.stringify(turnstileSiteKey)};
437
443
  var FIELDS = ${JSON.stringify(fields.map((f) => ({ id: f.id, type: f.type, required: f.required, validation: f.validation, label: f.label, conditional: f.conditional })))};
444
+ var SENSITIVE_QUERY_PARAMS = ${JSON.stringify(SENSITIVE_QUERY_PARAMS)};
445
+
446
+ function scrubPageUrl(value) {
447
+ try {
448
+ var url = new URL(value);
449
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
450
+ url.username = "";
451
+ url.password = "";
452
+ scrubParams(url.searchParams);
453
+ if (url.hash.indexOf("=") >= 0) {
454
+ var hashParams = new URLSearchParams(url.hash.slice(1));
455
+ scrubParams(hashParams);
456
+ url.hash = hashParams.toString();
457
+ }
458
+ return url.toString();
459
+ // coercion-ok: an invalid browser URL is absent page context, not a valid submission value
460
+ } catch (_) {
461
+ return null;
462
+ }
463
+ }
464
+
465
+ function scrubParams(params) {
466
+ Array.from(params.keys()).forEach(function(key) {
467
+ if (SENSITIVE_QUERY_PARAMS.indexOf(key.toLowerCase()) >= 0) {
468
+ params.set(key, "<redacted>");
469
+ }
470
+ });
471
+ }
438
472
 
439
473
  function revalidateFormVersion() {
440
474
  fetch(PUBLIC_FORM_API, { cache: "no-store" })
@@ -631,11 +665,12 @@ function renderFormPage(
631
665
  btn.textContent = "Submitting...";
632
666
  btn.disabled = true;
633
667
  var hp = (document.getElementById("_hp") || {}).value || "";
668
+ var pageUrl = scrubPageUrl(window.location.href);
634
669
 
635
670
  fetch(${JSON.stringify(submitPath)} + FORM_ID, {
636
671
  method: "POST",
637
672
  headers: { "Content-Type": "application/json" },
638
- body: JSON.stringify({ data: data, captchaToken: captchaToken, _hp: hp, _t: PAGE_LOAD_T }),
673
+ body: JSON.stringify({ data: data, captchaToken: captchaToken, _hp: hp, _t: PAGE_LOAD_T, _meta: { pageUrl: pageUrl } }),
639
674
  })
640
675
  .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
641
676
  .then(function(res) {