@datalayer/core 1.1.66 → 1.2.10

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 (210) hide show
  1. package/README.md +41 -18
  2. package/lib/api/constants.d.ts +1 -1
  3. package/lib/api/constants.js +1 -1
  4. package/lib/api/contents/attachments.js +3 -7
  5. package/lib/api/contents/datasets.d.ts +48 -0
  6. package/lib/api/contents/datasets.js +44 -7
  7. package/lib/api/contents/runtimeMounts.js +1 -1
  8. package/lib/api/contents/sandboxUid.d.ts +13 -45
  9. package/lib/api/contents/sandboxUid.js +14 -65
  10. package/lib/api/evals/client.d.ts +402 -0
  11. package/lib/api/evals/client.js +359 -0
  12. package/lib/api/evals/index.d.ts +10 -0
  13. package/lib/api/evals/index.js +18 -0
  14. package/lib/api/evals/request.d.ts +43 -0
  15. package/lib/api/evals/request.js +67 -0
  16. package/lib/api/evals/status.d.ts +27 -0
  17. package/lib/api/evals/status.js +122 -0
  18. package/lib/api/evals/types.d.ts +1335 -0
  19. package/lib/api/evals/types.js +5 -0
  20. package/lib/api/iam/connectedAgents.d.ts +29 -0
  21. package/lib/api/iam/connectedAgents.js +35 -0
  22. package/lib/api/iam/identityProviders.d.ts +76 -0
  23. package/lib/api/iam/identityProviders.js +187 -0
  24. package/lib/api/iam/index.d.ts +1 -0
  25. package/lib/api/iam/index.js +1 -0
  26. package/lib/api/iam/mcpPolicy.d.ts +102 -1
  27. package/lib/api/iam/mcpPolicy.js +38 -2
  28. package/lib/api/iam/profile.d.ts +17 -1
  29. package/lib/api/iam/profile.js +27 -0
  30. package/lib/api/iam/scimTokens.d.ts +79 -0
  31. package/lib/api/iam/scimTokens.js +141 -0
  32. package/lib/api/iam/secrets.js +9 -21
  33. package/lib/api/iam/trials.d.ts +38 -0
  34. package/lib/api/iam/trials.js +66 -0
  35. package/lib/api/index.d.ts +4 -0
  36. package/lib/api/index.js +4 -0
  37. package/lib/api/mcp/observability.d.ts +25 -9
  38. package/lib/api/mcp/observability.js +63 -50
  39. package/lib/api/orchestration/client.d.ts +53 -0
  40. package/lib/api/orchestration/client.js +105 -0
  41. package/lib/api/orchestration/events.d.ts +88 -0
  42. package/lib/api/orchestration/events.js +245 -0
  43. package/lib/api/orchestration/generated.d.ts +693 -0
  44. package/lib/api/orchestration/generated.js +687 -0
  45. package/lib/api/orchestration/index.d.ts +30 -0
  46. package/lib/api/orchestration/index.js +34 -0
  47. package/lib/api/orchestration/lifecycle.d.ts +39 -0
  48. package/lib/api/orchestration/lifecycle.js +57 -0
  49. package/lib/api/orchestration/measures.d.ts +55 -0
  50. package/lib/api/orchestration/measures.js +99 -0
  51. package/lib/api/otel/dashboards.d.ts +56 -0
  52. package/lib/api/otel/dashboards.js +50 -0
  53. package/lib/api/otel/index.d.ts +1 -0
  54. package/lib/api/otel/index.js +1 -0
  55. package/lib/api/otel/metrics.d.ts +16 -0
  56. package/lib/api/otel/metrics.js +15 -0
  57. package/lib/api/scheduler/client.d.ts +32 -0
  58. package/lib/api/scheduler/client.js +42 -0
  59. package/lib/api/scheduler/index.d.ts +8 -0
  60. package/lib/api/scheduler/index.js +16 -0
  61. package/lib/api/scheduler/request.d.ts +28 -0
  62. package/lib/api/scheduler/request.js +50 -0
  63. package/lib/api/scheduler/types.d.ts +88 -0
  64. package/lib/api/scheduler/types.js +5 -0
  65. package/lib/api/spacer/comments.d.ts +97 -0
  66. package/lib/api/spacer/comments.js +45 -0
  67. package/lib/api/spacer/index.d.ts +9 -0
  68. package/lib/api/spacer/index.js +17 -0
  69. package/lib/api/spacer/notebooks.d.ts +33 -0
  70. package/lib/api/spacer/notebooks.js +39 -0
  71. package/lib/api/spacer/request.d.ts +25 -0
  72. package/lib/api/spacer/request.js +46 -0
  73. package/lib/api/spacer/spaces.d.ts +68 -0
  74. package/lib/api/spacer/spaces.js +45 -0
  75. package/lib/api/utils/validation.d.ts +4 -4
  76. package/lib/api/utils/validation.js +10 -7
  77. package/lib/client/constants.d.ts +1 -0
  78. package/lib/client/constants.js +1 -0
  79. package/lib/components/animation/AnimatedText.d.ts +1 -44
  80. package/lib/components/animation/AnimatedText.js +6 -122
  81. package/lib/components/anonymous/AnonymousKeyExpired.d.ts +56 -0
  82. package/lib/components/anonymous/AnonymousKeyExpired.js +96 -0
  83. package/lib/components/anonymous/AnonymousKeyTimer.d.ts +63 -0
  84. package/lib/components/anonymous/AnonymousKeyTimer.js +140 -0
  85. package/lib/components/anonymous/index.d.ts +7 -0
  86. package/lib/components/anonymous/index.js +15 -0
  87. package/lib/components/avatars/UserAvatar.d.ts +7 -1
  88. package/lib/components/avatars/UserAvatar.js +15 -1
  89. package/lib/components/billing/BillingEntitySelect.d.ts +6 -0
  90. package/lib/components/billing/BillingEntitySelect.js +49 -10
  91. package/lib/components/billing/eligibility.d.ts +22 -0
  92. package/lib/components/billing/eligibility.js +27 -0
  93. package/lib/components/billing/index.d.ts +1 -0
  94. package/lib/components/billing/index.js +1 -0
  95. package/lib/components/collaboration/LiveEditorCollaborators.d.ts +5 -0
  96. package/lib/components/collaboration/LiveEditorCollaborators.js +3 -1
  97. package/lib/components/display/DatalayerBox.d.ts +2 -3
  98. package/lib/components/display/DatalayerBox.js +1 -1
  99. package/lib/components/display/NavLink.js +3 -2
  100. package/lib/components/labels/StatusLabels.d.ts +2 -10
  101. package/lib/components/labels/StatusLabels.js +2 -26
  102. package/lib/components/principal/PrincipalAppearance.d.ts +21 -42
  103. package/lib/components/principal/PrincipalAppearance.js +232 -174
  104. package/lib/components/principal/PrincipalAvatar.d.ts +8 -1
  105. package/lib/components/principal/PrincipalAvatar.js +2 -2
  106. package/lib/components/principal/PrincipalDetailsOverlay.d.ts +8 -1
  107. package/lib/components/principal/PrincipalDetailsOverlay.js +2 -2
  108. package/lib/components/screencapture/Screencapture.js +46 -3
  109. package/lib/components/sharing/ShareAccessComponent.d.ts +45 -4
  110. package/lib/components/sharing/ShareAccessComponent.js +110 -78
  111. package/lib/components/sharing/SharingEditor.d.ts +5 -1
  112. package/lib/components/sharing/SharingEditor.js +9 -4
  113. package/lib/components/sharing/index.d.ts +1 -0
  114. package/lib/components/sharing/index.js +1 -0
  115. package/lib/components/sharing/sandboxSharing.d.ts +26 -0
  116. package/lib/components/sharing/sandboxSharing.js +41 -0
  117. package/lib/components/spaces/SpaceDetailsOverlay.d.ts +39 -0
  118. package/lib/components/spaces/SpaceDetailsOverlay.js +80 -0
  119. package/lib/components/spaces/SpaceDisplay.d.ts +38 -0
  120. package/lib/components/spaces/SpaceDisplay.js +92 -0
  121. package/lib/components/spaces/index.d.ts +4 -0
  122. package/lib/components/spaces/index.js +7 -0
  123. package/lib/components/spaces/spaceDisplayModel.d.ts +93 -0
  124. package/lib/components/spaces/spaceDisplayModel.js +170 -0
  125. package/lib/hooks/cacheConverters.d.ts +0 -7
  126. package/lib/hooks/cacheConverters.js +0 -25
  127. package/lib/hooks/index.d.ts +1 -0
  128. package/lib/hooks/index.js +1 -0
  129. package/lib/hooks/useBillingEntityStore.js +3 -0
  130. package/lib/hooks/useCache.d.ts +168 -25
  131. package/lib/hooks/useCache.js +1424 -307
  132. package/lib/hooks/useContents.d.ts +22 -1
  133. package/lib/hooks/useContents.js +90 -21
  134. package/lib/hooks/useMcp.d.ts +87 -1
  135. package/lib/hooks/useMcp.js +247 -12
  136. package/lib/hooks/useNavigate.d.ts +41 -3
  137. package/lib/hooks/useNavigate.js +106 -52
  138. package/lib/hooks/usePrincipalCacheStore.js +3 -0
  139. package/lib/hooks/usePrincipalStore.js +3 -0
  140. package/lib/hooks/useSpaceCacheStore.d.ts +50 -0
  141. package/lib/hooks/useSpaceCacheStore.js +85 -0
  142. package/lib/models/CreditsDTO.d.ts +6 -5
  143. package/lib/models/CreditsDTO.js +7 -5
  144. package/lib/models/McpBinding.d.ts +6 -0
  145. package/lib/models/Organization.d.ts +69 -0
  146. package/lib/models/Organization.js +96 -0
  147. package/lib/models/Secret.d.ts +1 -2
  148. package/lib/models/Secret.js +2 -18
  149. package/lib/models/StartedBy.d.ts +34 -0
  150. package/lib/models/StartedBy.js +36 -0
  151. package/lib/models/Team.d.ts +2 -0
  152. package/lib/models/Team.js +2 -0
  153. package/lib/models/User.d.ts +4 -0
  154. package/lib/models/User.js +3 -0
  155. package/lib/models/UserSettings.d.ts +13 -0
  156. package/lib/models/UserSettings.js +9 -0
  157. package/lib/models/contents/__fixtures__/v1-contracts.json +158 -30
  158. package/lib/navigation/components.js +2 -3
  159. package/lib/routes/publicPaths.js +4 -0
  160. package/lib/state/index.d.ts +1 -0
  161. package/lib/state/index.js +1 -0
  162. package/lib/state/sessionEnd.d.ts +55 -0
  163. package/lib/state/sessionEnd.js +162 -0
  164. package/lib/state/substates/CoreState.js +3 -3
  165. package/lib/state/substates/IAMState.js +7 -0
  166. package/lib/state/substates/LayoutState.js +12 -0
  167. package/lib/state/substates/ProfileState.d.ts +1 -0
  168. package/lib/state/substates/ProfileState.js +1 -0
  169. package/lib/utils/Lazy.d.ts +20 -0
  170. package/lib/utils/Lazy.js +32 -6
  171. package/lib/utils/Screencapture.js +3 -0
  172. package/lib/utils/WithSuspense.d.ts +11 -1
  173. package/lib/utils/WithSuspense.js +11 -1
  174. package/lib/views/mcp/AdmittedClients.d.ts +44 -0
  175. package/lib/views/mcp/AdmittedClients.js +59 -0
  176. package/lib/views/mcp/ApprovalQueue.d.ts +66 -0
  177. package/lib/views/mcp/ApprovalQueue.js +95 -0
  178. package/lib/views/mcp/ConnectedAgents.js +38 -3
  179. package/lib/views/mcp/EnterpriseConsole.d.ts +1 -1
  180. package/lib/views/mcp/EnterpriseConsole.js +23 -1
  181. package/lib/views/mcp/IdentityProviders.d.ts +33 -0
  182. package/lib/views/mcp/IdentityProviders.js +299 -0
  183. package/lib/views/mcp/McpDashboard.js +90 -7
  184. package/lib/views/mcp/McpObservability.d.ts +30 -1
  185. package/lib/views/mcp/McpObservability.js +105 -19
  186. package/lib/views/mcp/NotebookRuns.d.ts +39 -0
  187. package/lib/views/mcp/NotebookRuns.js +44 -0
  188. package/lib/views/mcp/OrganizationPolicy.js +2 -1
  189. package/lib/views/mcp/PersonalPolicy.js +2 -1
  190. package/lib/views/mcp/PolicyForm.d.ts +17 -1
  191. package/lib/views/mcp/PolicyForm.js +25 -3
  192. package/lib/views/mcp/PolicyHistory.d.ts +13 -0
  193. package/lib/views/mcp/PolicyHistory.js +14 -1
  194. package/lib/views/mcp/RunDetail.d.ts +87 -0
  195. package/lib/views/mcp/RunDetail.js +186 -0
  196. package/lib/views/mcp/ScimProvisioning.d.ts +54 -0
  197. package/lib/views/mcp/ScimProvisioning.js +173 -0
  198. package/lib/views/mcp/TeamPolicies.js +66 -5
  199. package/lib/views/mcp/ToolAccess.d.ts +60 -0
  200. package/lib/views/mcp/ToolAccess.js +101 -0
  201. package/lib/views/mcp/TraceTimeline.d.ts +81 -0
  202. package/lib/views/mcp/TraceTimeline.js +132 -0
  203. package/lib/views/mcp/index.d.ts +5 -0
  204. package/lib/views/mcp/index.js +5 -0
  205. package/lib/views/otel/simpleAuthStore.js +3 -0
  206. package/lib/views/secrets/SecretEdit.js +3 -2
  207. package/lib/views/secrets/SecretNew.js +5 -1
  208. package/package.json +269 -264
  209. package/scripts/generate-mcp-types.py +1 -1
  210. package/scripts/generate-orchestration-types.py +920 -0
