@agent-native/core 0.157.20 → 0.157.22

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 (36) hide show
  1. package/corpus/templates/analytics/.agents/skills/security/SKILL.md +29 -0
  2. package/corpus/templates/assets/.agents/skills/security/SKILL.md +29 -0
  3. package/corpus/templates/brain/.agents/skills/security/SKILL.md +29 -0
  4. package/corpus/templates/calendar/.agents/skills/security/SKILL.md +29 -0
  5. package/corpus/templates/chat/.agents/skills/security/SKILL.md +29 -0
  6. package/corpus/templates/clips/.agents/skills/security/SKILL.md +29 -0
  7. package/corpus/templates/content/.agents/skills/security/SKILL.md +29 -0
  8. package/corpus/templates/crm/.agents/skills/security/SKILL.md +29 -0
  9. package/corpus/templates/design/.agents/skills/security/SKILL.md +29 -0
  10. package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +29 -0
  11. package/corpus/templates/factory/.agents/skills/security/SKILL.md +29 -0
  12. package/corpus/templates/forms/.agents/skills/security/SKILL.md +29 -0
  13. package/corpus/templates/macros/.agents/skills/security/SKILL.md +29 -0
  14. package/corpus/templates/mail/.agents/skills/security/SKILL.md +29 -0
  15. package/corpus/templates/plan/.agents/skills/security/SKILL.md +29 -0
  16. package/corpus/templates/slides/.agents/skills/security/SKILL.md +29 -0
  17. package/corpus/templates/tasks/.agents/skills/security/SKILL.md +29 -0
  18. package/dist/collab/awareness.d.ts +2 -2
  19. package/dist/collab/struct-routes.d.ts +1 -1
  20. package/dist/observability/routes.d.ts +5 -5
  21. package/dist/progress/routes.d.ts +1 -1
  22. package/dist/secrets/routes.d.ts +9 -9
  23. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  24. package/dist/server/embed-route.js +62 -1
  25. package/dist/server/embed-session.d.ts +11 -0
  26. package/dist/server/embed-session.js +92 -6
  27. package/dist/server/realtime-token.d.ts +1 -1
  28. package/dist/templates/chat/.agents/skills/security/SKILL.md +29 -0
  29. package/dist/templates/default/.agents/skills/security/SKILL.md +29 -0
  30. package/dist/templates/headless/.agents/skills/security/SKILL.md +29 -0
  31. package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +29 -0
  32. package/package.json +1 -1
  33. package/src/templates/chat/.agents/skills/security/SKILL.md +29 -0
  34. package/src/templates/default/.agents/skills/security/SKILL.md +29 -0
  35. package/src/templates/headless/.agents/skills/security/SKILL.md +29 -0
  36. package/src/templates/workspace-core/.agents/skills/security/SKILL.md +29 -0
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ error?: undefined;
45
+ ok?: undefined;
44
46
  summary: import("./types.js").TraceSummary;
45
47
  spans: import("./types.js").TraceSpan[];
46
48
  id?: undefined;
49
+ } | {
47
50
  error?: undefined;
48
51
  ok?: undefined;
49
- } | {
50
52
  summary?: undefined;
51
53
  spans?: undefined;
52
54
  id: string;
53
- error?: undefined;
54
- ok?: undefined;
55
55
  } | {
56
+ ok?: undefined;
56
57
  summary?: undefined;
57
58
  spans?: undefined;
58
59
  id?: undefined;
59
60
  error: any;
60
- ok?: undefined;
61
61
  } | {
62
+ error?: undefined;
62
63
  summary?: undefined;
63
64
  spans?: undefined;
64
65
  id?: undefined;
65
66
  ok: boolean;
66
- error?: undefined;
67
67
  }>>;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- status?: undefined;
38
37
  ok?: undefined;
38
+ status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ ok?: undefined;
57
58
  error: string;
58
59
  note?: undefined;
59
- ok?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -83,6 +83,53 @@ function textResponse(event, message, status) {
83
83
  }),
84
84
  });
85
85
  }
