@agent-native/core 0.154.0 → 0.154.2

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 (54) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/changelog/2026-08-12-feature-flags-can-be-managed-across-apps-whose-workspace-rec.md +6 -0
  3. package/corpus/templates/analytics/changelog/2026-08-12-kept-feature-flag-details-readable-by-stacking-rollout-contr.md +6 -0
  4. package/corpus/templates/analytics/scripts/preview-dashboard-report.ts +16 -5
  5. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +98 -31
  6. package/corpus/templates/content/actions/resolve-content-landing.ts +162 -32
  7. package/corpus/templates/content/shared/content-landing.ts +5 -0
  8. package/corpus/templates/dispatch/changelog/2026-08-12-dispatch-now-appears-in-authorized-workspace-app-directories.md +6 -0
  9. package/corpus/templates/dispatch/server/lib/org-apps-directory.ts +3 -4
  10. package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +15 -3
  11. package/corpus/templates/slides/actions/patch-deck.ts +12 -3
  12. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +73 -1
  13. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +6 -1
  14. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -0
  15. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +15 -0
  16. package/corpus/templates/slides/app/context/DeckContext.tsx +19 -31
  17. package/corpus/templates/slides/app/hooks/use-navigation-state.ts +15 -16
  18. package/corpus/templates/slides/app/pages/DeckEditor.tsx +9 -0
  19. package/corpus/templates/slides/changelog/2026-08-13-clicking-a-slide-thumbnail-now-focuses-it-so-the-slide-shortc.md +6 -0
  20. package/corpus/templates/slides/changelog/2026-08-13-duplicating-or-restoring-a-slide-now-keeps-its-transition-ani.md +6 -0
  21. package/corpus/templates/slides/changelog/2026-08-13-opening-presentation-mode-from-the-agent-now-starts-on-the-re.md +6 -0
  22. package/corpus/templates/slides/changelog/2026-08-13-slide-transitions-can-be-set-from-a-visible-control-in-the-sl.md +6 -0
  23. package/dist/a2a/audience.d.ts +2 -0
  24. package/dist/a2a/audience.js +23 -0
  25. package/dist/a2a/client.d.ts +4 -0
  26. package/dist/a2a/client.js +32 -14
  27. package/dist/a2a/server.d.ts +9 -4
  28. package/dist/a2a/server.js +52 -25
  29. package/dist/a2a/types.d.ts +4 -4
  30. package/dist/a2a-claims.d.ts +1 -0
  31. package/dist/a2a-claims.js +3 -1
  32. package/dist/client/feature-flags/FeatureFlagsPanel.js +2 -2
  33. package/dist/collab/awareness.d.ts +2 -2
  34. package/dist/collab/routes.d.ts +1 -1
  35. package/dist/feature-flags/a2a-action-route.js +10 -1
  36. package/dist/feature-flags/actions/set-feature-flag.d.ts +2 -1
  37. package/dist/feature-flags/actions/set-feature-flag.js +9 -2
  38. package/dist/mcp/org-directory.d.ts +3 -0
  39. package/dist/mcp/org-directory.js +15 -5
  40. package/dist/notifications/routes.d.ts +1 -1
  41. package/dist/observability/routes.d.ts +6 -6
  42. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  43. package/dist/resources/handlers.d.ts +1 -1
  44. package/dist/secrets/routes.d.ts +3 -3
  45. package/dist/server/action-routes.js +10 -2
  46. package/dist/server/agent-capabilities.js +0 -0
  47. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -13
  48. package/dist/server/agent-chat/action-filters-a2a.js +59 -23
  49. package/dist/server/agent-discovery.d.ts +6 -2
  50. package/dist/server/agent-discovery.js +16 -9
  51. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  52. package/dist/server/identity-sso-store.d.ts +5 -4
  53. package/dist/server/identity-sso-store.js +5 -4
  54. package/package.json +1 -1