@@ -0,0 +1,186 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button, Heading, Label, Link, Spinner, Text, Textarea, RelativeTime, } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import { McpErrorBlankslate } from '../../components/mcp/index.js';
7
+ import { useAnswerTask, useCancelTask, useRunTrace, useTask, useTaskEvents, } from '../../hooks/useMcp.js';
8
+ import { TraceTimelineSection } from './TraceTimeline.js';
9
+ /** How a run's status is drawn. Shared with the runs list. */
10
+ export const RUN_STATUS_LOOK = {
11
+ working: { label: 'Working', variant: 'success' },
12
+ input_required: { label: 'Waiting on you', variant: 'attention' },
13
+ completed: { label: 'Completed', variant: 'secondary' },
14
+ failed: { label: 'Failed', variant: 'danger' },
15
+ cancelled: { label: 'Cancelled', variant: 'secondary' },
16
+ };
17
+ /** Whether the run has stopped for good. */
18
+ export const isRunOver = (status) => status === 'completed' || status === 'failed' || status === 'cancelled';
19
+ /**
20
+ * Everything the run has printed, as text.
21
+ *
22
+ * Two shapes reach here and both are the run's output: the streamed
23
+ * `outputs`, and the `result` a finished tool returned. A cancelled run has
24
+ * the first and usually the second — the partial output is promoted to the
25
+ * result when it stops — so they are concatenated rather than chosen
26
+ * between, and de-duplicated by text so a promoted partial is not printed
27
+ * twice.
28
+ */
29
+ export const outputTextOf = (task) => {
30
+ const lines = [];
31
+ for (const output of task.outputs ?? []) {
32
+ if (output.text) {
33
+ lines.push(output.text);
34
+ }
35
+ }
36
+ const result = task.result;
37
+ if (result && typeof result === 'object') {
38
+ for (const part of result.content ?? []) {
39
+ const text = part.text;
40
+ if (typeof text === 'string') {
41
+ lines.push(text);
42
+ }
43
+ }
44
+ }
45
+ const seen = new Set();
46
+ return lines
47
+ .filter(line => {
48
+ if (seen.has(line)) {
49
+ return false;
50
+ }
51
+ seen.add(line);
52
+ return true;
53
+ })
54
+ .join('');
55
+ };
56
+ /**
57
+ * A run for the documentation to draw: cancelled mid-flight, holding the
58
+ * lines it printed before it stopped.
59
+ *
60
+ * Cancelled rather than completed on purpose. It is the state that shows
61
+ * both halves of what this view is for — the output a run keeps when it is
62
+ * stopped, and the reason it stopped — and it is the one a reader is most
63
+ * likely to be looking at when they open the documentation.
64
+ */
65
+ export const SAMPLE_RUN = {
66
+ uid: 'tsk_0980147b852244d098b04efd',
67
+ status: 'cancelled',
68
+ statusMessage: 'cancelled by the client',
69
+ tool: 'execute_cell',
70
+ notebookUid: '01KW96PZYRVKZXT8FS644GFD0D',
71
+ cellId: 'fdcac494',
72
+ initiatingUser: '01JV1VE1T5VG22Z05F6EFBMW8E',
73
+ createdAt: '2026-09-07T12:00:00Z',
74
+ lastUpdatedAt: '2026-09-07T12:00:22Z',
75
+ outputs: [
76
+ {
77
+ index: 0,
78
+ outputType: 'stream',
79
+ text: 'tick 0\ntick 1\ntick 2\ntick 3\ntick 4\ntick 5\n',
80
+ },
81
+ ],
82
+ result: {
83
+ content: [{ type: 'text', text: 'tick 6\ntick 7\nKeyboardInterrupt\n' }],
84
+ },
85
+ };
86
+ /**
87
+ * The spans that run left behind, for the documentation to draw.
88
+ *
89
+ * The shape is the one a reader needs to recognise: the gateway's twenty-two
90
+ * seconds are almost entirely the sandbox's, and the durable step between
91
+ * them explains the gap. A timeline drawn from an empty trace would teach
92
+ * nobody what the timeline is for.
93
+ */
94
+ export const SAMPLE_TRACE = (() => {
95
+ const at = (second, millisecond = 0) => new Date(Date.UTC(2026, 8, 7, 12, 0, second, millisecond)).toISOString();
96
+ const span = (id, name, service, from, to) => ({
97
+ trace_id: '4f7a1c9d2b8e40a5b6c3d1e0f9a8b7c6',
98
+ span_id: id,
99
+ span_name: name,
100
+ service_name: service,
101
+ kind: 'SPAN_KIND_INTERNAL',
102
+ start_time: from,
103
+ end_time: to,
104
+ duration_ms: Date.parse(to) - Date.parse(from),
105
+ });
106
+ return [
107
+ {
108
+ span: span('01', 'mcp.tools/call execute_cell', 'jupyter-mcp-server', at(0), at(22)),
109
+ children: [
110
+ {
111
+ span: span('02', 'mcp.authorize', 'jupyter-mcp-server', at(0), at(0, 4)),
112
+ children: [],
113
+ },
114
+ {
115
+ span: span('03', 'durable.workflow.start', 'datalayer-durable', at(0, 120), at(0, 480)),
116
+ children: [
117
+ {
118
+ span: span('04', 'durable.step.execute', 'datalayer-durable', at(0, 500), at(22)),
119
+ children: [
120
+ {
121
+ span: span('05', 'runtimes.kernel.execute', 'datalayer-runtimes', at(1, 200), at(21, 900)),
122
+ children: [],
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ },
128
+ ],
129
+ },
130
+ ];
131
+ })();
132
+ export const RunDetail = ({ taskUid, errorState, routes, mock = false, }) => {
133
+ const navigate = useNavigate();
134
+ const run = useTask(mock ? undefined : taskUid);
135
+ const cancel = useCancelTask();
136
+ const answer = useAnswerTask();
137
+ // Asked for whatever the run's own trace holds; the hook itself stops
138
+ // polling when the run reaches a terminal state, and asks for nothing at
139
+ // all in `mock`.
140
+ const trace = useRunTrace(mock ? undefined : taskUid, { enabled: !mock });
141
+ const [reply, setReply] = useState('');
142
+ // Live while it runs, and it ends itself when the run is over. Never for
143
+ // the documentation's copy: it names a run that does not exist.
144
+ useTaskEvents(taskUid, {
145
+ enabled: !mock && Boolean(run.data) && !isRunOver(run.data.status),
146
+ });
147
+ if (!mock && run.isError) {
148
+ return (_jsx(McpErrorBlankslate, { state: errorState(run.error, 'Run'), onRetry: () => run.refetch() }));
149
+ }
150
+ if (!mock && (run.isLoading || !run.data)) {
151
+ return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 4 }, children: _jsx(Spinner, {}) }));
152
+ }
153
+ const task = mock ? SAMPLE_RUN : run.data;
154
+ const look = RUN_STATUS_LOOK[task.status];
155
+ const over = isRunOver(task.status);
156
+ const output = outputTextOf(task);
157
+ return (_jsxs(Box, { sx: { display: 'grid', gap: 3, minWidth: 0 }, children: [_jsxs(Box, { children: [_jsx(Link, { href: routes.runs, onClick: event => {
158
+ event.preventDefault();
159
+ navigate(routes.runs);
160
+ }, sx: { fontSize: 0 }, children: "All runs" }), _jsx(Heading, { as: "h2", sx: { fontSize: 3, mt: 1, mb: 1 }, children: task.tool || 'Run' }), _jsxs(Box, { sx: {
161
+ display: 'flex',
162
+ gap: 2,
163
+ alignItems: 'center',
164
+ flexWrap: 'wrap',
165
+ }, children: [_jsx(Label, { size: "small", variant: look.variant, children: look.label }), _jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0, color: 'fg.muted' }, children: task.uid }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["updated ", _jsx(RelativeTime, { datetime: task.lastUpdatedAt })] })] }), task.statusMessage && (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', mt: 2 }, children: task.statusMessage }))] }), task.status === 'input_required' && (_jsxs(Box, { sx: {
166
+ borderWidth: 1,
167
+ borderStyle: 'solid',
168
+ borderColor: 'attention.emphasis',
169
+ borderRadius: 2,
170
+ p: 3,
171
+ display: 'grid',
172
+ gap: 2,
173
+ }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 1 }, children: "This run is waiting on you" }), _jsx(Textarea, { "aria-label": "Your answer", value: reply, onChange: event => setReply(event.target.value), placeholder: "Your answer", rows: 3 }), _jsx(Box, { children: _jsx(Button, { variant: "primary", disabled: !reply.trim() || answer.isPending, onClick: () => answer.mutate({ taskUid: task.uid, input: { answer: reply } }, { onSuccess: () => setReply('') }), children: answer.isPending ? 'Answering…' : 'Answer' }) })] })), _jsxs(Box, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 1, mb: 2 }, children: "Output" }), output ? (_jsx(Box, { as: "pre", sx: {
174
+ fontFamily: 'mono',
175
+ fontSize: 0,
176
+ whiteSpace: 'pre-wrap',
177
+ overflowX: 'auto',
178
+ bg: 'canvas.subtle',
179
+ borderRadius: 2,
180
+ p: 3,
181
+ m: 0,
182
+ }, children: output })) : (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: over
183
+ ? 'This run printed nothing.'
184
+ : 'Nothing printed yet. Output appears here as it arrives.' }))] }), task.error && (_jsxs(Box, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 1, mb: 2 }, children: "Why it failed" }), _jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0, color: 'danger.fg' }, children: task.error })] })), _jsx(TraceTimelineSection, { tree: mock ? SAMPLE_TRACE : (trace.data?.tree ?? []), traceId: mock ? SAMPLE_TRACE[0].span.trace_id : trace.data?.traceId, live: !over }), !over && (_jsxs(Box, { children: [_jsx(Button, { variant: "danger", disabled: cancel.isPending, onClick: () => cancel.mutate(task.uid), children: cancel.isPending ? 'Cancelling…' : 'Cancel this run' }), _jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 2 }, children: "Cancelling interrupts the work itself, so the sandbox stops being billed for it. What it has already printed is kept." })] }))] }));
185
+ };
186
+ export default RunDetail;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The directory's other half: who exists, and who has left.
3
+ *
4
+ * The identity provider above signs people in. It can only ever act on
5
+ * somebody who *arrives* — which makes it useless for the thing an
6
+ * organization actually needs from a directory, because the person who left
7
+ * is exactly the person who does not turn up. That is what SCIM is for, and
8
+ * why this sits under the same page rather than somewhere else.
9
+ *
10
+ * **The status line is the point of this view, not the table.** A table of
11
+ * tokens looks almost identical in three quite different situations: a live
12
+ * integration; one whose every token has been revoked; and one configured
13
+ * months ago that has never once been called. In the last two, nobody is
14
+ * being deprovisioned — and an administrator reading rows would have to
15
+ * notice an empty "last used" column to know it. So the state is computed
16
+ * (`scimStatus`) and said in a sentence, in the colour that matches.
17
+ *
18
+ * **The secret is shown once.** Only a SHA-256 of it is stored, so there is
19
+ * no call that could show it again and this view offers none. The dialog
20
+ * says so before the value, not after it.
21
+ *
22
+ * @module views/mcp/ScimProvisioning
23
+ */
24
+ import type { JSX } from 'react';
25
+ import { scimStatus } from '../../api/iam/scimTokens';
26
+ import type { McpErrorStateFn } from './types';
27
+ export interface ScimProvisioningProps {
28
+ errorState: McpErrorStateFn;
29
+ orgUid: string;
30
+ /**
31
+ * Read-only for anybody who is not an owner. A SCIM token *is* the
32
+ * organization's provisioning authority — whoever holds one can remove
33
+ * anybody from it.
34
+ */
35
+ readOnly?: boolean;
36
+ showTitle?: boolean;
37
+ }
38
+ /**
39
+ * What the status line says, and how alarming it looks.
40
+ *
41
+ * Exported because these sentences *are* the feature. The table below them
42
+ * is nearly identical in all four states, and the whole reason this view
43
+ * exists is that "a SCIM token is configured" and "people are actually being
44
+ * removed" are different facts. A message that reassured in a state where
45
+ * nobody is being deprovisioned would be worse than no view at all, so it is
46
+ * held by a test rather than by care taken while writing it.
47
+ */
48
+ export declare const SCIM_STATUS_MESSAGES: Record<ReturnType<typeof scimStatus>['state'], {
49
+ variant: 'default' | 'success' | 'warning';
50
+ heading: string;
51
+ body: string;
52
+ }>;
53
+ export declare const ScimProvisioning: ({ errorState, orgUid, readOnly, showTitle, }: ScimProvisioningProps) => JSX.Element;
54
+ export default ScimProvisioning;
@@ -0,0 +1,173 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useRef, useState } from 'react';
3
+ import { ActionList, ActionMenu, Button, Flash, FormControl, Heading, IconButton, Label, Spinner, Text, TextInput, } from '@primer/react';
4
+ import { Blankslate, DataTable, Dialog, Table, } from '@primer/react/experimental';
5
+ import { Box } from '@datalayer/primer-addons';
6
+ import { KebabHorizontalIcon, PeopleIcon } from '@primer/octicons-react';
7
+ import { McpErrorBlankslate } from '../../components/mcp/index.js';
8
+ import { useCoreStore } from '../../state/index.js';
9
+ import { useToast } from '../../hooks/index.js';
10
+ import { useCreateScimToken, useRevokeScimToken, useRotateScimToken, useScimTokens, } from '../../hooks/useMcp.js';
11
+ import { scimBaseUrl, scimStatus } from '../../api/iam/scimTokens.js';
12
+ import { timeAgo } from './format.js';
13
+ /**
14
+ * What the status line says, and how alarming it looks.
15
+ *
16
+ * Exported because these sentences *are* the feature. The table below them
17
+ * is nearly identical in all four states, and the whole reason this view
18
+ * exists is that "a SCIM token is configured" and "people are actually being
19
+ * removed" are different facts. A message that reassured in a state where
20
+ * nobody is being deprovisioned would be worse than no view at all, so it is
21
+ * held by a test rather than by care taken while writing it.
22
+ */
23
+ export const SCIM_STATUS_MESSAGES = {
24
+ none: {
25
+ variant: 'default',
26
+ heading: 'No directory is provisioning into this organization',
27
+ body: 'People are added and removed by hand. Issue a token to let the ' +
28
+ 'directory do it — and, more importantly, to have it remove people ' +
29
+ 'when they leave.',
30
+ },
31
+ revoked: {
32
+ variant: 'warning',
33
+ heading: 'Every SCIM token is revoked — nobody is being deprovisioned',
34
+ body: 'The directory can no longer reach this organization. Anybody it ' +
35
+ 'removes stays a member here until somebody does it by hand. Rotate a ' +
36
+ 'token, or issue a new one, and reconfigure the directory with it.',
37
+ },
38
+ 'never-used': {
39
+ variant: 'warning',
40
+ heading: 'A token exists and the directory has never used it',
41
+ body: 'Nothing has been provisioned or deprovisioned through it. Either the ' +
42
+ 'directory has not been configured with it yet, or it was configured ' +
43
+ 'with a token that has since been rotated.',
44
+ },
45
+ active: {
46
+ variant: 'success',
47
+ heading: 'The directory is provisioning into this organization',
48
+ body: 'People it adds become members; people it removes lose their ' +
49
+ 'membership and, with it, every agent grant this organization gave ' +
50
+ 'them.',
51
+ },
52
+ };
53
+ export const ScimProvisioning = ({ errorState, orgUid, readOnly = false, showTitle = true, }) => {
54
+ const { enqueueToast } = useToast();
55
+ const iamUrl = useCoreStore(state => state.configuration.iamUrl);
56
+ const tokens = useScimTokens(orgUid);
57
+ const create = useCreateScimToken(orgUid);
58
+ const rotate = useRotateScimToken(orgUid);
59
+ const revoke = useRevokeScimToken(orgUid);
60
+ const [creating, setCreating] = useState(false);
61
+ const [name, setName] = useState('');
62
+ /** The one moment a secret exists in this process. Cleared on close. */
63
+ const [secret, setSecret] = useState('');
64
+ // Primer's Dialog wants a non-nullable `RefObject<HTMLElement>`. React
65
+ // 19's `useRef(null)` is nullable, and this ref only ever reaches Dialog,
66
+ // so narrow it once here — the same way `IdentityProviders` does.
67
+ const returnFocusRef = useRef(null);
68
+ const rows = useMemo(() => (tokens.data ?? []).map(entry => ({ ...entry, id: entry.uid })), [tokens.data]);
69
+ const status = useMemo(() => scimStatus(tokens.data ?? []), [tokens.data]);
70
+ const closeSecret = () => setSecret('');
71
+ const openCreate = () => {
72
+ setName('');
73
+ setCreating(true);
74
+ };
75
+ const submitCreate = async () => {
76
+ try {
77
+ const minted = await create.mutateAsync(name.trim() || 'SCIM');
78
+ setCreating(false);
79
+ // Straight into the one dialog that will ever hold it.
80
+ setSecret(minted.secret);
81
+ }
82
+ catch (error) {
83
+ enqueueToast(error instanceof Error
84
+ ? error.message
85
+ : 'The token could not be created.', { variant: 'error' });
86
+ }
87
+ };
88
+ const doRotate = async (uid) => {
89
+ try {
90
+ const minted = await rotate.mutateAsync(uid);
91
+ setSecret(minted.secret);
92
+ }
93
+ catch (error) {
94
+ enqueueToast(error instanceof Error
95
+ ? error.message
96
+ : 'The token could not be rotated.', { variant: 'error' });
97
+ }
98
+ };
99
+ const doRevoke = async (uid) => {
100
+ try {
101
+ await revoke.mutateAsync(uid);
102
+ enqueueToast('SCIM token revoked.', { variant: 'success' });
103
+ }
104
+ catch (error) {
105
+ enqueueToast(error instanceof Error
106
+ ? error.message
107
+ : 'The token could not be revoked.', { variant: 'error' });
108
+ }
109
+ };
110
+ const columns = [
111
+ {
112
+ header: 'Name',
113
+ field: 'name',
114
+ rowHeader: true,
115
+ renderCell: row => (_jsxs(Box, { sx: { display: 'grid', gap: 1 }, children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: row.name || 'SCIM' }), row.revoked && (_jsx(Label, { variant: "danger", size: "small", children: "Revoked" }))] })),
116
+ },
117
+ {
118
+ header: 'Last used',
119
+ field: 'lastUsedAt',
120
+ renderCell: row => row.revoked ? (_jsx(Text, { sx: { color: 'fg.muted' }, children: "\u2014" })) : row.lastUsedAt ? (_jsx(Text, { children: timeAgo(row.lastUsedAt) })) : (
121
+ // Said rather than left blank. An empty cell reads as "no data
122
+ // yet"; this is the difference between a working integration and
123
+ // one nobody ever pointed a directory at.
124
+ _jsx(Text, { sx: { color: 'attention.fg' }, children: "Never" })),
125
+ },
126
+ {
127
+ header: 'Secret rotated',
128
+ field: 'rotatedAt',
129
+ renderCell: row => _jsx(Text, { children: timeAgo(row.rotatedAt) || '—' }),
130
+ },
131
+ ...(readOnly
132
+ ? []
133
+ : [
134
+ {
135
+ header: '',
136
+ field: 'uid',
137
+ renderCell: (row) => (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { icon: KebabHorizontalIcon, "aria-label": `Actions for ${row.name || 'this token'}`, variant: "invisible" }) }), _jsx(ActionMenu.Overlay, { width: "medium", children: _jsxs(ActionList, { children: [_jsxs(ActionList.Item, { disabled: row.revoked, onSelect: () => void doRotate(row.uid), children: ["Rotate secret", _jsx(ActionList.Description, { variant: "block", children: "The old secret stops at once. The directory must be reconfigured with the new one." })] }), _jsxs(ActionList.Item, { variant: "danger", disabled: row.revoked, onSelect: () => void doRevoke(row.uid), children: ["Revoke", _jsx(ActionList.Description, { variant: "block", children: "Provisioning through it stops. The rows it wrote stay readable." })] })] }) })] })),
138
+ },
139
+ ]),
140
+ ];
141
+ if (tokens.isError) {
142
+ return (_jsx(McpErrorBlankslate, { state: errorState(tokens.error, 'SCIM provisioning'), onRetry: () => tokens.refetch() }));
143
+ }
144
+ const shown = SCIM_STATUS_MESSAGES[status.state];
145
+ return (_jsxs(Box, { sx: { display: 'grid', gap: 3, minWidth: 0 }, children: [showTitle && (_jsxs(Box, { sx: {
146
+ display: 'flex',
147
+ alignItems: 'start',
148
+ justifyContent: 'space-between',
149
+ gap: 3,
150
+ }, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Directory provisioning" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, children: "Over SCIM. Sign-in can only act on somebody who turns up; this is what removes the person who does not." })] }), !readOnly && (_jsx(Button, { variant: "primary", ref: returnFocusRef, onClick: openCreate, children: "New SCIM token" }))] })), tokens.isPending && !tokens.data ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 5 }, children: _jsx(Spinner, {}) })) : (_jsxs(_Fragment, { children: [_jsxs(Flash, { variant: shown.variant, children: [_jsx(Text, { as: "p", sx: { fontWeight: 'semibold', m: 0 }, children: shown.heading }), _jsx(Text, { as: "p", sx: { fontSize: 1, mt: 1, mb: 0 }, children: shown.body }), status.lastUsedAt && (_jsxs(Text, { as: "p", sx: { fontSize: 1, mt: 1, mb: 0, color: 'fg.muted' }, children: ["Last provisioning call ", timeAgo(status.lastUsedAt), "."] }))] }), rows.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Table.Container, { children: [_jsx(Table.Title, { as: "h3", id: "scim-tokens", children: "SCIM tokens" }), _jsx(Table.Subtitle, { as: "p", id: "scim-tokens-subtitle", children: "A revoked token is kept rather than deleted, so the provisioning it did stays attributable." }), _jsx(DataTable, { "aria-labelledby": "scim-tokens", "aria-describedby": "scim-tokens-subtitle", data: rows, columns: columns })] }), _jsxs(Box, { sx: {
151
+ border: '1px solid',
152
+ borderColor: 'border.default',
153
+ borderRadius: 2,
154
+ p: 3,
155
+ display: 'grid',
156
+ gap: 2,
157
+ }, children: [_jsx(Text, { sx: { fontWeight: 'semibold', fontSize: 1 }, children: "What to give the directory" }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "SCIM base URL" }), _jsx(TextInput, { block: true, readOnly: true, value: scimBaseUrl(iamUrl), "aria-describedby": "scim-base-url-note" }), _jsxs(FormControl.Caption, { id: "scim-base-url-note", children: ["Okta and Entra append ", _jsx("code", { children: "/Users" }), " themselves. Use the token as a bearer credential; the token is what says which organization is being provisioned, so nothing else names this one."] })] }), _jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0 }, children: ["Groups are not provisioned over SCIM \u2014 this server answers them ", _jsx("code", { children: "501" }), " saying so. Roles come from the sign-in token\u2019s groups claim and the provider\u2019s role mappings, above; a second source of the same fact would eventually disagree with the first."] })] })] })) : (_jsxs(Blankslate, { border: true, spacious: true, children: [_jsx(Blankslate.Visual, { children: _jsx(PeopleIcon, { size: "medium" }) }), _jsx(Blankslate.Heading, { children: "No SCIM token" }), _jsx(Blankslate.Description, { children: _jsx(Text, { sx: { textAlign: 'center' }, children: "Issue one to let this organization\u2019s directory add people when they join and \u2014 the part sign-in cannot do \u2014 remove them when they leave." }) }), !readOnly && (_jsx(Button, { size: "small", onClick: openCreate, children: "New SCIM token" }))] }))] })), creating && (_jsx(Dialog, { title: "New SCIM token", onClose: () => setCreating(false), returnFocusRef: returnFocusRef, footerButtons: [
158
+ {
159
+ buttonType: 'default',
160
+ content: 'Cancel',
161
+ onClick: () => setCreating(false),
162
+ },
163
+ {
164
+ buttonType: 'primary',
165
+ content: 'Issue token',
166
+ onClick: () => void submitCreate(),
167
+ disabled: create.isPending,
168
+ },
169
+ ], children: _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Name" }), _jsx(TextInput, { block: true, autoFocus: true, value: name, placeholder: "Okta", onChange: event => setName(event.target.value) }), _jsx(FormControl.Caption, { children: "Whatever tells this directory from another. Only for your own reading \u2014 it is not part of the credential." })] }) })), secret && (_jsx(Dialog, { title: "Copy this token now", onClose: closeSecret, footerButtons: [
170
+ { buttonType: 'primary', content: 'Done', onClick: closeSecret },
171
+ ], children: _jsxs(Box, { sx: { display: 'grid', gap: 2 }, children: [_jsx(Flash, { variant: "warning", children: "This is the only time this token is shown. Only a hash of it is stored, so it cannot be shown again \u2014 if it is lost, rotate." }), _jsx(TextInput, { block: true, readOnly: true, value: secret, "aria-label": "SCIM token", sx: { fontFamily: 'mono' }, onFocus: event => event.target.select() }), _jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0 }, children: ["Give it to the directory as a bearer token, with the base URL", ' ', _jsx("code", { children: scimBaseUrl(iamUrl) }), "."] })] }) }))] }));
172
+ };
173
+ export default ScimProvisioning;
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useState } from 'react';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Button, Flash, Heading, Select, Spinner, Text } from '@primer/react';
4
- import { Blankslate } from '@primer/react/experimental';
4
+ import { Blankslate, Dialog } from '@primer/react/experimental';
5
5
  import { Box } from '@datalayer/primer-addons';