86
+ function embedTargetOrigin(event) {
87
+ const host = getHeader(event, "host")?.trim();
88
+ if (!host)
89
+ return null;
90
+ const forwardedProto = getHeader(event, "x-forwarded-proto")
91
+ ?.split(",", 1)[0]
92
+ ?.trim()
93
+ .toLowerCase();
94
+ const protocol = forwardedProto === "http" || forwardedProto === "https"
95
+ ? forwardedProto
96
+ : "https";
97
+ return `${protocol}://${host}`;
98
+ }
99
+ function embedTargetAppId(origin) {
100
+ if (origin) {
101
+ try {
102
+ const hostname = new URL(origin).hostname;
103
+ const suffix = ".agent-native.com";
104
+ if (hostname.endsWith(suffix) && hostname.length > suffix.length) {
105
+ return hostname.slice(0, -suffix.length);
106
+ }
107
+ // coercion-ok: diagnostic host parsing must never change the auth response.
108
+ }
109
+ catch {
110
+ // The host is diagnostic context only; keep the response path independent.
111
+ }
112
+ }
113
+ const configured = [process.env.AGENT_NATIVE_APP_ID, process.env.APP_ID].find((value) => /^[a-z0-9][a-z0-9._-]{0,63}$/i.test(value?.trim() ?? ""));
114
+ if (configured)
115
+ return configured.trim();
116
+ return "unknown";
117
+ }
118
+ function logEmbedConsumeResult(event, diagnostic, responseStatus) {
119
+ const targetOrigin = embedTargetOrigin(event);
120
+ console.info("[agent-native] workspace embed consume", {
121
+ targetAppId: embedTargetAppId(targetOrigin),
122
+ targetOrigin,
123
+ ticketKey: diagnostic.ticketKey,
124
+ outcome: diagnostic.outcome,
125
+ ticketRowFound: diagnostic.ticketRowFound,
126
+ consumed: diagnostic.consumed,
127
+ expired: diagnostic.expired,
128
+ expectedOrgKey: diagnostic.expectedOrgKey,
129
+ ticketOrgKey: diagnostic.ticketOrgKey,
130
+ responseStatus,
131
+ });
132
+ }
86
133
  function expiredEmbedSessionResponse(event) {
87
134
  setEmbedStartResponseHeaders(event);
88
135
  return new Response(`<!doctype html>
@@ -204,14 +251,24 @@ export function createEmbedStartRouteHandler(options = {}) {
204
251
  const existingSession = await options
205
252
  .getExistingSession?.(event)
206
253
  .catch(() => null);
254
+ let consumeDiagnostic = null;
207
255
  const consumed = await consumeEmbedSessionTicket(ticket, {
208
256
  expectedOrgId: existingSession?.orgId ?? null,
257
+ onResult: (diagnostic) => {
258
+ consumeDiagnostic = diagnostic;
259
+ },
209
260
  });
210
261
  if (!consumed) {
262
+ if (consumeDiagnostic) {
263
+ logEmbedConsumeResult(event, consumeDiagnostic, 401);
264
+ }
211
265
  return expiredEmbedSessionResponse(event);
212
266
  }
213
267
  const target = normalizeEmbedTargetPath(consumed.targetPath);
214
268
  if (!target) {
269
+ if (consumeDiagnostic) {
270
+ logEmbedConsumeResult(event, consumeDiagnostic, 400);
271
+ }
215
272
  return textResponse(event, "Invalid embed target.", 400);
216
273
  }
217
274
  const token = signEmbedSessionToken({
@@ -230,7 +287,11 @@ export function createEmbedStartRouteHandler(options = {}) {
230
287
  const chatBridgeActive = firstQueryValue(query[MCP_APP_CHAT_BRIDGE_QUERY_PARAM]) === "1" ||
231
288
  firstQueryValue(query[MCP_APP_CHAT_BRIDGE_QUERY_PARAM]) === "true";
232
289
  const location = withConfiguredBasePath(withCollapsedAgentSidebarParam(appendEmbedParams(target, token, chatBridgeActive)));
233
- if (wantsTransplantLocationResponse(event)) {
290
+ const transplant = wantsTransplantLocationResponse(event);
291
+ if (consumeDiagnostic) {
292
+ logEmbedConsumeResult(event, consumeDiagnostic, transplant ? 200 : 302);
293
+ }
294
+ if (transplant) {
234
295
  return transplantLocationResponse(event, location);
235
296
  }
236
297
  return redirectWithStagedCookies(event, location);
@@ -12,8 +12,19 @@ export interface EmbedSessionTicket {
12
12
  ticketHash: string;
13
13
  expiresAt: number;
14
14
  }
15
+ export type EmbedSessionTicketConsumeOutcome = "missing-ticket" | "not-found" | "already-consumed" | "expired" | "org-mismatch" | "consumption-race" | "invalid-row" | "consumed";
16
+ export interface EmbedSessionTicketConsumeDiagnostic {
17
+ outcome: EmbedSessionTicketConsumeOutcome;
18
+ ticketKey: string | null;
19
+ ticketRowFound: boolean;
20
+ consumed: boolean;
21
+ expired: boolean;
22
+ expectedOrgKey: string | null;
23
+ ticketOrgKey: string | null;
24
+ }
15
25
  export interface ConsumeEmbedSessionTicketOptions {
16
26
  expectedOrgId?: string | null;
27
+ onResult?: (result: EmbedSessionTicketConsumeDiagnostic) => void;
17
28
  }
18
29
  export interface ConsumedEmbedSessionTicket {
19
30
  ownerEmail: string;
@@ -133,6 +133,11 @@ function signPayload(payload) {
133
133
  function hashTicket(ticket) {
134
134
  return crypto.createHash("sha256").update(ticket).digest("hex");
135
135
  }
136
+ function redactedIdentifier(value) {
137
+ if (!value)
138
+ return null;
139
+ return crypto.createHash("sha256").update(value).digest("hex").slice(0, 12);
140
+ }
136
141
  function numberOrNull(value) {
137
142
  if (value == null)
138
143
  return null;
@@ -458,27 +463,79 @@ export async function createEmbedSessionTicket(input) {
458
463
  return { ticket, ticketHash, expiresAt };
459
464
  }
460
465
  export async function consumeEmbedSessionTicket(ticket, options = {}) {
461
- if (!ticket)
466
+ const expectedOrgKey = redactedIdentifier(options.expectedOrgId);
467
+ if (!ticket) {
468
+ options.onResult?.({
469
+ outcome: "missing-ticket",
470
+ ticketKey: null,
471
+ ticketRowFound: false,
472
+ consumed: false,
473
+ expired: false,
474
+ expectedOrgKey,
475
+ ticketOrgKey: null,
476
+ });
462
477
  return null;
478
+ }
463
479
  await ensureTable();
464
480
  const ticketHash = hashTicket(ticket);
481
+ const ticketKey = ticketHash.slice(0, 12);
465
482
  const now = Date.now();
466
483
  const { rows } = await getDbExec().execute({
467
484
  sql: "SELECT ticket_hash, owner_email, org_id, target_path, scope, expires_at, consumed_at " +
468
485
  "FROM agent_native_embed_tickets WHERE ticket_hash = ?",
469
486
  args: [ticketHash],
470
487
  });
471
- if (rows.length === 0)
488
+ if (rows.length === 0) {
489
+ options.onResult?.({
490
+ outcome: "not-found",
491
+ ticketKey,
492
+ ticketRowFound: false,
493
+ consumed: false,
494
+ expired: false,
495
+ expectedOrgKey,
496
+ ticketOrgKey: null,
497
+ });
472
498
  return null;
499
+ }
473
500
  const row = rows[0];
474
501
  const expiresAt = numberOrNull(row.expires_at ?? row.expiresAt);
475
502
  const consumedAt = numberOrNull(row.consumed_at ?? row.consumedAt);
476
503
  const orgId = stringOrUndefined(row.org_id ?? row.orgId);
477
- if (consumedAt != null)
504
+ const ticketOrgKey = redactedIdentifier(orgId);
505
+ if (consumedAt != null) {
506
+ options.onResult?.({
507
+ outcome: "already-consumed",
508
+ ticketKey,
509
+ ticketRowFound: true,
510
+ consumed: true,
511
+ expired: false,
512
+ expectedOrgKey,
513
+ ticketOrgKey,
514
+ });
478
515
  return null;
479
- if (expiresAt != null && expiresAt < now)
516
+ }
517
+ if (expiresAt != null && expiresAt < now) {
518
+ options.onResult?.({
519
+ outcome: "expired",
520
+ ticketKey,
521
+ ticketRowFound: true,
522
+ consumed: false,
523
+ expired: true,
524
+ expectedOrgKey,
525
+ ticketOrgKey,
526
+ });
480
527
  return null;
528
+ }
481
529
  if (options.expectedOrgId && orgId && orgId !== options.expectedOrgId) {
530
+ options.onResult?.({
531
+ outcome: "org-mismatch",
532
+ ticketKey,
533
+ ticketRowFound: true,
534
+ consumed: false,
535
+ expired: false,
536
+ expectedOrgKey,
537
+ ticketOrgKey,
538
+ });
482
539
  return null;
483
540
  }
484
541
  const result = await getDbExec().execute({
@@ -486,12 +543,41 @@ export async function consumeEmbedSessionTicket(ticket, options = {}) {
486
543
  "WHERE ticket_hash = ? AND consumed_at IS NULL",
487
544
  args: [now, ticketHash],
488
545
  });
489
- if (result.rowsAffected === 0)
546
+ if (result.rowsAffected === 0) {
547
+ options.onResult?.({
548
+ outcome: "consumption-race",
549
+ ticketKey,
550
+ ticketRowFound: true,
551
+ consumed: false,
552
+ expired: false,
553
+ expectedOrgKey,
554
+ ticketOrgKey,
555
+ });
490
556
  return null;
557
+ }
491
558
  const targetPath = normalizeEmbedTargetPath(stringOrUndefined(row.target_path ?? row.targetPath));
492
559
  const ownerEmail = stringOrUndefined(row.owner_email ?? row.ownerEmail);
493
- if (!targetPath || !ownerEmail || expiresAt == null)
560
+ if (!targetPath || !ownerEmail || expiresAt == null) {
561
+ options.onResult?.({
562
+ outcome: "invalid-row",
563
+ ticketKey,
564
+ ticketRowFound: true,
565
+ consumed: true,
566
+ expired: false,
567
+ expectedOrgKey,
568
+ ticketOrgKey,
569
+ });
494
570
  return null;
571
+ }
572
+ options.onResult?.({
573
+ outcome: "consumed",
574
+ ticketKey,
575
+ ticketRowFound: true,
576
+ consumed: true,
577
+ expired: false,
578
+ expectedOrgKey,
579
+ ticketOrgKey,
580
+ });
495
581
  return {
496
582
  ownerEmail,
497
583
  ...(orgId ? { orgId } : {}),
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
@@ -149,6 +149,35 @@ export default defineEventHandler(async (event) => {
149
149
 
150
150
  - Never create unprotected routes that modify data.
151
151
 
152
+ ## Same-Origin Workspace Apps
153
+
154
+ Path-mounted workspace apps share the Dispatch gateway origin. Because the
155
+ framework's session cookie is scoped to `/`, a mounted pane receives the
156
+ ambient Dispatch session and can act as the signed-in user through same-origin
157
+ requests. This is an intentional trusted-code model, not an isolation
158
+ boundary; removing a mounted app from SSO fanout does not revoke that ambient
159
+ access.
160
+
161
+ Only trusted, workspace-owner-authored code belongs on that origin. Treat each
162
+ of these as a trust-boundary change that requires an explicit origin, sandbox,
163
+ or capability design before shipping:
164
+
165
+ - non-owners can create or edit mounted app code;
166
+ - mounted apps render or execute untrusted external content or remote code;
167
+ - apps are publicly shareable, anonymously reachable, or installable by users
168
+ outside the owning workspace;
169
+ - app source, dependencies, or deployment artifacts can be replaced by a
170
+ third party without the workspace owner's authorization;
171
+ - a mounted app can be registered across workspaces or can change its mount
172
+ path/origin without an ownership check; or
173
+ - cookie scope, proxying, iframe policy, or navigation changes make this
174
+ ambient-session behavior broader than the owning workspace.
175
+
176
+ Do not describe canonical-only SSO eligibility as origin isolation. Canonical
177
+ apps and explicitly registered custom origins remain eligible; path-mounted
178
+ apps are deliberately excluded as SSO targets while retaining their existing
179
+ same-origin session behavior.
180
+
152
181
  **Exception — the SSR HTML/`.data` catch-all is deliberately session-blind.**
153
182
  The rule above is for routes that read or mutate user data. The SSR page
154
183
  render and React Router `.data` route are different: they serve one
@@ -149,6 +149,35 @@ export default defineEventHandler(async (event) => {
149
149
 
150
150
  - Never create unprotected routes that modify data.
151
151
 
152
+ ## Same-Origin Workspace Apps
153
+
154
+ Path-mounted workspace apps share the Dispatch gateway origin. Because the
155
+ framework's session cookie is scoped to `/`, a mounted pane receives the
156
+ ambient Dispatch session and can act as the signed-in user through same-origin
157
+ requests. This is an intentional trusted-code model, not an isolation
158
+ boundary; removing a mounted app from SSO fanout does not revoke that ambient
159
+ access.
160
+
161
+ Only trusted, workspace-owner-authored code belongs on that origin. Treat each
162
+ of these as a trust-boundary change that requires an explicit origin, sandbox,
163
+ or capability design before shipping:
164
+
165
+ - non-owners can create or edit mounted app code;
166
+ - mounted apps render or execute untrusted external content or remote code;
167
+ - apps are publicly shareable, anonymously reachable, or installable by users
168
+ outside the owning workspace;
169
+ - app source, dependencies, or deployment artifacts can be replaced by a
170
+ third party without the workspace owner's authorization;
171
+ - a mounted app can be registered across workspaces or can change its mount
172
+ path/origin without an ownership check; or
173
+ - cookie scope, proxying, iframe policy, or navigation changes make this
174
+ ambient-session behavior broader than the owning workspace.
175
+
176
+ Do not describe canonical-only SSO eligibility as origin isolation. Canonical
177
+ apps and explicitly registered custom origins remain eligible; path-mounted
178
+ apps are deliberately excluded as SSO targets while retaining their existing
179
+ same-origin session behavior.
180
+
152
181
  **Exception — the SSR HTML/`.data` catch-all is deliberately session-blind.**
153
182
  The rule above is for routes that read or mutate user data. The SSR page
154
183
  render and React Router `.data` route are different: they serve one
@@ -149,6 +149,35 @@ export default defineEventHandler(async (event) => {
149
149
 
150
150
  - Never create unprotected routes that modify data.
151
151
 
152
+ ## Same-Origin Workspace Apps
153
+
154
+ Path-mounted workspace apps share the Dispatch gateway origin. Because the
155
+ framework's session cookie is scoped to `/`, a mounted pane receives the
156
+ ambient Dispatch session and can act as the signed-in user through same-origin
157
+ requests. This is an intentional trusted-code model, not an isolation
158
+ boundary; removing a mounted app from SSO fanout does not revoke that ambient
159
+ access.
160
+
161
+ Only trusted, workspace-owner-authored code belongs on that origin. Treat each
162
+ of these as a trust-boundary change that requires an explicit origin, sandbox,
163
+ or capability design before shipping:
164
+
165
+ - non-owners can create or edit mounted app code;
166
+ - mounted apps render or execute untrusted external content or remote code;
167
+ - apps are publicly shareable, anonymously reachable, or installable by users
168
+ outside the owning workspace;
169
+ - app source, dependencies, or deployment artifacts can be replaced by a
170
+ third party without the workspace owner's authorization;
171
+ - a mounted app can be registered across workspaces or can change its mount
172
+ path/origin without an ownership check; or
173
+ - cookie scope, proxying, iframe policy, or navigation changes make this
174
+ ambient-session behavior broader than the owning workspace.
175
+
176
+ Do not describe canonical-only SSO eligibility as origin isolation. Canonical
177
+ apps and explicitly registered custom origins remain eligible; path-mounted
178
+ apps are deliberately excluded as SSO targets while retaining their existing
179
+ same-origin session behavior.
180
+
152
181
  **Exception — the SSR HTML/`.data` catch-all is deliberately session-blind.**
153
182
  The rule above is for routes that read or mutate user data. The SSR page
154
183
  render and React Router `.data` route are different: they serve one
@@ -149,6 +149,35 @@ export default defineEventHandler(async (event) => {
149
149
 
150
150
  - Never create unprotected routes that modify data.
151
151
 
152
+ ## Same-Origin Workspace Apps
153
+
154
+ Path-mounted workspace apps share the Dispatch gateway origin. Because the
155
+ framework's session cookie is scoped to `/`, a mounted pane receives the
156
+ ambient Dispatch session and can act as the signed-in user through same-origin
157
+ requests. This is an intentional trusted-code model, not an isolation
158
+ boundary; removing a mounted app from SSO fanout does not revoke that ambient
159
+ access.
160
+
161
+ Only trusted, workspace-owner-authored code belongs on that origin. Treat each
162
+ of these as a trust-boundary change that requires an explicit origin, sandbox,
163
+ or capability design before shipping:
164
+
165
+ - non-owners can create or edit mounted app code;
166
+ - mounted apps render or execute untrusted external content or remote code;
167
+ - apps are publicly shareable, anonymously reachable, or installable by users
168
+ outside the owning workspace;
169
+ - app source, dependencies, or deployment artifacts can be replaced by a
170
+ third party without the workspace owner's authorization;
171
+ - a mounted app can be registered across workspaces or can change its mount
172
+ path/origin without an ownership check; or
173
+ - cookie scope, proxying, iframe policy, or navigation changes make this
174
+ ambient-session behavior broader than the owning workspace.
175
+
176
+ Do not describe canonical-only SSO eligibility as origin isolation. Canonical
177
+ apps and explicitly registered custom origins remain eligible; path-mounted
178
+ apps are deliberately excluded as SSO targets while retaining their existing
179
+ same-origin session behavior.
180
+
152
181
  **Exception — the SSR HTML/`.data` catch-all is deliberately session-blind.**
153
182
  The rule above is for routes that read or mutate user data. The SSR page
154
183
  render and React Router `.data` route are different: they serve one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.157.20",
3
+ "version": "0.157.22",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -149,6 +149,35 @@ export default defineEventHandler(async (event) => {
149
149
 
150
150
  - Never create unprotected routes that modify data.
151
151
 
152
+ ## Same-Origin Workspace Apps
153
+
154
+ Path-mounted workspace apps share the Dispatch gateway origin. Because the
155
+ framework's session cookie is scoped to `/`, a mounted pane receives the
156
+ ambient Dispatch session and can act as the signed-in user through same-origin
157
+ requests. This is an intentional trusted-code model, not an isolation
158
+ boundary; removing a mounted app from SSO fanout does not revoke that ambient
159
+ access.
160
+
161
+ Only trusted, workspace-owner-authored code belongs on that origin. Treat each
162
+ of these as a trust-boundary change that requires an explicit origin, sandbox,
163
+ or capability design before shipping:
164
+
165
+ - non-owners can create or edit mounted app code;
166
+ - mounted apps render or execute untrusted external content or remote code;
167
+ - apps are publicly shareable, anonymously reachable, or installable by users
168
+ outside the owning workspace;
169
+ - app source, dependencies, or deployment artifacts can be replaced by a
170
+ third party without the workspace owner's authorization;
171
+ - a mounted app can be registered across workspaces or can change its mount
172
+ path/origin without an ownership check; or
173
+ - cookie scope, proxying, iframe policy, or navigation changes make this
174
+ ambient-session behavior broader than the owning workspace.
175
+
176
+ Do not describe canonical-only SSO eligibility as origin isolation. Canonical
177
+ apps and explicitly registered custom origins remain eligible; path-mounted
178
+ apps are deliberately excluded as SSO targets while retaining their existing
179
+ same-origin session behavior.
180
+
152
181
  **Exception — the SSR HTML/`.data` catch-all is deliberately session-blind.**
153
182
  The rule above is for routes that read or mutate user data. The SSR page
154
183
  render and React Router `.data` route are different: they serve one
@@ -149,6 +149,35 @@ export default defineEventHandler(async (event) => {
149
149
 
150
150
  - Never create unprotected routes that modify data.
151
151
 
152
+ ## Same-Origin Workspace Apps
153
+
154
+ Path-mounted workspace apps share the Dispatch gateway origin. Because the
155
+ framework's session cookie is scoped to `/`, a mounted pane receives the
156
+ ambient Dispatch session and can act as the signed-in user through same-origin
157
+ requests. This is an intentional trusted-code model, not an isolation
158
+ boundary; removing a mounted app from SSO fanout does not revoke that ambient
159
+ access.
160
+
161
+ Only trusted, workspace-owner-authored code belongs on that origin. Treat each
162
+ of these as a trust-boundary change that requires an explicit origin, sandbox,
163
+ or capability design before shipping:
164
+
165
+ - non-owners can create or edit mounted app code;
166
+ - mounted apps render or execute untrusted external content or remote code;
167
+ - apps are publicly shareable, anonymously reachable, or installable by users
168
+ outside the owning workspace;
169
+ - app source, dependencies, or deployment artifacts can be replaced by a
170
+ third party without the workspace owner's authorization;
171
+ - a mounted app can be registered across workspaces or can change its mount
172
+ path/origin without an ownership check; or
173
+ - cookie scope, proxying, iframe policy, or navigation changes make this
174
+ ambient-session behavior broader than the owning workspace.
175
+
176
+ Do not describe canonical-only SSO eligibility as origin isolation. Canonical
177
+ apps and explicitly registered custom origins remain eligible; path-mounted
178
+ apps are deliberately excluded as SSO targets while retaining their existing
179
+ same-origin session behavior.
180
+
152
181
  **Exception — the SSR HTML/`.data` catch-all is deliberately session-blind.**
153
182
  The rule above is for routes that read or mutate user data. The SSR page
154
183
  render and React Router `.data` route are different: they serve one
@@ -149,6 +149,35 @@ export default defineEventHandler(async (event) => {
149
149
 
150
150
  - Never create unprotected routes that modify data.
151
151
 
152
+ ## Same-Origin Workspace Apps
153
+
154
+ Path-mounted workspace apps share the Dispatch gateway origin. Because the
155
+ framework's session cookie is scoped to `/`, a mounted pane receives the
156
+ ambient Dispatch session and can act as the signed-in user through same-origin
157
+ requests. This is an intentional trusted-code model, not an isolation
158
+ boundary; removing a mounted app from SSO fanout does not revoke that ambient
159
+ access.
160
+
161
+ Only trusted, workspace-owner-authored code belongs on that origin. Treat each
162
+ of these as a trust-boundary change that requires an explicit origin, sandbox,
163
+ or capability design before shipping:
164
+
165
+ - non-owners can create or edit mounted app code;
166
+ - mounted apps render or execute untrusted external content or remote code;
167
+ - apps are publicly shareable, anonymously reachable, or installable by users
168
+ outside the owning workspace;
169
+ - app source, dependencies, or deployment artifacts can be replaced by a
170
+ third party without the workspace owner's authorization;
171
+ - a mounted app can be registered across workspaces or can change its mount
172
+ path/origin without an ownership check; or
173
+ - cookie scope, proxying, iframe policy, or navigation changes make this
174
+ ambient-session behavior broader than the owning workspace.
175
+
176
+ Do not describe canonical-only SSO eligibility as origin isolation. Canonical
177
+ apps and explicitly registered custom origins remain eligible; path-mounted
178
+ apps are deliberately excluded as SSO targets while retaining their existing
179
+ same-origin session behavior.
180
+
152
181
  **Exception — the SSR HTML/`.data` catch-all is deliberately session-blind.**
153
182
  The rule above is for routes that read or mutate user data. The SSR page
154
183
  render and React Router `.data` route are different: they serve one