@stigmer/react 3.1.8 → 3.1.9

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 (72) hide show
  1. package/environment/EnvironmentPicker.d.ts +17 -1
  2. package/environment/EnvironmentPicker.d.ts.map +1 -1
  3. package/environment/EnvironmentPicker.js +3 -2
  4. package/environment/EnvironmentPicker.js.map +1 -1
  5. package/index.d.ts +2 -2
  6. package/index.d.ts.map +1 -1
  7. package/index.js +2 -2
  8. package/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/sharing/ShareAgentDialog.d.ts +10 -8
  11. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  12. package/sharing/ShareAgentDialog.js +139 -54
  13. package/sharing/ShareAgentDialog.js.map +1 -1
  14. package/sharing/SharedAgentChat.d.ts +1 -1
  15. package/sharing/SharedAgentChat.d.ts.map +1 -1
  16. package/sharing/index.d.ts +4 -2
  17. package/sharing/index.d.ts.map +1 -1
  18. package/sharing/index.js +2 -1
  19. package/sharing/index.js.map +1 -1
  20. package/sharing/useAgentShare.d.ts +43 -0
  21. package/sharing/useAgentShare.d.ts.map +1 -0
  22. package/sharing/useAgentShare.js +54 -0
  23. package/sharing/useAgentShare.js.map +1 -0
  24. package/sharing/useRotateShareLink.d.ts +22 -19
  25. package/sharing/useRotateShareLink.d.ts.map +1 -1
  26. package/sharing/useRotateShareLink.js +22 -19
  27. package/sharing/useRotateShareLink.js.map +1 -1
  28. package/sharing/useSaveAgentShare.d.ts +105 -0
  29. package/sharing/useSaveAgentShare.d.ts.map +1 -0
  30. package/sharing/useSaveAgentShare.js +97 -0
  31. package/sharing/useSaveAgentShare.js.map +1 -0
  32. package/sharing/useShareAgent.d.ts +5 -4
  33. package/sharing/useShareAgent.d.ts.map +1 -1
  34. package/sharing/useShareAgent.js +5 -4
  35. package/sharing/useShareAgent.js.map +1 -1
  36. package/sharing/useShareToolReadiness.d.ts +29 -25
  37. package/sharing/useShareToolReadiness.d.ts.map +1 -1
  38. package/sharing/useShareToolReadiness.js +47 -51
  39. package/sharing/useShareToolReadiness.js.map +1 -1
  40. package/sharing/useSharedAgentProfile.d.ts +5 -4
  41. package/sharing/useSharedAgentProfile.d.ts.map +1 -1
  42. package/sharing/useSharedAgentProfile.js +6 -5
  43. package/sharing/useSharedAgentProfile.js.map +1 -1
  44. package/src/environment/EnvironmentPicker.tsx +23 -2
  45. package/src/index.ts +8 -4
  46. package/src/sharing/ShareAgentDialog.tsx +353 -119
  47. package/src/sharing/SharedAgentChat.tsx +1 -1
  48. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +550 -369
  49. package/src/sharing/__tests__/useAgentShare.test.tsx +149 -0
  50. package/src/sharing/__tests__/useRotateShareLink.test.tsx +8 -8
  51. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +241 -0
  52. package/src/sharing/__tests__/useShareAgent.test.tsx +6 -2
  53. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +84 -47
  54. package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +1 -1
  55. package/src/sharing/index.ts +9 -7
  56. package/src/sharing/useAgentShare.ts +91 -0
  57. package/src/sharing/useRotateShareLink.ts +30 -25
  58. package/src/sharing/useSaveAgentShare.ts +186 -0
  59. package/src/sharing/useShareAgent.tsx +5 -4
  60. package/src/sharing/useShareToolReadiness.ts +62 -60
  61. package/src/sharing/useSharedAgentProfile.ts +7 -6
  62. package/src/switch/Switch.tsx +6 -1
  63. package/src/switch/__tests__/Switch.test.tsx +19 -0
  64. package/switch/Switch.d.ts.map +1 -1
  65. package/switch/Switch.js +6 -1
  66. package/switch/Switch.js.map +1 -1
  67. package/sharing/useUpdateAgentSharing.d.ts +0 -86
  68. package/sharing/useUpdateAgentSharing.d.ts.map +0 -1
  69. package/sharing/useUpdateAgentSharing.js +0 -81
  70. package/sharing/useUpdateAgentSharing.js.map +0 -1
  71. package/src/sharing/__tests__/useUpdateAgentSharing.test.tsx +0 -200
  72. package/src/sharing/useUpdateAgentSharing.ts +0 -151