@@ -189,10 +189,10 @@ export interface A2AConfig {
189
189
  version?: string;
190
190
  skills: AgentSkill[];
191
191
  /**
192
- * Skills advertised only to a caller with a verified A2A identity — the set
193
- * `actions/invoke` will actually run. Anonymous card fetches never see these.
194
- * Without it the card advertises the public set, which is disjoint from the
195
- * invocable set, so siblings are told nothing is directly callable.
192
+ * Skills advertised only to a caller with a verified A2A identity. Read-only
193
+ * skills may also be available through `actions/invoke`; mutating skills are
194
+ * message-only capabilities for delegation. Anonymous card fetches never see
195
+ * either set.
196
196
  */
197
197
  authenticatedSkills?: AgentSkill[];
198
198
  /** If true, public agent-card discovery includes only explicit public-safe skills. */
@@ -1,6 +1,7 @@
1
1
  export interface VerifiedA2AClaims {
2
2
  email: string;
3
3
  orgId: string;
4
+ orgDomain: string;
4
5
  jti: string;
5
6
  issuer?: string;
6
7
  scope: string[];
@@ -24,10 +24,12 @@ export async function verifyA2ATokenWithClaims(token, event) {
24
24
  ? raw.scope.split(/\s+/).filter(Boolean)
25
25
  : [];
26
26
  const issuer = typeof raw.iss === "string" ? raw.iss.trim() : "";
27
- return orgId && jti
27
+ const orgDomain = identity.orgDomain?.trim().toLowerCase() ?? "";
28
+ return orgId && orgDomain && jti
28
29
  ? {
29
30
  email: identity.email,
30
31
  orgId,
32
+ orgDomain,
31
33
  jti,
32
34
  ...(issuer ? { issuer } : {}),
33
35
  scope: scopes,
@@ -117,9 +117,9 @@ function FeatureFlagRow({ flag, onMutate, isPending, error, }) {
117
117
  const actor = formatActor(flag.rules.updatedBy);
118
118
  const when = formatWhen(flag.rules.updatedAt);
119
119
  const metadata = [actor, when].filter(Boolean).join(" · ");
120
- return (_jsxs("article", { id: `feature-flag-${flag.key}`, className: "grid gap-4 scroll-mt-24 border-b border-border py-5 last:border-b-0 md:grid-cols-[minmax(0,1fr)_auto] md:items-center", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("h3", { className: "truncate text-sm font-medium text-foreground", children: flag.displayName ?? flag.key }), flag.displayName && flag.displayName !== flag.key ? (_jsx("code", { className: "truncate text-xs text-muted-foreground", children: flag.key })) : null, _jsx(Badge, { variant: flag.rules.mode === "on" ? "secondary" : "outline", className: "ms-auto shrink-0", children: modeLabel(flag.rules, t) })] }), flag.description ? (_jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: flag.description })) : null, _jsxs("div", { className: "mt-3 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { children: [t("featureFlags.enabledFor"), ": ", audienceLabel(flag.rules, t)] }), metadata ? (_jsx("span", { children: t("featureFlags.lastChanged", { metadata }) })) : null] }), _jsx("div", { className: "mt-2 flex items-center gap-1.5 text-xs font-medium text-foreground", children: flag.enabledForCurrentUser === true ? (_jsxs(_Fragment, { children: [_jsx(IconCheck, { className: "size-4 text-emerald-500" }), t("featureFlags.youHaveAccess")] })) : flag.enabledForCurrentUser === false ? (_jsx(_Fragment, { children: t("featureFlags.youDoNotHaveAccess") })) : (_jsx(_Fragment, { children: t("featureFlags.currentUserUnknown") })) }), error ? (_jsx("p", { className: "mt-3 text-sm text-destructive", children: t("featureFlags.mutationUnverified", {
120
+ return (_jsxs("article", { id: `feature-flag-${flag.key}`, className: "grid gap-4 scroll-mt-24 border-b border-border py-5 last:border-b-0 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("h3", { className: "truncate text-sm font-medium text-foreground", children: flag.displayName ?? flag.key }), flag.displayName && flag.displayName !== flag.key ? (_jsx("code", { className: "truncate text-xs text-muted-foreground", children: flag.key })) : null, _jsx(Badge, { variant: flag.rules.mode === "on" ? "secondary" : "outline", className: "ms-auto shrink-0", children: modeLabel(flag.rules, t) })] }), flag.description ? (_jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: flag.description })) : null, _jsxs("div", { className: "mt-3 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { children: [t("featureFlags.enabledFor"), ": ", audienceLabel(flag.rules, t)] }), metadata ? (_jsx("span", { children: t("featureFlags.lastChanged", { metadata }) })) : null] }), _jsx("div", { className: "mt-2 flex items-center gap-1.5 text-xs font-medium text-foreground", children: flag.enabledForCurrentUser === true ? (_jsxs(_Fragment, { children: [_jsx(IconCheck, { className: "size-4 text-emerald-500" }), t("featureFlags.youHaveAccess")] })) : flag.enabledForCurrentUser === false ? (_jsx(_Fragment, { children: t("featureFlags.youDoNotHaveAccess") })) : (_jsx(_Fragment, { children: t("featureFlags.currentUserUnknown") })) }), error ? (_jsx("p", { className: "mt-3 text-sm text-destructive", children: t("featureFlags.mutationUnverified", {
121
121
  name: flag.displayName ?? flag.key,
122
- }) })) : null] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 md:justify-end", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "text-xs", disabled: isPending || flag.enabledForCurrentUser === true, onClick: () => onMutate({
122
+ }) })) : null] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 lg:justify-end", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "text-xs", disabled: isPending || flag.enabledForCurrentUser === true, onClick: () => onMutate({
123
123
  key: flag.key,
