@stigmer/react 3.1.12 → 3.1.14

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 (63) hide show
  1. package/agent/AgentDetailView.d.ts +11 -3
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +22 -17
  4. package/agent/AgentDetailView.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/AgentShareList.d.ts +44 -0
  11. package/sharing/AgentShareList.d.ts.map +1 -0
  12. package/sharing/AgentShareList.js +149 -0
  13. package/sharing/AgentShareList.js.map +1 -0
  14. package/sharing/ShareAgentDialog.d.ts +38 -15
  15. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  16. package/sharing/ShareAgentDialog.js +135 -57
  17. package/sharing/ShareAgentDialog.js.map +1 -1
  18. package/sharing/index.d.ts +10 -6
  19. package/sharing/index.d.ts.map +1 -1
  20. package/sharing/index.js +5 -3
  21. package/sharing/index.js.map +1 -1
  22. package/sharing/useAgentShares.d.ts +42 -0
  23. package/sharing/useAgentShares.d.ts.map +1 -0
  24. package/sharing/useAgentShares.js +38 -0
  25. package/sharing/useAgentShares.js.map +1 -0
  26. package/sharing/useCanCreateAgentShare.d.ts +43 -0
  27. package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
  28. package/sharing/useCanCreateAgentShare.js +44 -0
  29. package/sharing/useCanCreateAgentShare.js.map +1 -0
  30. package/sharing/useDeleteAgentShare.d.ts +42 -0
  31. package/sharing/useDeleteAgentShare.d.ts.map +1 -0
  32. package/sharing/useDeleteAgentShare.js +45 -0
  33. package/sharing/useDeleteAgentShare.js.map +1 -0
  34. package/sharing/useSaveAgentShare.d.ts +45 -14
  35. package/sharing/useSaveAgentShare.d.ts.map +1 -1
  36. package/sharing/useSaveAgentShare.js +59 -19
  37. package/sharing/useSaveAgentShare.js.map +1 -1
  38. package/src/agent/AgentDetailView.tsx +37 -20
  39. package/src/index.ts +11 -6
  40. package/src/sharing/AgentShareList.tsx +507 -0
  41. package/src/sharing/ShareAgentDialog.tsx +325 -95
  42. package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
  43. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +333 -286
  44. package/src/sharing/__tests__/{useAgentShare.test.tsx → useAgentShares.test.tsx} +47 -51
  45. package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
  46. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +48 -0
  47. package/src/sharing/index.ts +10 -7
  48. package/src/sharing/useAgentShares.ts +68 -0
  49. package/src/sharing/useCanCreateAgentShare.ts +74 -0
  50. package/src/sharing/useDeleteAgentShare.ts +73 -0
  51. package/src/sharing/useSaveAgentShare.ts +77 -19
  52. package/styles.css +1 -1
  53. package/sharing/useAgentShare.d.ts +0 -43
  54. package/sharing/useAgentShare.d.ts.map +0 -1
  55. package/sharing/useAgentShare.js +0 -54
  56. package/sharing/useAgentShare.js.map +0 -1
  57. package/sharing/useShareAgent.d.ts +0 -69
  58. package/sharing/useShareAgent.d.ts.map +0 -1
  59. package/sharing/useShareAgent.js +0 -42
  60. package/sharing/useShareAgent.js.map +0 -1
  61. package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
  62. package/src/sharing/useAgentShare.ts +0 -91
  63. package/src/sharing/useShareAgent.tsx +0 -107
@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
4
4
  import type { GetAgentSharesByAgentRequest } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
5
5
  import { StigmerContext } from "../../context";
6
6
  import { FetchCacheContext } from "../../internal/FetchCacheProvider";
7
- import { useAgentShare } from "../useAgentShare";
7
+ import { useAgentShares } from "../useAgentShares";
8
8
 