@@ -9,8 +9,12 @@ import {
9
9
  chatPath,
10
10
  getUserMessage,
11
11
  validateOrigin,
12
+ type ResourceRef,
12
13
  } from "@stigmer/sdk";
13
14
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
15
+ import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
16
+ import type { Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
17
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
14
18
  import { Switch } from "../switch/Switch.js";
15
19
  import { Tabs, type TabItem } from "../tabs/Tabs.js";
16
20
  import { toast } from "../feedback/toast.js";
@@ -18,12 +22,14 @@ import { useCopyResource } from "../resource-detail/useCopyResource.js";
18
22
  import { useDeploymentMode } from "../deployment-mode.js";
19
23
  import { useBillingAccount } from "../billing/useBillingAccount.js";
20
24
  import { formatCreditBalance } from "../billing/format.js";
25
+ import { EnvironmentPicker } from "../environment/EnvironmentPicker.js";
26
+ import { useAgentShare } from "./useAgentShare.js";
21
27
  import {
22
28
  sharingAudienceFromProto,
23
- useUpdateAgentSharing,
24
- type AgentSharingDraft,
29
+ useSaveAgentShare,
30
+ type AgentShareDraft,
25
31
  type SharingAudience,
26
- } from "./useUpdateAgentSharing.js";
32
+ } from "./useSaveAgentShare.js";
27
33
  import { useRotateShareLink } from "./useRotateShareLink.js";
28
34
  import { useShareToolReadiness } from "./useShareToolReadiness.js";
29
35
 
@@ -70,20 +76,22 @@ export interface ShareAgentDialogProps {
70
76
  }
71
77
 