124
124
  operation: "enable-for-current-user",
125
125
  }), children: [flag.enabledForCurrentUser === true ? (_jsx(IconCheck, {})) : (_jsx(IconUserCheck, {})), flag.enabledForCurrentUser === true
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -1,6 +1,8 @@
1
1
  import { getHeader } from "h3";
2
2
  import * as jose from "jose";
3
3
  import { verifyA2ATokenWithClaims } from "../a2a-claims.js";
4
+ import { resolveOrgByDomain } from "../org/context.js";
5
+ import { consumeOneTimeJti } from "../server/identity-sso-store.js";
4
6
  const FLAG_ACTION_SCOPES = {
5
7
  "list-feature-flags": "flags:read",
6
8
  "set-feature-flag": "flags:write",
@@ -37,9 +39,16 @@ export function createFeatureFlagA2AActionRouteAuth(actionName) {
37
39
  if (!claims || !claims.scope.includes(FLAG_ACTION_SCOPES[actionName])) {
38
40
  throw new Error("Invalid feature flag delegation");
39
41
  }
42
+ const localOrg = await resolveOrgByDomain(claims.orgDomain);
43
+ if (!localOrg)
44
+ throw new Error("Invalid feature flag delegation");
45
+ if (actionName === "set-feature-flag" &&
46
+ (await consumeOneTimeJti(claims.jti))) {
47
+ throw new Error("Invalid feature flag delegation");
48
+ }
40
49
  return {
41
50
  owner: claims.email,
42
- orgId: claims.orgId,
51
+ orgId: localOrg.orgId,
43
52
  anonymous: false,
44
53
  delegationJti: claims.jti,
45
54
  delegationIssuer: claims.issuer,
@@ -14,12 +14,13 @@ declare const _default: import("../../action.js").ActionDefinition<{
14
14
  percentage?: number;
15
15
  };
16
16
  }, {
17
- contractVersion: 1;
17
+ contractVersion: 2;
18
18
  status: "ready";
19
19
  key: string;
20
20
  rules: import("../store.js").FeatureFlagRules;
21
21
  scope: {
22
22
  orgId: string;
23
+ orgDomain: string;
23
24
  };
24
25
  }>;
25
26
  export default _default;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { defineAction } from "../../action.js";
3
+ import { getOrgDomain } from "../../org/context.js";
3
4
  import { requireFeatureFlagManager } from "../permissions.js";
4
5
  import { getFeatureFlagDefinition } from "../registry.js";
5
6
  import { defaultFeatureFlagRules, mutateFeatureFlagRules, normalizeFeatureFlagRules, } from "../store.js";
@@ -47,6 +48,12 @@ export default defineAction({
47
48
  if (!getFeatureFlagDefinition(args.key)) {
48
49
  throw new Error(`Unknown feature flag: ${args.key}`);
49
50
  }
51
+ const orgDomain = manager.orgId
52
+ ? (await getOrgDomain(manager.orgId))?.trim().toLowerCase() || null
53
+ : null;
54
+ if (ctx?.caller === "a2a" && manager.orgId && !orgDomain) {
55
+ throw new Error("Feature flag organization domain is unavailable.");
56
+ }
50
57
  const persistedRules = await mutateFeatureFlagRules(args.key, manager, (current) => {
51
58
  let rules;
52
59
  if (args.operation === "off") {
@@ -67,11 +74,11 @@ export default defineAction({
67
74
  return { ...rules, updatedAt: Date.now(), updatedBy: manager.email };
68
75
  });
69
76
  return {
70
- contractVersion: 1,
77
+ contractVersion: 2,
71
78
  status: "ready",
72
79
  key: args.key,
73
80
  rules: persistedRules,
74
- scope: { orgId: manager.orgId },
81
+ scope: { orgId: manager.orgId, orgDomain },
75
82
  };
76
83
  },
77
84
  });
@@ -71,11 +71,14 @@ export declare function resolveOrgDirectoryOrigin(env?: NodeJS.ProcessEnv): stri
71
71
  *
72
72
  * @param opts.selfId Current app id (so it's stripped from the result).
73
73
  * @param opts.selfOrigin Current app origin (so it's stripped by origin too).
74
+ * @param opts.includeDirectoryApp Request the directory authority itself. The
75
+ * server keeps legacy self-filtering unless this explicit signal is present.
74
76
  * @param opts.env Injectable env (tests). Defaults to `process.env`.
75
77
  */
76
78
  export declare function fetchOrgApps(opts?: {
77
79
  selfId?: string;
78
80
  selfOrigin?: string;
81
+ includeDirectoryApp?: boolean;
79
82
  serviceOrgId?: string;
80
83
  env?: NodeJS.ProcessEnv;
81
84
  }): Promise<OrgApp[]>;
@@ -109,18 +109,23 @@ function sameOrigin(a, b) {
109
109
  return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
110
110
  }
111
111
  }
112
- function scopedCacheKey(origin, auth) {
112
+ function scopedCacheKey(origin, auth, includeDirectoryApp = false) {
113
113
  return [
114
114
  origin,
115
115
  `user:${auth.userEmail ?? ""}`,
116
116
  `org:${auth.orgId ?? auth.orgDomain ?? ""}`,
117
+ `include-directory:${includeDirectoryApp ? "1" : "0"}`,
117
118
  ].join("|");
118
119
  }
119
120
  function authTokenAttempts(auth) {
120
121
  return [auth.apiKey, ...(auth.apiKeyFallbacks ?? [])].filter((token) => typeof token === "string" && token.length > 0);
121
122
  }
122
- function serviceScopedCacheKey(origin, orgId) {
123
- return [origin, `service-org:${orgId}`].join("|");
123
+ function serviceScopedCacheKey(origin, orgId, includeDirectoryApp = false) {
124
+ return [
125
+ origin,
126
+ `service-org:${orgId}`,
127
+ `include-directory:${includeDirectoryApp ? "1" : "0"}`,
128
+ ].join("|");
124
129
  }
125
130
  /**
126
131
  * Fetch the org's first-party sibling apps from the org directory.
@@ -133,6 +138,8 @@ function serviceScopedCacheKey(origin, orgId) {
133
138
  *
134
139
  * @param opts.selfId Current app id (so it's stripped from the result).
135
140
  * @param opts.selfOrigin Current app origin (so it's stripped by origin too).
141
+ * @param opts.includeDirectoryApp Request the directory authority itself. The
142
+ * server keeps legacy self-filtering unless this explicit signal is present.
136
143
  * @param opts.env Injectable env (tests). Defaults to `process.env`.
137
144
  */
138
145
  export async function fetchOrgApps(opts) {
@@ -155,7 +162,7 @@ export async function fetchOrgApps(opts) {
155
162
  let ttl = EMPTY_TTL_MS;
156
163
  const serviceOrgId = opts?.serviceOrgId?.trim();
157
164
  if (serviceOrgId) {
158
- const serviceCacheKey = serviceScopedCacheKey(origin, serviceOrgId);
165
+ const serviceCacheKey = serviceScopedCacheKey(origin, serviceOrgId, opts?.includeDirectoryApp);
159
166
  const cached = cache.get(serviceCacheKey);
160
167
  if (cached && cached.expiresAt > Date.now()) {
161
168
  return stripSelf(cached.apps);
@@ -174,7 +181,7 @@ export async function fetchOrgApps(opts) {
174
181
  }
175
182
  if (!cacheKey) {
176
183
  const now = Date.now();
177
- cacheKey = scopedCacheKey(origin, auth);
184
+ cacheKey = scopedCacheKey(origin, auth, opts?.includeDirectoryApp);
178
185
  const cached = cache.get(cacheKey);
179
186
  if (cached && cached.expiresAt > now) {
180
187
  return stripSelf(cached.apps);
@@ -186,6 +193,9 @@ export async function fetchOrgApps(opts) {
186
193
  headers: {
187
194
  Authorization: `Bearer ${attempts[i]}`,
188
195
  Accept: "application/json",
196
+ ...(opts?.includeDirectoryApp
197
+ ? { "X-Agent-Native-Include-Directory-App": "1" }
198
+ : {}),
189
199
  },
190
200
  signal: AbortSignal.timeout(4000),
191
201
  });
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- ok: boolean;
32
31
  error?: undefined;
32
+ ok: boolean;
33
33
  }>>;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
- ok?: undefined;
45
- error?: undefined;
46
44
  summary: import("./types.js").TraceSummary;
47
45
  spans: import("./types.js").TraceSpan[];
48
46
  id?: undefined;
49
- } | {
50
- ok?: undefined;
51
47
  error?: undefined;
48
+ ok?: undefined;
49
+ } | {
52
50
  summary?: undefined;
53
51
  spans?: undefined;
54
52
  id: string;
55
- } | {
53
+ error?: undefined;
56
54
  ok?: undefined;
55
+ } | {
57
56
  summary?: undefined;
58
57
  spans?: undefined;
59
58
  id?: undefined;
60
59
  error: any;
60
+ ok?: undefined;
61
61
  } | {
62
- error?: undefined;
63
62
  summary?: undefined;
64
63
  spans?: undefined;
65
64
  id?: undefined;
66
65
  ok: boolean;
66
+ error?: undefined;
67
67
  }>>;
@@ -75,8 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- id?: undefined;
79
78
  deleted?: undefined;
79
+ id?: undefined;
80
80
  provider?: undefined;
81
81
  providers: {
82
82
  id: string;
@@ -93,8 +93,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
93
93
  label?: undefined;
94
94
  message?: undefined;
95
95
  } | {
96
- id?: undefined;
97
96
  deleted?: undefined;
97
+ id?: undefined;
98
98
  providers?: undefined;
99
99
  count?: undefined;
100
100
  found: boolean;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -34,16 +34,16 @@ 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
- ok?: undefined;
38
37
  status?: undefined;
38
+ ok?: undefined;
39
39
  } | {
40
40
  ok: boolean;
41
41
  status: string;
42
42
  error?: undefined;
43
43
  } | {
44
- ok?: undefined;
45
44
  error: string;
46
45
  removed?: undefined;
46
+ ok?: undefined;
47
47
  } | {
48
48
  ok: boolean;
49
49
  removed: boolean;
@@ -54,9 +54,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
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;
58
57
  error: string;
59
58
  note?: undefined;
59
+ ok?: undefined;
60
60
  } | {
61
61
  ok: boolean;
62
62
  note?: undefined;
@@ -2,7 +2,7 @@ import { createError, defineEventHandler, setResponseStatus, setResponseHeader,
2
2
  import { verifyA2ATokenWithClaims } from "../a2a-claims.js";
3
3
  import { isAgentActionStopError } from "../action.js";
4
4
  import { declaresFeatureFlagDelegation } from "../feature-flags/a2a-action-route.js";
5
- import { resolveOrgIdForEmail } from "../org/context.js";
5
+ import { resolveOrgByDomain, resolveOrgIdForEmail } from "../org/context.js";
6
6
  import { readBody } from "../server/h3-helpers.js";
7
7
  import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
8
8
  import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
@@ -12,6 +12,7 @@ import { captureError } from "./capture-error.js";
12
12
  import { getAllowedCorsOrigin as resolveAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
13
13
  import { resolveEmbedSessionFromRequest, resolvedEmbedCapabilityScope, } from "./embed-session.js";
14
14
  import { getHttpRequestTelemetryId } from "./http-response-telemetry.js";
15
+ import { consumeOneTimeJti } from "./identity-sso-store.js";
15
16
  function requiredClientCompatibilityVersion() {
16
17
  const configured = typeof __AGENT_NATIVE_CLIENT_COMPATIBILITY_VERSION__ === "string"
17
18
  ? __AGENT_NATIVE_CLIENT_COMPATIBILITY_VERSION__
@@ -52,9 +53,16 @@ async function resolveFeatureFlagA2ACaller(event, actionName) {
52
53
  const claims = await verifyA2ATokenWithClaims(token, event);
53
54
  if (!claims || !claims.scope.includes(required))
54
55
  throw new Error("Invalid feature flag delegation");
56
+ const localOrg = await resolveOrgByDomain(claims.orgDomain);
57
+ if (!localOrg)
58
+ throw new Error("Invalid feature flag delegation");
59
+ if (actionName === "set-feature-flag" &&
60
+ (await consumeOneTimeJti(claims.jti))) {
61
+ throw new Error("Invalid feature flag delegation");
62
+ }
55
63
  return {
56
64
  owner: claims.email,
57
- orgId: claims.orgId,
65
+ orgId: localOrg.orgId,
58
66
  anonymous: false,
59
67
  delegationJti: claims.jti,
60
68
  delegationIssuer: claims.issuer,
Binary file
@@ -1,4 +1,5 @@
1
1
  import { type A2AArtifactResponseOptions, type A2AToolResultSummary } from "../../a2a/artifact-response.js";
2
+ import type { AgentSkill } from "../../a2a/types.js";
2
3
  import type { EngineTool } from "../../agent/engine/types.js";
3
4
  import { type ActionEntry, type AgentLoopOutcome } from "../../agent/production-agent.js";
4
5
  import { runAgentLoopDirectWithSoftTimeout } from "../../agent/run-loop-with-resume.js";
@@ -44,6 +45,13 @@ export interface A2AExternalAgentSurface {
44
45
  * query the caller wrote.
45
46
  */
46
47
  export declare function filterDirectA2AActions(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): Record<string, ActionEntry>;
48
+ /**
49
+ * Authenticated write capabilities that a sibling may delegate to this app's
50
+ * agent, but never invoke directly. `publicAgent` is the action-owned opt-in;
51
+ * keeping this projection independent of `connectorCatalog` avoids making apps
52
+ * maintain a second capability registry. The deny list remains a final veto.
53
+ */
54
+ export declare function filterDelegatedA2ACapabilityActions(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): Record<string, ActionEntry>;
47
55
  export declare function buildPublicAgentA2ASkills(actions: Record<string, ActionEntry>): Array<{
48
56
  id: string;
49
57
  name: string;
@@ -52,20 +60,13 @@ export declare function buildPublicAgentA2ASkills(actions: Record<string, Action
52
60
  inputSchema?: Record<string, unknown>;
53
61
  }>;
54
62
  /**
55
- * Skills for a caller with a verified A2A identity: exactly what
56
- * `filterDirectA2AActions` will execute. Kept separate from
57
- * `buildPublicAgentA2ASkills` because the public card may only name actions
58
- * with `requiresAuth !== true` while direct invocation requires
59
- * `requiresAuth === true` — advertising the public set alone tells siblings
60
- * an app has nothing callable when it does.
63
+ * Skills for a caller with a verified A2A identity. Exact bounded reads carry
64
+ * schemas and may be invoked directly; explicitly exposed writes carry no
65
+ * schema and are discoverable only as natural-language delegation targets.
66
+ * Kept separate from `buildPublicAgentA2ASkills` because neither authenticated
67
+ * surface may be disclosed by the anonymous card.
61
68
  */
62
- export declare function buildAuthenticatedAgentA2ASkills(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): Array<{
63
- id: string;
64
- name: string;
65
- description: string;
66
- publicAgent: ActionEntry["publicAgent"];
67
- inputSchema?: Record<string, unknown>;
68
- }>;
69
+ export declare function buildAuthenticatedAgentA2ASkills(actions: Record<string, ActionEntry>, options: A2AExternalAgentSurface): AgentSkill[];
69
70
  export declare function resolveArtifactBaseUrl(event: any | undefined): string | undefined;
70
71
  export declare function assembleA2AFinalResponse(events: readonly AgentChatEvent[], toolResults: readonly A2AToolResultSummary[], options?: A2AArtifactResponseOptions & {
71
72
  event?: any;
@@ -96,6 +96,24 @@ export function filterDirectA2AActions(actions, options) {
96
96
  exposure.isConsequential !== true);
97
97
  }));
98
98
  }
99
+ /**
100
+ * Authenticated write capabilities that a sibling may delegate to this app's
101
+ * agent, but never invoke directly. `publicAgent` is the action-owned opt-in;
102
+ * keeping this projection independent of `connectorCatalog` avoids making apps
103
+ * maintain a second capability registry. The deny list remains a final veto.
104
+ */
105
+ export function filterDelegatedA2ACapabilityActions(actions, options) {
106
+ const denied = new Set(options.externalAgents?.denyActions ?? []);
107
+ return Object.fromEntries(Object.entries(actions).filter(([name, entry]) => {
108
+ const exposure = entry.publicAgent;
109
+ return (!denied.has(name) &&
110
+ entry.agentTool !== false &&
111
+ entry.readOnly !== true &&
112
+ exposure?.expose === true &&
113
+ exposure.readOnly === false &&
114
+ exposure.requiresAuth === true);
115
+ }));
116
+ }
99
117
  export function buildPublicAgentA2ASkills(actions) {
100
118
  return Object.entries(filterPublicAgentActions(actions)).map(([name, entry]) => ({
101
119
  id: name,
@@ -110,31 +128,49 @@ export function buildPublicAgentA2ASkills(actions) {
110
128
  }));
111
129
  }
112
130
  /**
113
- * Skills for a caller with a verified A2A identity: exactly what
114
- * `filterDirectA2AActions` will execute. Kept separate from
115
- * `buildPublicAgentA2ASkills` because the public card may only name actions
116
- * with `requiresAuth !== true` while direct invocation requires
117
- * `requiresAuth === true` — advertising the public set alone tells siblings
118
- * an app has nothing callable when it does.
131
+ * Skills for a caller with a verified A2A identity. Exact bounded reads carry
132
+ * schemas and may be invoked directly; explicitly exposed writes carry no
133
+ * schema and are discoverable only as natural-language delegation targets.
134
+ * Kept separate from `buildPublicAgentA2ASkills` because neither authenticated
135
+ * surface may be disclosed by the anonymous card.
119
136
  */
120
137
  export function buildAuthenticatedAgentA2ASkills(actions, options) {
121
- return Object.entries(filterDirectA2AActions(actions, options)).map(([name, entry]) => ({
122
- id: name,
123
- name,
124
- description: entry.tool.description,
125
- publicAgent: entry.publicAgent,
126
- // Every action in this set is read-only by construction. Omitting the
127
- // flag made discovery label all of them "(mutating)", which pushes a
128
- // caller away from invoking them and back into open-ended delegation.
129
- readOnly: true,
130
- // Naming an action without its parameters is what makes a caller invoke
131
- // it with `{}` and fail on a required field.
132
- ...(entry.tool.parameters
133
- ? {
134
- inputSchema: entry.tool.parameters,
135
- }
136
- : {}),
137
- }));
138
+ const directActions = filterDirectA2AActions(actions, options);
139
+ const delegatedActions = filterDelegatedA2ACapabilityActions(actions, options);
140
+ return Object.entries(actions).flatMap(([name, entry]) => {
141
+ const directEntry = directActions[name];
142
+ const delegatedEntry = delegatedActions[name];
143
+ if (!directEntry && !delegatedEntry)
144
+ return [];
145
+ if (directEntry) {
146
+ return {
147
+ id: name,
148
+ name,
149
+ description: entry.tool.description,
150
+ publicAgent: entry.publicAgent,
151
+ // Every action in this set is read-only by construction. Omitting the
152
+ // flag made discovery label all of them "(mutating)", which pushes a
153
+ // caller away from invoking them and back into open-ended delegation.
154
+ readOnly: true,
155
+ // Naming a directly callable action without its parameters is what
156
+ // makes a caller invoke it with `{}` and fail on a required field.
157
+ ...(entry.tool.parameters
158
+ ? {
159
+ inputSchema: entry.tool.parameters,
160
+ }
161
+ : {}),
162
+ };
163
+ }
164
+ return {
165
+ id: name,
166
+ name,
167
+ description: entry.tool.description,
168
+ publicAgent: entry.publicAgent,
169
+ // No input schema: the sibling sends an objective to the receiving app,
170
+ // whose agent chooses and validates its own local actions.
171
+ readOnly: false,
172
+ };
173
+ });
138
174
  }
139
175
  export function resolveArtifactBaseUrl(event) {
140
176
  const fromEnv = process.env.APP_URL ||
@@ -70,12 +70,16 @@ export declare function shouldIncludeRemoteAgentManifest(manifest: {
70
70
  /**
71
71
  * Get built-in agents (static, no DB). Used as fallback and for seeding.
72
72
  */
73
- export declare function getBuiltinAgents(selfAppId?: string): DiscoveredAgent[];
73
+ export declare function getBuiltinAgents(selfAppId?: string, options?: {
74
+ preferLocalUrls?: boolean;
75
+ }): DiscoveredAgent[];
74
76
  /**
75
77
  * Discover all agents: built-in + custom agents stored as resources.
76
78
  * Custom agents override built-in agents with the same ID.
77
79
  */
78
- export declare function discoverAgents(selfAppId?: string): Promise<DiscoveredAgent[]>;
80
+ export declare function discoverAgents(selfAppId?: string, options?: {
81
+ preferLocalUrls?: boolean;
82
+ }): Promise<DiscoveredAgent[]>;
79
83
  /**
80
84
  * Look up a single agent by ID or name (case-insensitive).
81
85
  */
@@ -191,13 +191,13 @@ export function shouldIncludeRemoteAgentManifest(manifest, selfAppId) {
191
191
  /**
192
192
  * Get built-in agents (static, no DB). Used as fallback and for seeding.
193
193
  */
194
- export function getBuiltinAgents(selfAppId) {
194
+ export function getBuiltinAgents(selfAppId, options) {
195
195
  const normalizedSelfAppId = selfAppId ? normalizeAgentId(selfAppId) : "";
196
196
  return BUILTIN_AGENTS.filter((app) => app.id !== normalizedSelfAppId && app.url).map((app) => ({
197
197
  id: app.id,
198
198
  name: app.name,
199
199
  description: app.description,
200
- url: resolveAgentUrl(app),
200
+ url: resolveAgentUrl(app, options?.preferLocalUrls),
201
201
  color: app.color,
202
202
  }));
203
203
  }
@@ -205,8 +205,8 @@ export function getBuiltinAgents(selfAppId) {
205
205
  * Discover all agents: built-in + custom agents stored as resources.
206
206
  * Custom agents override built-in agents with the same ID.
207
207
  */
208
- export async function discoverAgents(selfAppId) {
209
- const builtins = getBuiltinAgents(selfAppId);
208
+ export async function discoverAgents(selfAppId, options) {
209
+ const builtins = getBuiltinAgents(selfAppId, options);
210
210
  const agentsById = new Map();
211
211
  // Start with built-ins
212
212
  for (const agent of builtins) {
@@ -249,6 +249,11 @@ export async function discoverAgents(selfAppId) {
249
249
  url = builtin.url;
250
250
  }
251
251
  const builtin = agentsById.get(manifestId);
252
+ if (options?.preferLocalUrls && builtin) {
253
+ const isBuiltinAgent = BUILTIN_AGENTS.some((candidate) => candidate.id === manifestId);
254
+ if (isBuiltinAgent)
255
+ url = builtin.url;
256
+ }
252
257
  const isLegacyAssetsManifest = manifest.id.trim().toLowerCase() !== manifestId;
253
258
  if (isLegacyAssetsManifest && builtin?.url) {
254
259
  try {
@@ -280,7 +285,7 @@ export async function discoverAgents(selfAppId) {
280
285
  }
281
286
  // Overlay sibling workspace apps last so same-origin workspaces prefer the
282
287
  // app mounted in this workspace over the public template with the same id.
283
- for (const agent of await discoverWorkspaceAgents(selfAppId)) {
288
+ for (const agent of await discoverWorkspaceAgents(selfAppId, options)) {
284
289
  agentsById.set(agent.id, agent);
285
290
  }
286
291
  return Array.from(agentsById.values());
@@ -347,8 +352,8 @@ function isHostedRuntime() {
347
352
  function shouldUseLocalAgentUrls() {
348
353
  return !isHostedRuntime();
349
354
  }
350
- function resolveAgentUrl(app) {
351
- if (shouldUseLocalAgentUrls()) {
355
+ function resolveAgentUrl(app, preferLocalUrls = false) {
356
+ if (preferLocalUrls || shouldUseLocalAgentUrls()) {
352
357
  return app.devUrl || `http://localhost:${app.devPort}`;
353
358
  }
354
359
  return app.url;
@@ -527,7 +532,7 @@ function workspaceAppUrl(app, hostedLoopbackFallbackUrl) {
527
532
  return null;
528
533
  }
529
534
  }
530
- async function discoverWorkspaceAgents(selfAppId) {
535
+ async function discoverWorkspaceAgents(selfAppId, options) {
531
536
  const workspaceApps = loadWorkspaceAppsManifest();
532
537
  if (!workspaceApps)
533
538
  return [];
@@ -537,7 +542,9 @@ async function discoverWorkspaceAgents(selfAppId) {
537
542
  .map((app) => {
538
543
  const withOverride = applyWorkspaceAppMetadataOverride(app, metadataSettings);
539
544
  const builtin = BUILTIN_AGENTS.find((agent) => agent.id === withOverride.id);
540
- const url = workspaceAppUrl(withOverride, builtin?.url);
545
+ const url = options?.preferLocalUrls && builtin
546
+ ? resolveAgentUrl(builtin, true)
547
+ : workspaceAppUrl(withOverride, builtin?.url);
541
548
  if (!url)
542
549
  return null;
543
550
  return {