9
9
  function createMockStigmer(overrides: {
10
10
  getByAgent?: (input: GetAgentSharesByAgentRequest) => Promise<unknown>;
@@ -30,105 +30,84 @@ function wrapper(client: unknown) {
30
30
  };
31
31
  }
32
32
 
33
- const AGENT = {
34
- metadata: {
35
- id: "agt_1",
36
- org: "acme",
37
- slug: "support-agent",
38
- name: "Support Agent",
39
- },
40
- } as never;
41
-
42
- function makeShare(slug: string, id = `ash_${slug}`) {
33
+ function makeShare(org: string, slug: string, id = `ash_${org}_${slug}`) {
43
34
  return {
44
- metadata: { id, org: "acme", slug },
35
+ metadata: { id, org, slug },
45
36
  spec: { enabled: true },
46
37
  };
47
38
  }
48
39
 
49
- describe("useAgentShare", () => {
40
+ describe("useAgentShares", () => {
50
41
  beforeEach(() => {
51
42
  vi.restoreAllMocks();
52
43
  });
53
44
 
54
45
  it("loads the agent's shares by agent id", async () => {
55
- const share = makeShare("support-agent");
46
+ const share = makeShare("acme", "support-agent");
56
47
  const getByAgent = vi
57
48
  .fn()
58
49
  .mockResolvedValue({ totalCount: 1, items: [share] });
59
50
  const client = createMockStigmer({ getByAgent });
60
51
 
61
- const { result } = renderHook(() => useAgentShare(AGENT), {
52
+ const { result } = renderHook(() => useAgentShares("agt_1"), {
62
53
  wrapper: wrapper(client),
63
54
  });
64
55
 
65
56
  expect(result.current.isLoading).toBe(true);
66
57
  await waitFor(() => expect(result.current.isLoading).toBe(false));
67
58
 
68
- expect(result.current.share).toBe(share);
59
+ expect(result.current.shares).toEqual([share]);
69
60
  expect(result.current.error).toBeNull();
70
61
  expect(getByAgent).toHaveBeenCalledWith(
71
62
  expect.objectContaining({ agentId: "agt_1" }),
72
63
  );
73
64
  });
74
65
 
75
- it("resolves null (no share yet) when the agent has never been shared", async () => {
76
- const client = createMockStigmer();
77
-
78
- const { result } = renderHook(() => useAgentShare(AGENT), {
79
- wrapper: wrapper(client),
80
- });
81
-
82
- await waitFor(() => expect(result.current.isLoading).toBe(false));
83
-
84
- // The no-share state is not an error: the first save creates the share.
85
- expect(result.current.share).toBeNull();
86
- expect(result.current.error).toBeNull();
87
- });
88
-
89
- it("picks the slug-matching share as canonical when several exist", async () => {
90
- const canonical = makeShare("support-agent");
91
- const renamed = makeShare("support-help-desk");
66
+ it("returns the FULL list never collapses to a canonical share", async () => {
67
+ // An agent can carry N shares across N orgs (decision 011 D3 +
68
+ // decision 013): the owner's, a renamed sibling, another org's
69
+ // external channel. The management surface shows them all.
70
+ const owner = makeShare("acme", "support-agent");
71
+ const renamed = makeShare("acme", "support-help-desk");
72
+ const external = makeShare("consumer-org", "support-agent");
92
73
  const getByAgent = vi
93
74
  .fn()
94
- .mockResolvedValue({ totalCount: 2, items: [renamed, canonical] });
75
+ .mockResolvedValue({ totalCount: 3, items: [owner, renamed, external] });
95
76
  const client = createMockStigmer({ getByAgent });
96
77
 
97
- const { result } = renderHook(() => useAgentShare(AGENT), {
78
+ const { result } = renderHook(() => useAgentShares("agt_1"), {
98
79
  wrapper: wrapper(client),
99
80
  });
100
81
 
101
82
  await waitFor(() => expect(result.current.isLoading).toBe(false));
102
- expect(result.current.share).toBe(canonical);
83
+ expect(result.current.shares).toEqual([owner, renamed, external]);
103
84
  });
104
85
 
105
- it("falls back to the first share when none matches the agent slug", async () => {
106
- // A manifest-created share with a custom slug is still manageable —
107
- // preferring it over "no share" keeps the dialog editing the real row.
108
- const renamed = makeShare("support-help-desk");
109
- const getByAgent = vi
110
- .fn()
111
- .mockResolvedValue({ totalCount: 1, items: [renamed] });
112
- const client = createMockStigmer({ getByAgent });
86
+ it("resolves empty (no shares yet) when the agent has never been shared", async () => {
87
+ const client = createMockStigmer();
113
88
 
114
- const { result } = renderHook(() => useAgentShare(AGENT), {
89
+ const { result } = renderHook(() => useAgentShares("agt_1"), {
115
90
  wrapper: wrapper(client),
116
91
  });
117
92
 
118
93
  await waitFor(() => expect(result.current.isLoading).toBe(false));
119
- expect(result.current.share).toBe(renamed);
94
+
95
+ // The no-share state is not an error: creating the first share is
96
+ // the empty state's call to action.
97
+ expect(result.current.shares).toEqual([]);
98
+ expect(result.current.error).toBeNull();
120
99
  });
121
100
 
122
- it("skips fetching while the agent is null (stable no-op)", () => {
101
+ it("skips fetching while the agent id is empty (stable no-op)", () => {
123
102
  const getByAgent = vi.fn();
124
103
  const client = createMockStigmer({ getByAgent });
125
104
 
126
- const { result } = renderHook(() => useAgentShare(null), {
105
+ const { result } = renderHook(() => useAgentShares(""), {
127
106
  wrapper: wrapper(client),
128
107
  });
129
108
 
130
109
  expect(result.current.isLoading).toBe(false);
131
- expect(result.current.share).toBeNull();
110
+ expect(result.current.shares).toEqual([]);
132
111
  expect(getByAgent).not.toHaveBeenCalled();
133
112
  });
134
113
 
@@ -138,12 +117,29 @@ describe("useAgentShare", () => {
138
117
  .mockRejectedValue(new Error("backend unavailable"));
139
118
  const client = createMockStigmer({ getByAgent });
140
119
 
141
- const { result } = renderHook(() => useAgentShare(AGENT), {
120
+ const { result } = renderHook(() => useAgentShares("agt_1"), {
142
121
  wrapper: wrapper(client),
143
122
  });
144
123
 
145
124
  await waitFor(() => expect(result.current.isLoading).toBe(false));
146
125
  expect(result.current.error).toBeTruthy();
147
- expect(result.current.share).toBeNull();
126
+ expect(result.current.shares).toEqual([]);
127
+ });
128
+
129
+ it("refetches on demand", async () => {
130
+ const getByAgent = vi
131
+ .fn()
132
+ .mockResolvedValue({ totalCount: 0, items: [] });
133
+ const client = createMockStigmer({ getByAgent });
134
+
135
+ const { result } = renderHook(() => useAgentShares("agt_1"), {
136
+ wrapper: wrapper(client),
137
+ });
138
+
139
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
140
+ expect(getByAgent).toHaveBeenCalledTimes(1);
141
+
142
+ result.current.refetch();
143
+ await waitFor(() => expect(getByAgent).toHaveBeenCalledTimes(2));
148
144
  });
149
145
  });
@@ -0,0 +1,190 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { renderHook, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
5
+ import { StigmerContext } from "../../context";
6
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
7
+ import { useCanCreateAgentShare } from "../useCanCreateAgentShare";
8
+
9
+ function createMockStigmer(overrides: {
10
+ isAuthorized?: boolean;
11
+ checkMyPermission?: (...args: unknown[]) => Promise<unknown>;
12
+ } = {}) {
13
+ return {
14
+ iamPolicy: {
15
+ checkMyPermission:
16
+ overrides.checkMyPermission ??
17
+ vi.fn().mockResolvedValue({
18
+ isAuthorized: overrides.isAuthorized ?? true,
19
+ }),
20
+ },
21
+ } as never;
22
+ }
23
+
24
+ function wrapper(client: unknown) {
25
+ return function Wrapper({ children }: { children: ReactNode }) {
26
+ return (
27
+ <FetchCacheContext.Provider value={null}>
28
+ <StigmerContext.Provider value={client as never}>
29
+ {children}
30
+ </StigmerContext.Provider>
31
+ </FetchCacheContext.Provider>
32
+ );
33
+ };
34
+ }
35
+
36
+ function makeAgent(visibility?: ApiResourceVisibility) {
37
+ return {
38
+ metadata: {
39
+ id: "agt_1",
40
+ org: "acme",
41
+ slug: "support-agent",
42
+ name: "Support Agent",
43
+ ...(visibility !== undefined && { visibility }),
44
+ },
45
+ spec: {},
46
+ } as never;
47
+ }
48
+
49
+ function permissionCheckOf(client: unknown) {
50
+ return (
51
+ client as { iamPolicy: { checkMyPermission: ReturnType<typeof vi.fn> } }
52
+ ).iamPolicy.checkMyPermission;
53
+ }
54
+
55
+ describe("useCanCreateAgentShare", () => {
56
+ it("is not allowed while the agent is loading — no affordance flash", () => {
57
+ const client = createMockStigmer();
58
+ const { result } = renderHook(() => useCanCreateAgentShare(null, "acme"), {
59
+ wrapper: wrapper(client),
60
+ });
61
+
62
+ expect(result.current.allowed).toBe(false);
63
+ expect(permissionCheckOf(client)).not.toHaveBeenCalled();
64
+ });
65
+
66
+ describe("same-org (viewer org equals the agent's, or omitted)", () => {
67
+ it("requires agent can_edit — the server's Phase A create bar", async () => {
68
+ const client = createMockStigmer({ isAuthorized: true });
69
+ const { result } = renderHook(
70
+ () => useCanCreateAgentShare(makeAgent(), "acme"),
71
+ { wrapper: wrapper(client) },
72
+ );
73
+
74
+ await waitFor(() => expect(result.current.allowed).toBe(true));
75
+ expect(result.current.isCrossOrg).toBe(false);
76
+ expect(result.current.shareOrg).toBe("acme");
77
+
78
+ const input = permissionCheckOf(client).mock.calls[0][0] as {
79
+ resource?: { kind: string; id: string };
80
+ relation: string;
81
+ };
82
+ expect(input.relation).toBe("can_edit");
83
+ expect(input.resource?.kind).toBe("agent");
84
+ expect(input.resource?.id).toBe("agt_1");
85
+ });
86
+
87
+ it("defaults the share org to the agent's own when viewerOrg is omitted", async () => {
88
+ const client = createMockStigmer({ isAuthorized: true });
89
+ const { result } = renderHook(
90
+ () => useCanCreateAgentShare(makeAgent()),
91
+ { wrapper: wrapper(client) },
92
+ );
93
+
94
+ await waitFor(() => expect(result.current.allowed).toBe(true));
95
+ expect(result.current.shareOrg).toBe("acme");
96
+ expect(result.current.isCrossOrg).toBe(false);
97
+ });
98
+
99
+ it("refuses when the viewer lacks can_edit", async () => {
100
+ const client = createMockStigmer({ isAuthorized: false });
101
+ const { result } = renderHook(
102
+ () => useCanCreateAgentShare(makeAgent(), "acme"),
103
+ { wrapper: wrapper(client) },
104
+ );
105
+
106
+ await waitFor(() => expect(permissionCheckOf(client)).toHaveBeenCalled());
107
+ await waitFor(() => expect(result.current.allowed).toBe(false));
108
+ });
109
+ });
110
+
111
+ describe("cross-org (decision 013 D2's two-sided bar)", () => {
112
+ it("allows on a public agent when the viewer holds can_create_agent_share in their org", async () => {
113
+ const client = createMockStigmer({ isAuthorized: true });
114
+ const { result } = renderHook(
115
+ () =>
116
+ useCanCreateAgentShare(
117
+ makeAgent(ApiResourceVisibility.visibility_public),
118
+ "consumer-org",
119
+ ),
120
+ { wrapper: wrapper(client) },
121
+ );
122
+
123
+ await waitFor(() => expect(result.current.allowed).toBe(true));
124
+ expect(result.current.isCrossOrg).toBe(true);
125
+ expect(result.current.shareOrg).toBe("consumer-org");
126
+
127
+ const input = permissionCheckOf(client).mock.calls[0][0] as {
128
+ resource?: { kind: string; id: string };
129
+ relation: string;
130
+ };
131
+ expect(input.relation).toBe("can_create_agent_share");
132
+ expect(input.resource?.kind).toBe("organization");
133
+ // An Organization's id equals its slug (ApiResourceMetadata.id).
134
+ expect(input.resource?.id).toBe("consumer-org");
135
+ });
136
+
137
+ it("refuses on a non-public agent without any permission RPC — visibility IS the consent (D1)", () => {
138
+ const client = createMockStigmer();
139
+ const { result } = renderHook(
140
+ () => useCanCreateAgentShare(makeAgent(), "consumer-org"),
141
+ { wrapper: wrapper(client) },
142
+ );
143
+
144
+ expect(result.current.allowed).toBe(false);
145
+ expect(result.current.isCrossOrg).toBe(true);
146
+ expect(permissionCheckOf(client)).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it("refuses when the viewer lacks can_create_agent_share", async () => {
150
+ const client = createMockStigmer({ isAuthorized: false });
151
+ const { result } = renderHook(
152
+ () =>
153
+ useCanCreateAgentShare(
154
+ makeAgent(ApiResourceVisibility.visibility_public),
155
+ "consumer-org",
156
+ ),
157
+ { wrapper: wrapper(client) },
158
+ );
159
+
160
+ await waitFor(() => expect(permissionCheckOf(client)).toHaveBeenCalled());
161
+ await waitFor(() => expect(result.current.allowed).toBe(false));
162
+ });
163
+
164
+ it("creates in the VIEWER's org even when the viewer could also edit the agent", async () => {
165
+ // The case that produced the old double-entry confusion: a user
166
+ // holding can_edit on a public agent while acting as another org.
167
+ // There is exactly one answer now — the share lands in the org
168
+ // they are acting as, on that org's bill.
169
+ const client = createMockStigmer({ isAuthorized: true });
170
+ const { result } = renderHook(
171
+ () =>
172
+ useCanCreateAgentShare(
173
+ makeAgent(ApiResourceVisibility.visibility_public),
174
+ "personal",
175
+ ),
176
+ { wrapper: wrapper(client) },
177
+ );
178
+
179
+ await waitFor(() => expect(result.current.allowed).toBe(true));
180
+ expect(result.current.shareOrg).toBe("personal");
181
+ expect(result.current.isCrossOrg).toBe(true);
182
+ // Only the org-side bar is consulted — the agent-side can_edit
183
+ // check belongs to the same-org branch alone.
184
+ const relations = permissionCheckOf(client).mock.calls.map(
185
+ (call: unknown[]) => (call[0] as { relation: string }).relation,
186
+ );
187
+ expect(relations).toEqual(["can_create_agent_share"]);
188
+ });
189
+ });
190
+ });
@@ -163,6 +163,54 @@ describe("useSaveAgentShare", () => {
163
163
  expect(second.audience).toBe(AgentShareAudience.org);
164
164
  });
165
165
 
166
+ describe("cross-org create identity (shareOrg — decision 013)", () => {
167
+ it("a first save lands the share in the sharing org, agent_ref stays the agent's", async () => {
168
+ const apply = vi.fn().mockResolvedValue({});
169
+ const client = createMockStigmer({ apply });
170
+
171
+ const { result } = renderHook(
172
+ () => useSaveAgentShare(AGENT, "consumer-org"),
173
+ { wrapper: wrapper(client) },
174
+ );
175
+
176
+ await act(() => result.current.save(FULL_DRAFT, null));
177
+
178
+ const input = apply.mock.calls[0][0] as AgentShareInput;
179
+ // The share is the sharing org's resource (its URL, billing, and
180
+ // credentials), while agent_ref keeps pointing at the provider's
181
+ // blueprint — the whole point of a cross-org share.
182
+ expect(input.org).toBe("consumer-org");
183
+ expect(input.slug).toBe("support-agent");
184
+ expect(input.agentRef).toEqual({ org: "acme", slug: "support-agent" });
185
+ });
186
+
187
+ it("editing an existing cross-org share keeps ITS identity, not the hook argument's", async () => {
188
+ const apply = vi.fn().mockResolvedValue({});
189
+ const client = createMockStigmer({ apply });
190
+ const externalShare = {
191
+ metadata: {
192
+ id: "ash_ext",
193
+ org: "consumer-org",
194
+ slug: "renamed-channel",
195
+ name: "Renamed Channel",
196
+ },
197
+ spec: { enabled: true },
198
+ } as AgentShare;
199
+
200
+ const { result } = renderHook(
201
+ () => useSaveAgentShare(AGENT, "consumer-org"),
202
+ { wrapper: wrapper(client) },
203
+ );
204
+
205
+ await act(() => result.current.save(FULL_DRAFT, externalShare));
206
+
207
+ const input = apply.mock.calls[0][0] as AgentShareInput;
208
+ expect(input.org).toBe("consumer-org");
209
+ expect(input.slug).toBe("renamed-channel");
210
+ expect(input.agentRef).toEqual({ org: "acme", slug: "support-agent" });
211
+ });
212
+ });
213
+
166
214
  it("is a stable no-op when the agent is null", async () => {
167
215
  const apply = vi.fn();
168
216
  const client = createMockStigmer({ apply });
@@ -5,26 +5,29 @@ export type {
5
5
  } from "./useSharedAgentProfile.js";
6
6
  export { SharedAgentChat } from "./SharedAgentChat.js";
7
7
  export type { SharedAgentChatProps } from "./SharedAgentChat.js";
8
- export { useAgentShare } from "./useAgentShare.js";
9
- export type { UseAgentShareReturn } from "./useAgentShare.js";
8
+ export { useAgentShares } from "./useAgentShares.js";
9
+ export type { UseAgentSharesReturn } from "./useAgentShares.js";
10
+ export { useCanCreateAgentShare } from "./useCanCreateAgentShare.js";
11
+ export type { UseCanCreateAgentShareReturn } from "./useCanCreateAgentShare.js";
10
12
  export {
13
+ draftFromShare,
11
14
  sharingAudienceFromProto,
12
15
  useSaveAgentShare,
13
16
  } from "./useSaveAgentShare.js";
14
17
  export type {
18
+ AgentShareCreateIdentity,
15
19
  AgentShareDraft,
16
20
  SharingAudience,
17
21
  UseSaveAgentShareReturn,
18
22
  } from "./useSaveAgentShare.js";
23
+ export { useDeleteAgentShare } from "./useDeleteAgentShare.js";
24
+ export type { UseDeleteAgentShareReturn } from "./useDeleteAgentShare.js";
19
25
  export { useRotateShareLink } from "./useRotateShareLink.js";
20
26
  export type { UseRotateShareLinkReturn } from "./useRotateShareLink.js";
21
27
  export { ShareAgentDialog } from "./ShareAgentDialog.js";
22
28
  export type { ShareAgentDialogProps } from "./ShareAgentDialog.js";
23
- export { useShareAgent } from "./useShareAgent.js";
24
- export type {
25
- UseShareAgentArgs,
26
- UseShareAgentReturn,
27
- } from "./useShareAgent.js";
29
+ export { AgentShareList } from "./AgentShareList.js";
30
+ export type { AgentShareListProps } from "./AgentShareList.js";
28
31
  export { useShareToolReadiness } from "./useShareToolReadiness.js";
29
32
  export type { ShareToolReadiness } from "./useShareToolReadiness.js";
30
33
  // Origin validation moved to @stigmer/sdk (framework-free, shared with the
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ import { create } from "@bufbuild/protobuf";
4
+ import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
5
+ import { GetAgentSharesByAgentRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
6
+ import { useStigmer } from "../hooks.js";
7
+ import { useFetch } from "../internal/useFetch.js";
8
+
9
+ /** Return value of {@link useAgentShares}. */
10
+ export interface UseAgentSharesReturn {
11
+ /**
12
+ * Every share of the agent visible to the caller, in server order.
13
+ * Empty while loading, on error, or when the agent has never been
14
+ * shared — `shares.length === 0 && !isLoading && !error` means "no
15
+ * shares exist yet".
16
+ */
17
+ readonly shares: readonly AgentShare[];
18
+ /** `true` while the initial fetch or a refetch is in flight. */
19
+ readonly isLoading: boolean;
20
+ /** `true` while a background refetch is in flight and stale data is shown. */
21
+ readonly isRefetching: boolean;
22
+ /** Error from the last failed request, or `null` when healthy. */
23
+ readonly error: Error | null;
24
+ /** Discard cached data and re-fetch the shares from the server. */
25
+ readonly refetch: () => void;
26
+ }
27
+
28
+ /**
29
+ * Data hook that loads all of an agent's {@link AgentShare} resources —
30
+ * the channels carrying its hosted-chat configuration (audience, allowed
31
+ * origins, visitor messages, tool credentials, link token).
32
+ *
33
+ * Sharing is channel configuration, not agent behavior (decision 011):
34
+ * an agent can carry N shares, each with its own URL, billing org, and
35
+ * credentials (decision 011 D3 + decision 013 cross-org shares). This
36
+ * hook returns the full list; the server already scopes it to shares the
37
+ * caller can view (FGA-filtered in cloud, unfiltered in the single-user
38
+ * OSS edition), so a viewer sees their own org's channels and never
39
+ * another org's.
40
+ *
41
+ * Pass an empty `agentId` to skip fetching (stable no-op) — useful
42
+ * while the agent is still loading.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * const { shares, isLoading, refetch } = useAgentShares(agent?.metadata?.id ?? "");
47
+ * ```
48
+ */
49
+ export function useAgentShares(agentId: string): UseAgentSharesReturn {
50
+ const stigmer = useStigmer();
51
+
52
+ const fetchFn = agentId
53
+ ? async () => {
54
+ const result = await stigmer.agentShare.getByAgent(
55
+ create(GetAgentSharesByAgentRequestSchema, { agentId }),
56
+ );
57
+ return result.items;
58
+ }
59
+ : null;
60
+
61
+ const { data: shares, isLoading, isRefetching, error, refetch } = useFetch(
62
+ fetchFn,
63
+ [agentId, stigmer],
64
+ [] as AgentShare[],
65
+ );
66
+
67
+ return { shares, isLoading, isRefetching, error, refetch };
68
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
4
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
5
+ import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
6
+
7
+ /** Return value of {@link useCanCreateAgentShare}. */
8
+ export interface UseCanCreateAgentShareReturn {
9
+ /** Whether the viewer may create a share of this agent in `shareOrg`. */
10
+ readonly allowed: boolean;
11
+ /**
12
+ * The org that would own the new share — the viewer's org when set,
13
+ * else the agent's own. The share's URL, billing, and credential
14
+ * bindings all belong to this org.
15
+ */
16
+ readonly shareOrg: string;
17
+ /** Whether creating in `shareOrg` would be a cross-org share (decision 013). */
18
+ readonly isCrossOrg: boolean;
19
+ }
20
+
21
+ /**
22
+ * Mirrors the server's AgentShare create bar so create affordances never
23
+ * appear to a user whose create would be refused:
24
+ *
25
+ * - **Same-org** (`shareOrg` equals the agent's org): agent `can_edit` —
26
+ * the exact permission the create/apply handlers enforce on the
27
+ * referenced agent.
28
+ * - **Cross-org** (decision 013 D2, two-sided): the agent must be
29
+ * `visibility_public` — the origin org's implicit consent (D1), and
30
+ * the client-side proxy for the `can_execute`-via-`allow_public` half
31
+ * of the bar — and the viewer must hold `can_create_agent_share` in
32
+ * their own org (admin-level: a public share spends that org's credits
33
+ * on the open internet). The org check uses the org slug as the
34
+ * resource id — an Organization's id equals its slug
35
+ * (ApiResourceMetadata.id).
36
+ *
37
+ * On the OSS edition {@link useCheckPermission} degrades to allowed
38
+ * (no IAM service), matching the backend's documented no-op
39
+ * authorization (decision 011 D4).
40
+ *
41
+ * Pass `null` for `agent` while it loads — `allowed` stays `false` so
42
+ * no affordance flashes before the gate can be evaluated.
43
+ *
44
+ * @param agent The agent to share, or `null` while loading.
45
+ * @param viewerOrg The viewer's active org. Empty/omitted means the
46
+ * agent's own org (the same-org owner flow).
47
+ */
48
+ export function useCanCreateAgentShare(
49
+ agent: Agent | null,
50
+ viewerOrg?: string,
51
+ ): UseCanCreateAgentShareReturn {
52
+ const agentId = agent?.metadata?.id ?? "";
53
+ const agentOrg = agent?.metadata?.org ?? "";
54
+ const shareOrg = viewerOrg || agentOrg;
55
+ const isCrossOrg = shareOrg !== "" && shareOrg !== agentOrg;
56
+ const isPublic =
57
+ agent?.metadata?.visibility === ApiResourceVisibility.visibility_public;
58
+
59
+ // Both permission checks are declared unconditionally (hook rules);
60
+ // each skips its RPC (null resource) when its branch doesn't apply.
61
+ const { allowed: canEditAgent } = useCheckPermission(
62
+ !isCrossOrg && agentId ? { kind: "agent", id: agentId } : null,
63
+ "can_edit",
64
+ );
65
+ const { allowed: canCreateInOrg } = useCheckPermission(
66
+ isCrossOrg && isPublic ? { kind: "organization", id: shareOrg } : null,
67
+ "can_create_agent_share",
68
+ );
69
+
70
+ const allowed =
71
+ !!agent && (isCrossOrg ? isPublic && canCreateInOrg : canEditAgent);
72
+
73
+ return { allowed, shareOrg, isCrossOrg };
74
+ }
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
5
+ import { useStigmer } from "../hooks.js";
6
+ import { toError } from "../internal/toError.js";
7
+
8
+ /** Return value of {@link useDeleteAgentShare}. */
9
+ export interface UseDeleteAgentShareReturn {
10
+ /**
11
+ * Delete an agent share by ID. Returns the deleted resource.
12
+ *
13
+ * Deleting a share is full teardown (decision 011 D1): its hosted
14
+ * link dies immediately — including for visitors mid-conversation —
15
+ * and its configuration (origins, messages, credential bindings, link
16
+ * token) is gone. To stop serving while keeping the configuration,
17
+ * save the share with `enabled: false` instead (a config-preserving
18
+ * pause via {@link useSaveAgentShare}).
19
+ */
20
+ readonly deleteShare: (id: string) => Promise<AgentShare>;
21
+ /** `true` while the delete request is in flight. */
22
+ readonly isDeleting: boolean;
23
+ /** Error from the last failed delete, or `null` when healthy. */
24
+ readonly error: Error | null;
25
+ /** Reset `error` to `null`. */
26
+ readonly clearError: () => void;
27
+ }
28
+
29
+ /**
30
+ * Mutation hook that deletes an {@link AgentShare} resource.
31
+ *
32
+ * Wraps `stigmer.agentShare.delete()` (authorized by the share's
33
+ * `can_delete`) with loading/error state. The caller is responsible for
34
+ * post-delete UI updates (e.g. refreshing the share list) and for
35
+ * confirming the destructive action — the delete-vs-pause distinction
36
+ * belongs in the confirmation copy.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * const { deleteShare, isDeleting } = useDeleteAgentShare();
41
+ *
42
+ * const handleDelete = async () => {
43
+ * await deleteShare(share.metadata.id);
44
+ * refetch(); // refresh the share list
45
+ * };
46
+ * ```
47
+ */
48
+ export function useDeleteAgentShare(): UseDeleteAgentShareReturn {
49
+ const stigmer = useStigmer();
50
+ const [isDeleting, setIsDeleting] = useState(false);
51
+ const [error, setError] = useState<Error | null>(null);
52
+
53
+ const clearError = useCallback(() => setError(null), []);
54
+
55
+ const deleteShare = useCallback(
56
+ async (id: string): Promise<AgentShare> => {
57
+ setIsDeleting(true);
58
+ setError(null);
59
+
60
+ try {
61
+ return await stigmer.agentShare.delete(id);
62
+ } catch (err) {
63
+ setError(toError(err));
64
+ throw err;
65
+ } finally {
66
+ setIsDeleting(false);
67
+ }
68
+ },
69
+ [stigmer],
70
+ );
71
+
72
+ return { deleteShare, isDeleting, error, clearError };
73
+ }