6
6
  import { PeopleIcon } from '@primer/octicons-react';
7
7
  import { McpErrorBlankslate } from '../../components/mcp/index.js';
8
- import { useMcpPolicyLayer, useOrganizationTeams, useSetMcpPolicyLayer, useDeleteMcpPolicyLayer, } from '../../hooks/useMcp.js';
8
+ import { useDisconnectEveryAgentInTeam, useMcpPolicyLayer, useOrganizationTeams, useSetMcpPolicyLayer, useDeleteMcpPolicyLayer, } from '../../hooks/useMcp.js';
9
9
  import { useToast } from '../../hooks/index.js';
10
10
  import { McpPolicyConflict } from '../../api/iam/mcpPolicy.js';
11
+ import { AdmittedClients } from './AdmittedClients.js';
11
12
  import { PolicyHistory } from './PolicyHistory.js';
12
13
  import { EMPTY_POLICY_DRAFT, PolicyForm, draftOf, policyChanged, rulesFrom, } from './PolicyForm.js';
13
14
  /**
@@ -61,14 +62,14 @@ const TeamPolicyForm = ({ orgUid, teamUid, teamName, readOnly, }) => {
61
62
  maxConcurrentSandboxes: "Counted over this team's own sandboxes, not the organization's " +
62
63
  '— so another team being busy never uses up this limit. The ' +
63
64
  "organization's cap still applies on top.",
64
- } }), !readOnly && (_jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center' }, children: [_jsxs(Button, { variant: "primary", onClick: apply, disabled: !changed || save.isPending, children: ["Save ", teamName, "\u2019s policy"] }), changed && (_jsx(Button, { variant: "invisible", onClick: () => setDraft(stored), children: "Discard changes" })), layer.data && (_jsx(Box, { sx: { marginLeft: 'auto' }, children: _jsx(Button, { variant: "danger", disabled: remove.isPending, onClick: () => remove.mutate(undefined, {
65
+ }, belowAllowedClients: _jsx(AdmittedClients, { scope: "team", subjectUid: teamUid, edited: draft.allowedClients !== stored.allowedClients }) }), !readOnly && (_jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center' }, children: [_jsxs(Button, { variant: "primary", onClick: apply, disabled: !changed || save.isPending, children: ["Save ", teamName, "\u2019s policy"] }), changed && (_jsx(Button, { variant: "invisible", onClick: () => setDraft(stored), children: "Discard changes" })), layer.data && (_jsx(Box, { sx: { marginLeft: 'auto' }, children: _jsx(Button, { variant: "danger", disabled: remove.isPending, onClick: () => remove.mutate(undefined, {
65
66
  onSuccess: () => enqueueToast(`${teamName} narrows nothing of its own.`, {
66
67
  variant: 'success',
67
68
  }),
68
69
  onError: error => enqueueToast(`Could not remove: ${error.message}`, {
69
70
  variant: 'error',
70
71
  }),
71
- }), children: "Remove" }) }))] })), _jsxs(Box, { sx: {
72
+ }), children: "Remove" }) }))] })), !readOnly && _jsx(TeamSessions, { teamUid: teamUid, teamName: teamName }), _jsxs(Box, { sx: {
72
73
  borderTop: '1px solid',
73
74
  borderColor: 'border.muted',
74
75
  pt: 3,
@@ -76,6 +77,66 @@ const TeamPolicyForm = ({ orgUid, teamUid, teamName, readOnly, }) => {
76
77
  gap: 2,
77
78
  }, children: [_jsx(Text, { as: "h3", sx: { fontSize: 1, fontWeight: 'semibold', m: 0 }, children: "History" }), _jsx(PolicyHistory, { orgUid: orgUid, subjectUid: teamUid })] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.subtle' }, children: "A team can be stricter than its organization and never looser. Permitting a tool the organization denies does nothing, and a cap above the organization\u2019s leaves the organization\u2019s in force." })] }));
78
79
  };
80
+ /**
81
+ * Ending every session consented in one team.
82
+ *
83
+ * On the team's own page rather than on Connected agents, because that page
84
+ * is a person's list of their own grants — "disconnect all" there means
85
+ * *yours*, and a control that quietly meant a whole team's would be the same
86
+ * word doing two very different things one click apart.
87
+ *
88
+ * It is the team's counterpart of the personal one, and the same authority
89
+ * as removing somebody from the team: IAM asks `_can_manage_team`, so a team
90
+ * owner or the organization's owner may do it and nobody else. Narrow in the
91
+ * way deprovisioning is — no personal agent and no other team's grant is
92
+ * touched.
93
+ *
94
+ * **Not a policy rule, and kept out of the form above on purpose.** Saving a
95
+ * policy is reversible and takes effect on the next call; this ends live
96
+ * sessions now. Folding it into the same Save button would make somebody
97
+ * adjusting a cap sign every one of their team's agents out.
98
+ */
99
+ const TeamSessions = ({ teamUid, teamName, }) => {
100
+ const { enqueueToast } = useToast();
101
+ const disconnect = useDisconnectEveryAgentInTeam();
102
+ const [asking, setAsking] = useState(false);
103
+ const returnFocusRef = useRef(null);
104
+ const confirm = () => {
105
+ disconnect.mutate(teamUid, {
106
+ onSuccess: answer => {
107
+ enqueueToast(answer.revoked === 1
108
+ ? `1 session ended for ${teamName}.`
109
+ : `${answer.revoked} sessions ended for ${teamName}.`, { variant: 'success' });
110
+ setAsking(false);
111
+ },
112
+ onError: error => {
113
+ setAsking(false);
114
+ enqueueToast(`Could not end sessions: ${error.message}`, {
115
+ variant: 'error',
116
+ });
117
+ },
118
+ });
119
+ };
120
+ return (_jsxs(Box, { sx: {
121
+ borderTop: '1px solid',
122
+ borderColor: 'border.muted',
123
+ pt: 3,
124
+ display: 'grid',
125
+ gap: 2,
126
+ }, children: [_jsx(Text, { as: "h3", sx: { fontSize: 1, fontWeight: 'semibold', m: 0 }, children: "Sessions" }), _jsxs(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: ["Every agent connected while acting for ", teamName, " loses its grant at once. Each asks again the next time somebody connects it, and goes through the approval screen rather than being answered from what was agreed before \u2014 so this is also how a fresh approval is forced."] }), _jsx(Box, { children: _jsxs(Button, { variant: "danger", ref: returnFocusRef, onClick: () => setAsking(true), disabled: disconnect.isPending, children: ["End ", teamName, "\u2019s sessions"] }) }), asking && (_jsxs(Dialog, { title: `End every session in ${teamName}?`, onClose: () => setAsking(false), returnFocusRef: returnFocusRef, footerButtons: [
127
+ {
128
+ buttonType: 'default',
129
+ content: 'Keep them',
130
+ onClick: () => setAsking(false),
131
+ },
132
+ {
133
+ buttonType: 'danger',
134
+ content: 'End them',
135
+ onClick: confirm,
136
+ disabled: disconnect.isPending,
137
+ },
138
+ ], children: [_jsxs(Text, { sx: { fontSize: 1 }, children: ["Every grant consented in ", teamName, " ends now, whoever holds it. Work an agent is in the middle of stops where it is."] }), _jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', mt: 2, mb: 0 }, children: "This team only. Nobody\u2019s personal agents end, no other team\u2019s do, and nothing any agent already did is undone." })] }))] }));
139
+ };
79
140
  export const TeamPolicies = ({ errorState, orgUid, readOnly = false, showTitle = true, }) => {
80
141
  const teams = useOrganizationTeams(orgUid);
81
142
  const [chosen, setChosen] = useState('');
@@ -0,0 +1,60 @@
1
+ /**
2
+ * What an agent may actually call, in one space or one organization.
3
+ *
4
+ * A source is not exposed to agents because it exists. Somebody allows a set
5
+ * of its tools on the source, and each session an agent opens narrows that
6
+ * further. This is where those two lists are read together — because the
7
+ * only list that matters is their **intersection**, and neither record shows
8
+ * it on its own.
9
+ *
10
+ * That gap is not theoretical. Contents computes the effective policy as
11
+ * `session ∩ source` and refuses anything outside it, so a tool removed from
12
+ * the source after a session opened goes on sitting in that session's
13
+ * `allowedTools` and is refused at every call. A page that drew the session's
14
+ * list would show a tool the agent cannot use, and the person debugging it
15
+ * would be reading a page that agreed with them.
16
+ *
17
+ * **An empty allowlist allows nothing.** It is not "unrestricted", here or
18
+ * in Contents: `check_tool` refuses every tool not in the list, so a session
19
+ * whose effective set is empty can call none of them. Said plainly on the
20
+ * row, because the opposite reading is the natural one and it is the
21
+ * dangerous direction to be wrong in.
22
+ *
23
+ * @module views/mcp/ToolAccess
24
+ */
25
+ import type { JSX } from 'react';
26
+ import type { ContentSource, McpConfiguration } from '../../api/contents/generated';
27
+ import { type McpErrorStateFn } from './types';
28
+ /**
29
+ * The tools a session can really call: what it was granted, still allowed by
30
+ * the source.
31
+ *
32
+ * The same intersection Contents applies, computed here so the page shows
33
+ * what will happen rather than what was asked for. Sorted, so two sessions
34
+ * of the same source read the same way.
35
+ */
36
+ export declare const effectiveTools: (sessionAllowed: readonly string[] | null | undefined, sourceAllowed: readonly string[] | null | undefined) => string[];
37
+ /**
38
+ * The tools a session still lists that its source has stopped allowing.
39
+ *
40
+ * Worth its own answer rather than a subtraction at the call site: this is
41
+ * the set that makes an agent fail in a way nobody can explain from either
42
+ * record alone, and naming it is the whole reason to read the two together.
43
+ */
44
+ export declare const withdrawnTools: (sessionAllowed: readonly string[] | null | undefined, sourceAllowed: readonly string[] | null | undefined) => string[];
45
+ /** How a source's approval policy reads to somebody deciding whether to trust it. */
46
+ export declare const APPROVAL_POLICY_LOOK: Record<McpConfiguration['approvalPolicy'], {
47
+ label: string;
48
+ variant: 'attention' | 'success' | 'secondary';
49
+ note: string;
50
+ }>;
51
+ /** The MCP configuration of a source, where it has one. */
52
+ export declare const mcpConfigOf: (source: ContentSource) => McpConfiguration | undefined;
53
+ export interface ToolAccessProps {
54
+ /** The application's words for a failed request. */
55
+ errorState: McpErrorStateFn;
56
+ /** One space's sources, rather than every source the caller can see. */
57
+ spaceUid?: string;
58
+ }
59
+ export declare const ToolAccess: ({ errorState, spaceUid, }: ToolAccessProps) => JSX.Element;
60
+ export default ToolAccess;