72
78
  /**
73
- * The Share dialog for an agent: toggle public sharing, copy the hosted
74
- * chat link, copy an embeddable iframe snippet, manage allowed embed
75
- * origins, customize visitor refusal messages, and discover the
76
- * PlatformClient SDK path.
79
+ * The Share dialog for an agent: toggle sharing, copy the hosted chat
80
+ * link, copy an embeddable snippet, manage allowed embed origins, bind
81
+ * tool credentials for visitors, customize visitor refusal messages,
82
+ * and discover the PlatformClient SDK path.
77
83
  *
78
84
  * Sharing is a distinct consent from marketplace visibility: visibility
79
85
  * governs who can *read* the blueprint; sharing governs who can *chat*
80
86
  * with the running agent — billed to the owning org. The dialog states
81
87
  * who pays next to the toggle so enabling sharing never surprises.
82
88
  *
83
- * Every save sends the complete sharing configuration (the RPC replaces
84
- * `spec.sharing` wholesale see {@link AgentSharingDraft}), and the
85
- * local draft is refreshed from the RPC's returned agent, so the dialog
86
- * never drifts from the server.
89
+ * Sharing lives in its own **AgentShare resource** (decision 011), so
90
+ * the dialog loads the agent's canonical share when it opens and every
91
+ * save is an idempotent `apply` of the complete configuration the
92
+ * first enable creates the share, later edits update it, one code path
93
+ * (see {@link useSaveAgentShare}). The local draft is refreshed from
94
+ * every returned share, so the dialog never drifts from the server.
87
95
  *
88
96
  * Built on the native `<dialog>` element for focus trapping and escape
89
97
  * handling, matching the SDK's modal convention ({@link ManageAccessDialog}).
@@ -135,8 +143,8 @@ export function ShareAgentDialog({
135
143
  )}
136
144
  aria-labelledby="share-agent-title"
137
145
  >
138
- {/* Body mounts only while open so the draft resets per session and
139
- the billing fetch never fires on a closed dialog. */}
146
+ {/* Body mounts only while open so the share is re-read per session
147
+ (the draft resets with it) and no fetch fires on a closed dialog. */}
140
148
  {open && (
141
149
  <ShareAgentDialogBody
142
150
  agent={agent}
@@ -150,65 +158,186 @@ export function ShareAgentDialog({
150
158
  }
151
159
 
152
160
  // ---------------------------------------------------------------------------
153
- // Dialog body — owns the sharing draft for the session
161
+ // Dialog body — resolves the canonical share, then hands off to the form
154
162
  // ---------------------------------------------------------------------------
155
163
 
156
- function draftFromAgent(agent: Agent): AgentSharingDraft {
157
- const sharing = agent.spec?.sharing;
164
+ /**
165
+ * Loads the agent's canonical share and mounts the form once resolved.
166
+ * The split keeps the form's draft seeding synchronous (`useState`
167
+ * initializer from the loaded share) — no hydrate-on-effect, no window
168
+ * where the switch shows a state the server never had.
169
+ */
170
+ function ShareAgentDialogBody({
171
+ agent,
172
+ buildShareUrl,
173
+ onSharingChanged,
174
+ onClose,
175
+ }: {
176
+ readonly agent: Agent;
177
+ readonly buildShareUrl?: (org: string, slug: string) => string;
178
+ readonly onSharingChanged?: () => void;
179
+ readonly onClose: () => void;
180
+ }) {
181
+ const { share, isLoading, error, refetch } = useAgentShare(agent);
182
+
183
+ return (
184
+ <div className="flex flex-col">
185
+ {/* Header */}
186
+ <div className="flex items-start justify-between border-b border-border px-6 py-4">
187
+ <div className="min-w-0">
188
+ <h2
189
+ id="share-agent-title"
190
+ className="text-base font-semibold text-foreground"
191
+ >
192
+ Share
193
+ </h2>
194
+ <p className="mt-0.5 truncate text-xs text-muted-foreground">
195
+ {agent.metadata?.name || agent.metadata?.slug}
196
+ </p>
197
+ </div>
198
+ <button
199
+ type="button"
200
+ onClick={onClose}
201
+ aria-label="Close"
202
+ className={cn(
203
+ "rounded-md p-1 text-muted-foreground",
204
+ "hover:text-foreground hover:bg-accent-hover",
205
+ "focus:outline-none focus:ring-2 focus:ring-ring",
206
+ )}
207
+ >
208
+ <CloseIcon />
209
+ </button>
210
+ </div>
211
+
212
+ {isLoading ? (
213
+ <div
214
+ className="px-6 py-10 text-center"
215
+ aria-busy="true"
216
+ aria-label="Loading sharing settings"
217
+ >
218
+ <div className="mx-auto h-4 w-2/3 animate-pulse rounded bg-muted" />
219
+ <div className="mx-auto mt-2 h-3 w-1/2 animate-pulse rounded bg-muted" />
220
+ </div>
221
+ ) : error ? (
222
+ <div className="px-6 py-8 text-center" role="alert">
223
+ <p className="text-sm text-foreground">
224
+ Couldn&apos;t load this agent&apos;s sharing settings.
225
+ </p>
226
+ <p className="mt-1 text-xs text-muted-foreground">
227
+ {getUserMessage(error)}
228
+ </p>
229
+ <button
230
+ type="button"
231
+ onClick={refetch}
232
+ className={cn(
233
+ "mt-3 rounded-md px-3 py-1.5 text-xs font-medium",
234
+ "border border-border text-foreground hover:bg-accent-hover",
235
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
236
+ )}
237
+ >
238
+ Try again
239
+ </button>
240
+ </div>
241
+ ) : (
242
+ <ShareAgentForm
243
+ agent={agent}
244
+ initialShare={share}
245
+ buildShareUrl={buildShareUrl}
246
+ onSharingChanged={onSharingChanged}
247
+ />
248
+ )}
249
+
250
+ {/* Footer */}
251
+ <div className="flex items-center justify-end border-t border-border px-6 py-3">
252
+ <button
253
+ type="button"
254
+ onClick={onClose}
255
+ className={cn(
256
+ "rounded-md px-3 py-1.5 text-sm font-medium",
257
+ "bg-primary text-primary-foreground hover:bg-primary-hover",
258
+ "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
259
+ )}
260
+ >
261
+ Done
262
+ </button>
263
+ </div>
264
+ </div>
265
+ );
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Form — owns the share draft for the session
270
+ // ---------------------------------------------------------------------------
271
+
272
+ /**
273
+ * The dialog's editable projection of a share. `null` (never shared)
274
+ * seeds the same defaults the server would apply on first create, so
275
+ * the form needs no "no share yet" special case beyond the disabled
276
+ * copy fields that `enabled: false` already produces.
277
+ */
278
+ function draftFromShare(share: AgentShare | null): AgentShareDraft {
279
+ const spec = share?.spec;
158
280
  return {
159
- enabled: sharing?.enabled ?? false,
160
- audience: sharingAudienceFromProto(sharing?.audience),
161
- allowedOrigins: sharing?.allowedOrigins ?? [],
281
+ enabled: spec?.enabled ?? false,
282
+ audience: sharingAudienceFromProto(spec?.audience),
283
+ allowedOrigins: spec?.allowedOrigins ?? [],
162
284
  messages: {
163
- rateLimited: sharing?.messages?.rateLimited ?? "",
164
- unavailable: sharing?.messages?.unavailable ?? "",
165
- conversationEnded: sharing?.messages?.conversationEnded ?? "",
285
+ rateLimited: spec?.messages?.rateLimited ?? "",
286
+ unavailable: spec?.messages?.unavailable ?? "",
287
+ conversationEnded: spec?.messages?.conversationEnded ?? "",
166
288
  },
289
+ environmentRefs: (spec?.environmentRefs ?? []).map((ref) => ({
290
+ org: ref.org,
291
+ slug: ref.slug,
292
+ })),
167
293
  };
168
294
  }
169
295
 
170
- function ShareAgentDialogBody({
296
+ function ShareAgentForm({
171
297
  agent,
298
+ initialShare,
172
299
  buildShareUrl,
173
300
  onSharingChanged,
174
- onClose,
175
301
  }: {
176
302
  readonly agent: Agent;
303
+ readonly initialShare: AgentShare | null;
177
304
  readonly buildShareUrl?: (org: string, slug: string) => string;
178
305
  readonly onSharingChanged?: () => void;
179
- readonly onClose: () => void;
180
306
  }) {
181
- const org = agent.metadata?.org ?? "";
182
- const slug = agent.metadata?.slug ?? "";
183
- const agentName = agent.metadata?.name || slug;
184
-
185
- const [draft, setDraft] = useState<AgentSharingDraft>(() =>
186
- draftFromAgent(agent),
187
- );
188
- // The share-link token is server-owned status (never part of the sharing
189
- // draft — rotateShareLink is its sole writer); track it separately and
190
- // adopt it from every returned agent so the shown link never goes stale.
191
- const [linkToken, setLinkToken] = useState(
192
- () => agent.status?.shareLinkToken ?? "",
307
+ const agentName = agent.metadata?.name || (agent.metadata?.slug ?? "");
308
+
309
+ // The latest server share is the single baseline: the draft, the link
310
+ // token, and the share id for rotation all derive from it. `null`
311
+ // until the first save creates the share.
312
+ const [share, setShare] = useState<AgentShare | null>(initialShare);
313
+ const [draft, setDraft] = useState<AgentShareDraft>(() =>
314
+ draftFromShare(initialShare),
193
315
  );
194
316
  const [activeTab, setActiveTab] = useState("link");
195
317
 
196
- const { updateSharing, isPending } = useUpdateAgentSharing(
197
- agent.metadata?.id ?? null,
198
- );
318
+ const { save, isPending } = useSaveAgentShare(agent);
199
319
  const { rotateShareLink, isPending: isRotating } = useRotateShareLink(
200
- agent.metadata?.id ?? null,
320
+ share?.metadata?.id ?? null,
201
321
  );
202
322
 
203
- // Single commit path: send the complete draft, adopt the server's
204
- // returned agent as the new baseline, notify the host.
323
+ // The share's own org/slug form the hosted URL. Before the first save
324
+ // the agent's stand in exactly the identity the server will assign
325
+ // on create (D2: share slug defaults to the agent's).
326
+ const org = share?.metadata?.org || (agent.metadata?.org ?? "");
327
+ const slug = share?.metadata?.slug || (agent.metadata?.slug ?? "");
328
+ const linkToken = share?.status?.shareLinkToken ?? "";
329
+
330
+ // Single commit path: apply the complete draft, adopt the server's
331
+ // returned share as the new baseline, notify the host.
205
332
  const commit = useCallback(
206
- async (next: AgentSharingDraft, successMessage: string): Promise<boolean> => {
333
+ async (next: AgentShareDraft, successMessage: string): Promise<boolean> => {
207
334
  try {
208
- const updated = await updateSharing(next);
209
- setDraft(updated ? draftFromAgent(updated) : next);
210
- if (updated) {
211
- setLinkToken(updated.status?.shareLinkToken ?? "");
335
+ const persisted = await save(next, share);
336
+ if (persisted) {
337
+ setShare(persisted);
338
+ setDraft(draftFromShare(persisted));
339
+ } else {
340
+ setDraft(next);
212
341
  }
213
342
  toast.success(successMessage);
214
343
  onSharingChanged?.();
@@ -218,14 +347,14 @@ function ShareAgentDialogBody({
218
347
  return false;
219
348
  }
220
349
  },
221
- [updateSharing, onSharingChanged],
350
+ [save, share, onSharingChanged],
222
351
  );
223
352
 
224
353
  const handleRotateLink = useCallback(async () => {
225
354
  try {
226
355
  const updated = await rotateShareLink();
227
356
  if (updated) {
228
- setLinkToken(updated.status?.shareLinkToken ?? "");
357
+ setShare(updated);
229
358
  }
230
359
  toast.success("Link reset — the old link no longer works");
231
360
  onSharingChanged?.();
@@ -247,10 +376,22 @@ function ShareAgentDialogBody({
247
376
  const handleAudienceChange = useCallback(
248
377
  (audience: SharingAudience) => {
249
378
  if (audience === draft.audience) return;
379
+ // Credential bindings are public-audience only (the proto CEL rule
380
+ // rejects them on org shares — decision 011 addendum), so switching
381
+ // to org drops them, and the toast says so: silent config loss is
382
+ // worse than a wordier confirmation.
383
+ const dropsBindings =
384
+ audience === "org" && draft.environmentRefs.length > 0;
250
385
  void commit(
251
- { ...draft, audience },
386
+ {
387
+ ...draft,
388
+ audience,
389
+ environmentRefs: dropsBindings ? [] : draft.environmentRefs,
390
+ },
252
391
  audience === "org"
253
- ? "Only organization members can chat now"
392
+ ? dropsBindings
393
+ ? "Only organization members can chat now — tool credential bindings were removed (they apply to public links only)"
394
+ : "Only organization members can chat now"
254
395
  : "Anyone with the link can chat now",
255
396
  );
256
397
  },
@@ -268,34 +409,7 @@ function ShareAgentDialogBody({
268
409
  : appendLinkToken(baseShareUrl, linkToken);
269
410
 
270
411
  return (
271
- <div className="flex flex-col">
272
- {/* Header */}
273
- <div className="flex items-start justify-between border-b border-border px-6 py-4">
274
- <div className="min-w-0">
275
- <h2
276
- id="share-agent-title"
277
- className="text-base font-semibold text-foreground"
278
- >
279
- Share
280
- </h2>
281
- <p className="mt-0.5 truncate text-xs text-muted-foreground">
282
- {agentName}
283
- </p>
284
- </div>
285
- <button
286
- type="button"
287
- onClick={onClose}
288
- aria-label="Close"
289
- className={cn(
290
- "rounded-md p-1 text-muted-foreground",
291
- "hover:text-foreground hover:bg-accent-hover",
292
- "focus:outline-none focus:ring-2 focus:ring-ring",
293
- )}
294
- >
295
- <CloseIcon />
296
- </button>
297
- </div>
298
-
412
+ <>
299
413
  {/* Sharing master switch — governs every tab, so it sits above them. */}
300
414
  <div className="border-b border-border px-6 py-4">
301
415
  <div className="flex items-start justify-between gap-4">
@@ -322,7 +436,7 @@ function ShareAgentDialogBody({
322
436
  onChange={handleAudienceChange}
323
437
  disabled={isPending}
324
438
  />
325
- <ToolReadinessHint agent={agent} enabled={draft.enabled} />
439
+ <ToolReadinessHint agent={agent} draft={draft} />
326
440
  </div>
327
441
 
328
442
  {/* Tabs */}
@@ -338,6 +452,7 @@ function ShareAgentDialogBody({
338
452
  <LinkTab
339
453
  shareUrl={shareUrl}
340
454
  org={org}
455
+ agent={agent}
341
456
  enabled={draft.enabled}
342
457
  draft={draft}
343
458
  isPending={isPending}
@@ -364,44 +479,41 @@ function ShareAgentDialogBody({
364
479
  </div>
365
480
  </Tabs>
366
481
  </div>
367
-
368
- {/* Footer */}
369
- <div className="flex items-center justify-end border-t border-border px-6 py-3">
370
- <button
371
- type="button"
372
- onClick={onClose}
373
- className={cn(
374
- "rounded-md px-3 py-1.5 text-sm font-medium",
375
- "bg-primary text-primary-foreground hover:bg-primary-hover",
376
- "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
377
- )}
378
- >
379
- Done
380
- </button>
381
- </div>
382
- </div>
482
+ </>
383
483
  );
384
484
  }
385
485
 
386
486
  // ---------------------------------------------------------------------------
387
- // Tool readiness — private credentials block visitors' tool use
487
+ // Tool readiness — visitors' tool use needs credentials bound to the share
388
488
  // ---------------------------------------------------------------------------
389
489
 
390
490
  /**
391
- * Pre-flight hint for tool-using agents: visitors' chats can only use
392
- * environments shared with the organization. Renders nothing unless the
393
- * check finds bound environments that are still private — sharing stays
394
- * one toggle; this only catches the misconfiguration at share time
395
- * instead of at the visitor's first message.
491
+ * Pre-flight hint for tool-using agents: visitors' chats receive
492
+ * credentials only from the share's own environment bindings, so a
493
+ * tool-using agent with no bindings (`needs-credentials`) or a binding
494
+ * that is still private (`blocked`) will fail at the visitor's first
495
+ * message. Renders nothing when there is nothing to fix — sharing stays
496
+ * one toggle; this only catches the misconfiguration at share time.
396
497
  */
397
498
  function ToolReadinessHint({
398
499
  agent,
399
- enabled,
500
+ draft,
400
501
  }: {
401
502
  readonly agent: Agent;
402
- readonly enabled: boolean;
503
+ readonly draft: AgentShareDraft;
403
504
  }) {
404
- const readiness = useShareToolReadiness(agent, enabled);
505
+ const readiness = useShareToolReadiness(agent, draft);
506
+
507
+ if (readiness.status === "needs-credentials") {
508
+ return (
509
+ <p className="mt-2 text-xs text-warning" role="status">
510
+ Visitors&apos; chats can&apos;t use this agent&apos;s tools yet: no
511
+ credentials are bound to this share. Bind an org-shared environment
512
+ under <span className="font-medium">Tool credentials</span> in the
513
+ Link tab below.
514
+ </p>
515
+ );
516
+ }
405
517
 
406
518
  if (readiness.status !== "blocked") {
407
519
  return null;
@@ -415,8 +527,8 @@ function ToolReadinessHint({
415
527
  Visitors&apos; chats can&apos;t use this agent&apos;s tools yet: the
416
528
  environment{plural ? "s" : ""} <span className="font-medium">{envList}</span>{" "}
417
529
  {plural ? "are" : "is"} private. Share {plural ? "them" : "it"} with your
418
- organization (Settings &rarr; Environments) so visitor and teammate runs
419
- can use the credentials. Secret values stay hidden either way.
530
+ organization (Settings &rarr; Environments) so visitor runs can use the
531
+ credentials. Secret values stay hidden either way.
420
532
  </p>
421
533
  );
422
534
  }
@@ -493,7 +605,7 @@ function WhoPaysLine({
493
605
  }) {
494
606
  const mode = useDeploymentMode();
495
607
  // An Organization's id equals its slug (see ApiResourceMetadata.id), so
496
- // the agent's org reference is directly usable as the billing org id.
608
+ // the share's org reference is directly usable as the billing org id.
497
609
  // Cloud-only: local mode has no billing accounts.
498
610
  const { account } = useBillingAccount(mode === "cloud" ? org : null);
499
611
 
@@ -515,9 +627,25 @@ function WhoPaysLine({
515
627
  // Link tab
516
628
  // ---------------------------------------------------------------------------
517
629
 
630
+ /**
631
+ * Names the reason the copy fields above are grayed out: sharing is off, so
632
+ * the link/embed would lead to NOT_FOUND (the fields stay disabled — handing
633
+ * out a dead link is worse than a disabled button). Points at the master
634
+ * switch as the remedy.
635
+ */
636
+ function SharingOffHint({ subject }: { readonly subject: string }) {
637
+ return (
638
+ <p className="text-xs text-muted-foreground" role="status">
639
+ Sharing is off, so {subject} doesn&apos;t work yet — turn on the switch
640
+ above to activate it.
641
+ </p>
642
+ );
643
+ }
644
+
518
645
  function LinkTab({
519
646
  shareUrl,
520
647
  org,
648
+ agent,
521
649
  enabled,
522
650
  draft,
523
651
  isPending,
@@ -528,11 +656,12 @@ function LinkTab({
528
656
  }: {
529
657
  readonly shareUrl: string;
530
658
  readonly org: string;
659
+ readonly agent: Agent;
531
660
  readonly enabled: boolean;
532
- readonly draft: AgentSharingDraft;
661
+ readonly draft: AgentShareDraft;
533
662
  readonly isPending: boolean;
534
663
  readonly commit: (
535
- next: AgentSharingDraft,
664
+ next: AgentShareDraft,
536
665
  successMessage: string,
537
666
  ) => Promise<boolean>;
538
667
  readonly hasLinkToken: boolean;
@@ -550,6 +679,7 @@ function LinkTab({
550
679
  disabled={!enabled}
551
680
  openHref={enabled ? shareUrl : undefined}
552
681
  />
682
+ {!enabled && <SharingOffHint subject="this link" />}
553
683
 
554
684
  {isOrgAudience ? (
555
685
  <p className="text-xs text-muted-foreground">
@@ -575,6 +705,16 @@ function LinkTab({
575
705
  />
576
706
  )}
577
707
 
708
+ {!isOrgAudience && (
709
+ <ToolCredentialsSection
710
+ org={org}
711
+ agent={agent}
712
+ draft={draft}
713
+ isPending={isPending}
714
+ commit={commit}
715
+ />
716
+ )}
717
+
578
718
  <MessagesEditor draft={draft} isPending={isPending} commit={commit} />
579
719
  </div>
580
720
  );
@@ -627,6 +767,101 @@ function ResetLinkControl({
627
767
  );
628
768
  }
629
769
 
770
+ // ---------------------------------------------------------------------------
771
+ // Tool credentials — org-shared environments bound to the share for visitors
772
+ // ---------------------------------------------------------------------------
773
+
774
+ /**
775
+ * Binds org-shared environments to the share's `environment_refs` — the
776
+ * consent act that makes a tool-using agent work for visitors (decision
777
+ * 011: credentials belong to the channel, never to the agent's pristine
778
+ * default instance). Public audience only; the section disappears for
779
+ * org shares, whose member sessions carry no share linkage in Phase A.
780
+ *
781
+ * Expanded by default when the agent uses MCP tools — for those agents
782
+ * this is essential configuration, not an advanced option.
783
+ */
784
+ function ToolCredentialsSection({
785
+ org,
786
+ agent,
787
+ draft,
788
+ isPending,
789
+ commit,
790
+ }: {
791
+ readonly org: string;
792
+ readonly agent: Agent;
793
+ readonly draft: AgentShareDraft;
794
+ readonly isPending: boolean;
795
+ readonly commit: (
796
+ next: AgentShareDraft,
797
+ successMessage: string,
798
+ ) => Promise<boolean>;
799
+ }) {
800
+ const hasMcpTools = (agent.spec?.mcpServerUsages?.length ?? 0) > 0;
801
+ const [expanded, setExpanded] = useState(
802
+ hasMcpTools || draft.environmentRefs.length > 0,
803
+ );
804
+
805
+ const handleChange = useCallback(
806
+ (refs: ResourceRef[]) => {
807
+ const added = refs.length > draft.environmentRefs.length;
808
+ void commit(
809
+ { ...draft, environmentRefs: refs },
810
+ added ? "Credentials bound" : "Credential bindings updated",
811
+ );
812
+ },
813
+ [commit, draft],
814
+ );
815
+
816
+ // Only org-shared environments are guest-usable (the runtime merge
817
+ // skips private ones — decision 006), so offering others would bind
818
+ // credentials that silently never apply.
819
+ const onlyOrgShared = useCallback(
820
+ (env: Environment) =>
821
+ env.metadata?.visibility === ApiResourceVisibility.visibility_org,
822
+ [],
823
+ );
824
+
825
+ return (
826
+ <section>
827
+ <button
828
+ type="button"
829
+ onClick={() => setExpanded((v) => !v)}
830
+ aria-expanded={expanded}
831
+ className={cn(
832
+ "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground",
833
+ "hover:text-foreground",
834
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded",
835
+ )}
836
+ >
837
+ <ChevronIcon
838
+ className={cn("size-3 transition-transform", expanded && "rotate-90")}
839
+ />
840
+ Tool credentials
841
+ </button>
842
+
843
+ {expanded && (
844
+ <div className="mt-2 flex flex-col gap-2">
845
+ <p className="text-[0.65rem] text-muted-foreground">
846
+ Environments whose values visitors&apos; chats can use — bind one
847
+ holding the credentials this agent&apos;s tools need (a read-only
848
+ token is safest). Only environments shared with your organization
849
+ can be bound; share one first in Settings &rarr; Environments.
850
+ Secret values stay hidden from visitors either way.
851
+ </p>
852
+ <EnvironmentPicker
853
+ org={org}
854
+ value={draft.environmentRefs}
855
+ onChange={handleChange}
856
+ disabled={isPending}
857
+ filterEnvironment={onlyOrgShared}
858
+ />
859
+ </div>
860
+ )}
861
+ </section>
862
+ );
863
+ }
864
+
630
865
  // ---------------------------------------------------------------------------
631
866
  // Embed tab
632
867
  // ---------------------------------------------------------------------------
@@ -675,10 +910,10 @@ function EmbedTab({
675
910
  readonly agentName: string;
676
911
  readonly linkToken: string;
677
912
  readonly enabled: boolean;
678
- readonly draft: AgentSharingDraft;
913
+ readonly draft: AgentShareDraft;
679
914
  readonly isPending: boolean;
680
915
  readonly commit: (
681
- next: AgentSharingDraft,
916
+ next: AgentShareDraft,
682
917
  successMessage: string,
683
918
  ) => Promise<boolean>;
684
919
  }) {
@@ -713,6 +948,7 @@ function EmbedTab({
713
948
  disabled={!enabled}
714
949
  multiline
715
950
  />
951
+ {!enabled && <SharingOffHint subject="this embed" />}
716
952
  <p className="text-xs text-muted-foreground">
717
953
  The widget hides itself on sites that aren&apos;t allowed to embed
718
954
  this agent. Free embeds show a &quot;Powered by Stigmer&quot; badge.
@@ -795,10 +1031,10 @@ function OriginsEditor({
795
1031
  isPending,
796
1032
  commit,
797
1033
  }: {
798
- readonly draft: AgentSharingDraft;
1034
+ readonly draft: AgentShareDraft;
799
1035
  readonly isPending: boolean;
800
1036
  readonly commit: (
801
- next: AgentSharingDraft,
1037
+ next: AgentShareDraft,
802
1038
  successMessage: string,
803
1039
  ) => Promise<boolean>;
804
1040
  }) {
@@ -950,17 +1186,15 @@ const MESSAGE_FIELDS = [
950
1186
  },
951
1187
  ] as const;
952
1188
 
953
- type MessageKey = (typeof MESSAGE_FIELDS)[number]["key"];
954
-
955
1189
  function MessagesEditor({
956
1190
  draft,
957
1191
  isPending,
958
1192
  commit,
959
1193
  }: {
960
- readonly draft: AgentSharingDraft;
1194
+ readonly draft: AgentShareDraft;
961
1195
  readonly isPending: boolean;
962
1196
  readonly commit: (
963
- next: AgentSharingDraft,
1197
+ next: AgentShareDraft,
964
1198
  successMessage: string,
965
1199
  ) => Promise<boolean>;
966
1200
  }) {