@stigmer/react 3.29.0 → 3.31.0
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.
- package/execution/McpToolDetail.d.ts +4 -1
- package/execution/McpToolDetail.d.ts.map +1 -1
- package/execution/McpToolDetail.js +6 -2
- package/execution/McpToolDetail.js.map +1 -1
- package/execution/ToolArgsView.d.ts.map +1 -1
- package/execution/ToolArgsView.js +9 -4
- package/execution/ToolArgsView.js.map +1 -1
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +6 -4
- package/execution/tool-categories.js.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.d.ts +5 -1
- package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.js +10 -5
- package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
- package/oauth-app/OAuthAppListPanel.d.ts +11 -3
- package/oauth-app/OAuthAppListPanel.d.ts.map +1 -1
- package/oauth-app/OAuthAppListPanel.js +4 -4
- package/oauth-app/OAuthAppListPanel.js.map +1 -1
- package/oauth-app/copy.d.ts +15 -0
- package/oauth-app/copy.d.ts.map +1 -0
- package/oauth-app/copy.js +15 -0
- package/oauth-app/copy.js.map +1 -0
- package/oauth-app/useDeleteOAuthApp.d.ts +4 -3
- package/oauth-app/useDeleteOAuthApp.d.ts.map +1 -1
- package/oauth-app/useDeleteOAuthApp.js +4 -3
- package/oauth-app/useDeleteOAuthApp.js.map +1 -1
- package/oauth-app/useOAuthAppList.d.ts +6 -4
- package/oauth-app/useOAuthAppList.d.ts.map +1 -1
- package/oauth-app/useOAuthAppList.js +6 -4
- package/oauth-app/useOAuthAppList.js.map +1 -1
- package/package.json +5 -5
- package/settings/OAuthAppsSection.d.ts +9 -1
- package/settings/OAuthAppsSection.d.ts.map +1 -1
- package/settings/OAuthAppsSection.js +17 -3
- package/settings/OAuthAppsSection.js.map +1 -1
- package/src/execution/McpToolDetail.tsx +6 -2
- package/src/execution/ToolArgsView.tsx +9 -4
- package/src/execution/__tests__/secret-args.test.tsx +81 -0
- package/src/execution/tool-categories.ts +6 -3
- package/src/oauth-app/OAuthAppDetailPanel.tsx +39 -24
- package/src/oauth-app/OAuthAppListPanel.tsx +12 -3
- package/src/oauth-app/__tests__/OAuthAppDetailPanel.test.tsx +57 -9
- package/src/oauth-app/copy.ts +16 -0
- package/src/oauth-app/useDeleteOAuthApp.ts +4 -3
- package/src/oauth-app/useOAuthAppList.ts +6 -4
- package/src/settings/OAuthAppsSection.tsx +21 -3
- package/src/settings/__tests__/OAuthAppsSection.test.tsx +133 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOAuthAppList.d.ts","sourceRoot":"","sources":["../../src/oauth-app/useOAuthAppList.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mDAAmD,CAAC;AAIlF,+CAA+C;AAC/C,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED
|
|
1
|
+
{"version":3,"file":"useOAuthAppList.d.ts","sourceRoot":"","sources":["../../src/oauth-app/useOAuthAppList.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mDAAmD,CAAC;AAIlF,+CAA+C;AAC/C,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,qBAAqB,CAevB"}
|
|
@@ -4,11 +4,13 @@ import { ListOAuthAppsByOrgInputSchema } from "@stigmer/protos/ai/stigmer/iam/oa
|
|
|
4
4
|
import { useStigmer } from "../hooks.js";
|
|
5
5
|
import { useFetch } from "../internal/useFetch.js";
|
|
6
6
|
/**
|
|
7
|
-
* Data hook that fetches
|
|
7
|
+
* Data hook that fetches the {@link OAuthApp} entries of an organization
|
|
8
|
+
* that the caller may view.
|
|
8
9
|
*
|
|
9
|
-
* Returns
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Returns the organization's OAuthApps that a `get` would return to the
|
|
11
|
+
* caller: each app's creator and the organization's admins see it, other
|
|
12
|
+
* members and viewers see none. In practice these are the BYOA OAuth apps
|
|
13
|
+
* created through the "Bring your own app" flow on MCP server detail pages.
|
|
12
14
|
*
|
|
13
15
|
* Pass `null` for `org` to skip fetching (stable no-op). Useful when
|
|
14
16
|
* the active organization has not been resolved yet.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOAuthAppList.js","sourceRoot":"","sources":["../../src/oauth-app/useOAuthAppList.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,6BAA6B,EAAE,MAAM,kDAAkD,CAAC;AAEjG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAgBnD
|
|
1
|
+
{"version":3,"file":"useOAuthAppList.js","sourceRoot":"","sources":["../../src/oauth-app/useOAuthAppList.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,6BAA6B,EAAE,MAAM,kDAAkD,CAAC;AAEjG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAgBnD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAkB;IAElB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,QAAQ,CAC3E,GAAG;QACD,CAAC,CAAC,GAAG,EAAE,CACH,OAAO,CAAC,QAAQ;aACb,SAAS,CAAC,MAAM,CAAC,6BAA6B,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;aACzD,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;QAClC,CAAC,CAAC,IAAI,EACR,CAAC,GAAG,EAAE,OAAO,CAAC,EACd,EAAgB,CACjB,CAAC;IAEF,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAChE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/react",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.31.0",
|
|
4
4
|
"description": "React provider and client hook for the Stigmer platform SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@bufbuild/protovalidate": "^1.1.1",
|
|
39
39
|
"@dagrejs/dagre": "^1.1.4",
|
|
40
|
-
"@stigmer/plugin-package": "3.
|
|
41
|
-
"@stigmer/theme": "3.
|
|
40
|
+
"@stigmer/plugin-package": "3.31.0",
|
|
41
|
+
"@stigmer/theme": "3.31.0",
|
|
42
42
|
"diff": "^8.0.3",
|
|
43
43
|
"fflate": "^0.8.2",
|
|
44
44
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"@codemirror/state": "^6.0.0",
|
|
62
62
|
"@codemirror/view": "^6.0.0",
|
|
63
63
|
"@lezer/highlight": "^1.0.0",
|
|
64
|
-
"@stigmer/protos": "3.
|
|
65
|
-
"@stigmer/sdk": "3.
|
|
64
|
+
"@stigmer/protos": "3.31.0",
|
|
65
|
+
"@stigmer/sdk": "3.31.0",
|
|
66
66
|
"@tanstack/react-table": "^8.20.0",
|
|
67
67
|
"@xyflow/react": "^12.0.0",
|
|
68
68
|
"elkjs": "^0.9.0",
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Settings section for organization OAuth app credentials.
|
|
3
|
+
*
|
|
4
|
+
* "New OAuth app" is offered once the server confirms the caller holds
|
|
5
|
+
* `can_create_oauth_app` on the organization. The list shows only the apps
|
|
6
|
+
* the caller may view (each app's creator and the organization's admins),
|
|
7
|
+
* so a caller who may not create OAuth apps is told that the
|
|
8
|
+
* organization's admins manage them rather than that none is configured.
|
|
9
|
+
*/
|
|
2
10
|
export declare function OAuthAppsSection(): import("react/jsx-runtime").JSX.Element;
|
|
3
11
|
//# sourceMappingURL=OAuthAppsSection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OAuthAppsSection.d.ts","sourceRoot":"","sources":["../../src/settings/OAuthAppsSection.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"OAuthAppsSection.d.ts","sourceRoot":"","sources":["../../src/settings/OAuthAppsSection.tsx"],"names":[],"mappings":"AAkBA;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,4CA2F/B"}
|
|
@@ -4,14 +4,28 @@ import { useCallback, useId, useRef, useState } from "react";
|
|
|
4
4
|
import { OAuthAppListPanel } from "../oauth-app/OAuthAppListPanel.js";
|
|
5
5
|
import { CreateOAuthAppForm } from "../oauth-app/CreateOAuthAppForm.js";
|
|
6
6
|
import { OAuthAppDetailPanel } from "../oauth-app/OAuthAppDetailPanel.js";
|
|
7
|
+
import { OAUTH_APPS_MANAGED_BY_ADMINS } from "../oauth-app/copy.js";
|
|
7
8
|
import { useResourceAvailable, ApiResourceKind } from "../deployment-mode.js";
|
|
9
|
+
import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
|
|
8
10
|
import { CloudFeatureNotice } from "../internal/CloudFeatureNotice.js";
|
|
9
|
-
import { useActiveOrgSlug } from "../organization/OrgProvider.js";
|
|
10
|
-
/**
|
|
11
|
+
import { useActiveOrgId, useActiveOrgSlug } from "../organization/OrgProvider.js";
|
|
12
|
+
/**
|
|
13
|
+
* Settings section for organization OAuth app credentials.
|
|
14
|
+
*
|
|
15
|
+
* "New OAuth app" is offered once the server confirms the caller holds
|
|
16
|
+
* `can_create_oauth_app` on the organization. The list shows only the apps
|
|
17
|
+
* the caller may view (each app's creator and the organization's admins),
|
|
18
|
+
* so a caller who may not create OAuth apps is told that the
|
|
19
|
+
* organization's admins manage them rather than that none is configured.
|
|
20
|
+
*/
|
|
11
21
|
export function OAuthAppsSection() {
|
|
12
22
|
const headingId = useId();
|
|
13
23
|
const org = useActiveOrgSlug();
|
|
24
|
+
const orgId = useActiveOrgId();
|
|
14
25
|
const oauthAppsAvailable = useResourceAvailable(ApiResourceKind.oauth_app);
|
|
26
|
+
const createCheck = useCheckPermission(oauthAppsAvailable && orgId ? { kind: "organization", id: orgId } : null, "can_create_oauth_app");
|
|
27
|
+
const canCreate = !createCheck.isLoading && createCheck.allowed;
|
|
28
|
+
const deniedCreate = !createCheck.isLoading && !createCheck.allowed;
|
|
15
29
|
const [flow, setFlow] = useState({ phase: "idle" });
|
|
16
30
|
const listRefetchRef = useRef(null);
|
|
17
31
|
const handleRefetchRef = useCallback((refetch) => {
|
|
@@ -29,6 +43,6 @@ export function OAuthAppsSection() {
|
|
|
29
43
|
listRefetchRef.current?.();
|
|
30
44
|
setFlow({ phase: "idle" });
|
|
31
45
|
}, []);
|
|
32
|
-
return (_jsxs("section", { "aria-labelledby": headingId, children: [_jsxs("div", { className: "stg:mb-3 stg:flex stg:items-center stg:justify-between", children: [_jsx("h2", { id: headingId, className: "stg:text-foreground stg:text-sm stg:font-semibold", children: "OAuth Apps" }), oauthAppsAvailable && org && flow.phase === "idle" && (_jsx("button", { type: "button", onClick: () => setFlow({ phase: "creating" }), className: "stg:text-primary stg:hover:text-foreground stg:text-xs stg:font-medium stg:transition-colors", children: "+ New OAuth app" }))] }), _jsx("p", { className: "stg:text-muted-foreground stg:mb-4 stg:text-xs", children: "OAuth app credentials configured for your organization. Create new apps here or bring your own from an MCP server's detail page." }), !oauthAppsAvailable ? (_jsx(CloudFeatureNotice, { children: "OAuth apps are not available in local mode. Bring-your-own-app OAuth requires the cloud platform." })) : flow.phase === "creating" ? (_jsx("div", { className: "stg:border-border stg:bg-card stg:rounded-lg stg:border stg:p-4", children: _jsx(CreateOAuthAppForm, { org: org, onCreated: handleCreated, onCancel: () => setFlow({ phase: "idle" }) }) })) : flow.phase === "editing" ? (_jsx("div", { className: "stg:border-border stg:bg-card stg:rounded-lg stg:border stg:p-4", children: _jsx(OAuthAppDetailPanel, { oauthApp: flow.oauthApp, onUpdated: handleUpdated, onDeleted: handleDeleted, onBack: () => setFlow({ phase: "idle" }) }) })) : (_jsx(OAuthAppListPanel, { org: org, onEdit: (app) => setFlow({ phase: "editing", oauthApp: app }), onRefetchRef: handleRefetchRef }))] }));
|
|
46
|
+
return (_jsxs("section", { "aria-labelledby": headingId, children: [_jsxs("div", { className: "stg:mb-3 stg:flex stg:items-center stg:justify-between", children: [_jsx("h2", { id: headingId, className: "stg:text-foreground stg:text-sm stg:font-semibold", children: "OAuth Apps" }), oauthAppsAvailable && org && canCreate && flow.phase === "idle" && (_jsx("button", { type: "button", onClick: () => setFlow({ phase: "creating" }), className: "stg:text-primary stg:hover:text-foreground stg:text-xs stg:font-medium stg:transition-colors", children: "+ New OAuth app" }))] }), _jsx("p", { className: "stg:text-muted-foreground stg:mb-4 stg:text-xs", children: "OAuth app credentials configured for your organization. Create new apps here or bring your own from an MCP server's detail page." }), !oauthAppsAvailable ? (_jsx(CloudFeatureNotice, { children: "OAuth apps are not available in local mode. Bring-your-own-app OAuth requires the cloud platform." })) : flow.phase === "creating" ? (_jsx("div", { className: "stg:border-border stg:bg-card stg:rounded-lg stg:border stg:p-4", children: _jsx(CreateOAuthAppForm, { org: org, onCreated: handleCreated, onCancel: () => setFlow({ phase: "idle" }) }) })) : flow.phase === "editing" ? (_jsx("div", { className: "stg:border-border stg:bg-card stg:rounded-lg stg:border stg:p-4", children: _jsx(OAuthAppDetailPanel, { oauthApp: flow.oauthApp, onUpdated: handleUpdated, onDeleted: handleDeleted, onBack: () => setFlow({ phase: "idle" }) }) })) : (_jsx(OAuthAppListPanel, { org: org, onEdit: (app) => setFlow({ phase: "editing", oauthApp: app }), onRefetchRef: handleRefetchRef, emptyState: deniedCreate ? OAUTH_APPS_MANAGED_BY_ADMINS : undefined }))] }));
|
|
33
47
|
}
|
|
34
48
|
//# sourceMappingURL=OAuthAppsSection.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OAuthAppsSection.js","sourceRoot":"","sources":["../../src/settings/OAuthAppsSection.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"OAuthAppsSection.js","sourceRoot":"","sources":["../../src/settings/OAuthAppsSection.tsx"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
5
|
+
import { redactSecretArgs } from "@stigmer/sdk";
|
|
5
6
|
import { cn } from "@stigmer/theme";
|
|
6
7
|
import { TruncatedText } from "../internal/truncated-text.js";
|
|
7
8
|
import { formatDuration } from "./ToolCallDetail.js";
|
|
@@ -247,10 +248,13 @@ export interface McpArgsViewProps {
|
|
|
247
248
|
* Renders MCP tool arguments as a structured key-value list.
|
|
248
249
|
*
|
|
249
250
|
* Scalars display inline; objects and arrays collapse into
|
|
250
|
-
* formatted JSON blocks via {@link CollapsibleJsonBlock}.
|
|
251
|
+
* formatted JSON blocks via {@link CollapsibleJsonBlock}. A secret-keyed
|
|
252
|
+
* value renders as `[REDACTED]` (`redactSecretArgs`): the runner redacts a
|
|
253
|
+
* row's `args` at creation, but a row persisted before it did (stigmer#1119)
|
|
254
|
+
* still carries the value, and this is where it would become visible.
|
|
251
255
|
*/
|
|
252
256
|
export function McpArgsView({ args }: McpArgsViewProps) {
|
|
253
|
-
const entries = Object.entries(args);
|
|
257
|
+
const entries = Object.entries(redactSecretArgs(args));
|
|
254
258
|
if (entries.length === 0) return null;
|
|
255
259
|
|
|
256
260
|
const scalars: [string, string][] = [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useMemo } from "react";
|
|
4
|
+
import { isSecretArgKey, redactSecretArgs } from "@stigmer/sdk";
|
|
4
5
|
import { cn } from "@stigmer/theme";
|
|
5
6
|
import {
|
|
6
7
|
resolveToolCategory,
|
|
@@ -229,9 +230,11 @@ function McpArgsPreview({
|
|
|
229
230
|
);
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
// Secret-keyed values are hidden here, at the sink: a row persisted before the
|
|
234
|
+
// runner redacted `args` (stigmer#1119) still carries them.
|
|
232
235
|
function GenericArgsView({ args }: { args: Record<string, unknown> }) {
|
|
233
236
|
return (
|
|
234
|
-
<CollapsibleCode label="Arguments" content={formatJson(args)} />
|
|
237
|
+
<CollapsibleCode label="Arguments" content={formatJson(redactSecretArgs(args))} />
|
|
235
238
|
);
|
|
236
239
|
}
|
|
237
240
|
|
|
@@ -261,10 +264,12 @@ function extractPrimaryArgValue(
|
|
|
261
264
|
if (v) return v;
|
|
262
265
|
}
|
|
263
266
|
|
|
267
|
+
// The first argument that is not a secret: a secret is never a title
|
|
268
|
+
// (stigmer#1119), and `[REDACTED]` would read as content.
|
|
264
269
|
if (info.category === "unknown" || info.category === "mcp") {
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
const val = args[
|
|
270
|
+
const first = Object.keys(args).find((key) => !isSecretArgKey(key));
|
|
271
|
+
if (first !== undefined) {
|
|
272
|
+
const val = args[first];
|
|
268
273
|
if (typeof val === "string") return val;
|
|
269
274
|
}
|
|
270
275
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A settled tool-call row never shows a secret-keyed argument (stigmer#1119).
|
|
3
|
+
*
|
|
4
|
+
* The runner redacts a row's `args` at creation, but rows persisted before it
|
|
5
|
+
* did still carry the values, so every sink that renders `args` hides them
|
|
6
|
+
* through `@stigmer/sdk`'s `redactSecretArgs` / `isSecretArgKey` — the reader
|
|
7
|
+
* side of test/fixtures/tool-view/secret-args.json. Each case renders a row as
|
|
8
|
+
* an older runner stored it, with the secret in clear.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
12
|
+
import { render, cleanup } from "@testing-library/react";
|
|
13
|
+
import { create, type JsonObject } from "@bufbuild/protobuf";
|
|
14
|
+
import {
|
|
15
|
+
ToolCallSchema,
|
|
16
|
+
type ToolCall,
|
|
17
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
18
|
+
import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
19
|
+
import { McpToolDetail } from "../McpToolDetail";
|
|
20
|
+
import { ToolArgsView } from "../ToolArgsView";
|
|
21
|
+
import { ToolCallDetail } from "../ToolCallDetail";
|
|
22
|
+
import { extractPrimaryArg } from "../tool-categories";
|
|
23
|
+
|
|
24
|
+
afterEach(cleanup);
|
|
25
|
+
|
|
26
|
+
const SECRET = "s3cret-value";
|
|
27
|
+
|
|
28
|
+
function settledRow(name: string, mcpServerSlug: string, args: Record<string, unknown>): ToolCall {
|
|
29
|
+
return create(ToolCallSchema, {
|
|
30
|
+
id: "tc-1",
|
|
31
|
+
name,
|
|
32
|
+
mcpServerSlug,
|
|
33
|
+
args: args as JsonObject,
|
|
34
|
+
result: "ok",
|
|
35
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("the detail views hide a secret-keyed argument and keep the rest", () => {
|
|
40
|
+
it("McpToolDetail's argument list", () => {
|
|
41
|
+
const { container } = render(
|
|
42
|
+
<McpToolDetail toolCall={settledRow("push", "github", { token: SECRET, repo: "acme/x" })} />,
|
|
43
|
+
);
|
|
44
|
+
expect(container.textContent).not.toContain(SECRET);
|
|
45
|
+
expect(container.textContent).toContain("[REDACTED]");
|
|
46
|
+
expect(container.textContent).toContain("acme/x");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("ToolArgsView's MCP branch", () => {
|
|
50
|
+
const { container } = render(
|
|
51
|
+
<ToolArgsView toolName="push" mcpServerSlug="github" args={{ Api_Key: SECRET, repo: "acme/x" }} />,
|
|
52
|
+
);
|
|
53
|
+
expect(container.textContent).not.toContain(SECRET);
|
|
54
|
+
expect(container.textContent).toContain("acme/x");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("ToolArgsView's generic branch, and ToolCallDetail through it", () => {
|
|
58
|
+
const args = { authorization: SECRET, region: "eu-west-1" };
|
|
59
|
+
const view = render(<ToolArgsView toolName="mystery_tool" args={args} />);
|
|
60
|
+
expect(view.container.textContent).not.toContain(SECRET);
|
|
61
|
+
expect(view.container.textContent).toContain("eu-west-1");
|
|
62
|
+
cleanup();
|
|
63
|
+
|
|
64
|
+
const detail = render(<ToolCallDetail toolCall={settledRow("mystery_tool", "", args)} />);
|
|
65
|
+
expect(detail.container.textContent).not.toContain(SECRET);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("the row title never falls back to a secret", () => {
|
|
70
|
+
it("skips a secret first argument for the next argument", () => {
|
|
71
|
+
expect(extractPrimaryArg(settledRow("push", "github", { token: SECRET, repo: "acme/x" }))).toBe("acme/x");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("has no title argument when every argument is secret", () => {
|
|
75
|
+
expect(extractPrimaryArg(settledRow("push", "github", { password: SECRET }))).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("keeps the first argument when it is not secret", () => {
|
|
79
|
+
expect(extractPrimaryArg(settledRow("push", "github", { repo: "acme/x", token: SECRET }))).toBe("acme/x");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -3,6 +3,7 @@ import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecutio
|
|
|
3
3
|
import type { JsonObject } from "@bufbuild/protobuf";
|
|
4
4
|
import {
|
|
5
5
|
ToolKind,
|
|
6
|
+
isSecretArgKey,
|
|
6
7
|
resolveToolKind,
|
|
7
8
|
resolveToolKindByName,
|
|
8
9
|
shellIntentFromArgs,
|
|
@@ -414,10 +415,12 @@ export function extractPrimaryArg(toolCall: ToolCall): string | null {
|
|
|
414
415
|
|
|
415
416
|
if (result) return result;
|
|
416
417
|
|
|
418
|
+
// The first argument that is not a secret: a secret is never a row title
|
|
419
|
+
// (stigmer#1119), and `[REDACTED]` would read as content.
|
|
417
420
|
if ((info.category === "unknown" || info.category === "mcp") && toolCall.args) {
|
|
418
|
-
const
|
|
419
|
-
if (
|
|
420
|
-
const val = toolCall.args[
|
|
421
|
+
const first = Object.keys(toolCall.args).find((key) => !isSecretArgKey(key));
|
|
422
|
+
if (first !== undefined) {
|
|
423
|
+
const val = toolCall.args[first];
|
|
421
424
|
if (typeof val === "string") return val;
|
|
422
425
|
}
|
|
423
426
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt";
|
|
12
12
|
import { useUpdateOAuthApp } from "./useUpdateOAuthApp.js";
|
|
13
13
|
import { useDeleteOAuthApp } from "./useDeleteOAuthApp.js";
|
|
14
|
+
import { PermissionGate } from "../iam-policy/PermissionGate.js";
|
|
14
15
|
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -35,7 +36,11 @@ export interface OAuthAppDetailPanelProps {
|
|
|
35
36
|
* View and edit panel for an existing OAuth app.
|
|
36
37
|
*
|
|
37
38
|
* In **view mode**, displays all OAuth configuration fields in a
|
|
38
|
-
* structured label/value layout with "Edit"
|
|
39
|
+
* structured label/value layout, with an "Edit" button for a caller who
|
|
40
|
+
* may edit the app (`can_edit`) and a "Delete" button for one who may
|
|
41
|
+
* delete it (`can_delete`). Both belong to the app's creator alone: the
|
|
42
|
+
* organization's admins may view an app they did not create, never change
|
|
43
|
+
* it.
|
|
39
44
|
*
|
|
40
45
|
* In **edit mode**, fields become editable inputs. The client secret
|
|
41
46
|
* field shows a placeholder — leave it empty to keep the existing
|
|
@@ -223,31 +228,41 @@ export function OAuthAppDetailPanel({
|
|
|
223
228
|
|
|
224
229
|
{mode === "view" && (
|
|
225
230
|
<div className="stg:flex stg:items-center stg:gap-1">
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
className={cn(
|
|
230
|
-
"stg:shrink-0 stg:rounded-md stg:px-2.5 stg:py-1.5 stg:text-xs stg:font-medium",
|
|
231
|
-
"stg:text-muted-foreground stg:hover:text-foreground stg:hover:bg-accent-hover",
|
|
232
|
-
"stg:transition-colors",
|
|
233
|
-
)}
|
|
231
|
+
<PermissionGate
|
|
232
|
+
resource={{ kind: "oauth_app", id: meta?.id ?? "" }}
|
|
233
|
+
relation="can_edit"
|
|
234
234
|
>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={enterEdit}
|
|
238
|
+
className={cn(
|
|
239
|
+
"stg:shrink-0 stg:rounded-md stg:px-2.5 stg:py-1.5 stg:text-xs stg:font-medium",
|
|
240
|
+
"stg:text-muted-foreground stg:hover:text-foreground stg:hover:bg-accent-hover",
|
|
241
|
+
"stg:transition-colors",
|
|
242
|
+
)}
|
|
243
|
+
>
|
|
244
|
+
Edit
|
|
245
|
+
</button>
|
|
246
|
+
</PermissionGate>
|
|
247
|
+
<PermissionGate
|
|
248
|
+
resource={{ kind: "oauth_app", id: meta?.id ?? "" }}
|
|
249
|
+
relation="can_delete"
|
|
248
250
|
>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
onClick={() => {
|
|
254
|
+
clearDeleteError();
|
|
255
|
+
setConfirmingDelete(true);
|
|
256
|
+
}}
|
|
257
|
+
className={cn(
|
|
258
|
+
"stg:shrink-0 stg:rounded-md stg:px-2.5 stg:py-1.5 stg:text-xs stg:font-medium",
|
|
259
|
+
"stg:text-destructive-muted stg:hover:text-destructive stg:hover:bg-destructive-subtle",
|
|
260
|
+
"stg:transition-colors",
|
|
261
|
+
)}
|
|
262
|
+
>
|
|
263
|
+
Delete
|
|
264
|
+
</button>
|
|
265
|
+
</PermissionGate>
|
|
251
266
|
</div>
|
|
252
267
|
)}
|
|
253
268
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import type { ReactNode } from "react";
|
|
3
4
|
import { cn } from "@stigmer/theme";
|
|
4
5
|
import { getUserMessage } from "@stigmer/sdk";
|
|
5
6
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
@@ -23,13 +24,20 @@ export interface OAuthAppListPanelProps {
|
|
|
23
24
|
readonly onEdit?: (app: OAuthApp) => void;
|
|
24
25
|
/** Re-expose refetch so parents can trigger a list refresh. */
|
|
25
26
|
readonly onRefetchRef?: (refetch: () => void) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Shown when the list is empty. Defaults to "No OAuth apps configured
|
|
29
|
+
* yet." The list holds only the apps the caller may view (each app's
|
|
30
|
+
* creator and the organization's admins), so a host that knows the
|
|
31
|
+
* caller may not manage OAuth apps should say who does instead.
|
|
32
|
+
*/
|
|
33
|
+
readonly emptyState?: ReactNode;
|
|
26
34
|
/** Additional CSS class names for the root container. */
|
|
27
35
|
readonly className?: string;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
|
-
* Displays
|
|
32
|
-
*
|
|
39
|
+
* Displays the {@link OAuthApp} resources of an organization that the
|
|
40
|
+
* caller may view.
|
|
33
41
|
*
|
|
34
42
|
* Each row shows the provider name, client ID (non-secret), and
|
|
35
43
|
* creation date. When `onEdit` is provided, rows include a pencil
|
|
@@ -56,6 +64,7 @@ export function OAuthAppListPanel({
|
|
|
56
64
|
org,
|
|
57
65
|
onEdit,
|
|
58
66
|
onRefetchRef,
|
|
67
|
+
emptyState = "No OAuth apps configured yet.",
|
|
59
68
|
className,
|
|
60
69
|
}: OAuthAppListPanelProps) {
|
|
61
70
|
const { oauthApps, isLoading, error, refetch } = useOAuthAppList(org);
|
|
@@ -97,7 +106,7 @@ export function OAuthAppListPanel({
|
|
|
97
106
|
className,
|
|
98
107
|
)}
|
|
99
108
|
>
|
|
100
|
-
|
|
109
|
+
{emptyState}
|
|
101
110
|
</p>
|
|
102
111
|
);
|
|
103
112
|
}
|
|
@@ -6,17 +6,24 @@ import {
|
|
|
6
6
|
type OAuthApp,
|
|
7
7
|
} from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/api_pb";
|
|
8
8
|
import { TokenEndpointAuthMethod } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/spec_pb";
|
|
9
|
+
import type { CheckMyPermissionInput } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
|
|
9
10
|
import type { OAuthAppInput } from "@stigmer/sdk";
|
|
10
11
|
import { StigmerContext } from "../../context";
|
|
11
12
|
import { OAuthAppDetailPanel } from "../OAuthAppDetailPanel";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
19
|
-
*
|
|
15
|
+
* Pins two things about the detail panel:
|
|
16
|
+
*
|
|
17
|
+
* - the full-spec-replace wipe bug: the panel must spread
|
|
18
|
+
* `toOAuthAppUpdateInput` and override only what it edits, so a spec
|
|
19
|
+
* field the form does not know about survives the save. The secret has
|
|
20
|
+
* its own contract: an empty secret input sends the fetched value — the
|
|
21
|
+
* server's redaction marker — which the update pipeline treats as "keep
|
|
22
|
+
* the stored secret" (oss#395 pins the marker/ciphertext boundary);
|
|
23
|
+
* - Edit is offered only with `can_edit` and Delete only with
|
|
24
|
+
* `can_delete` on the app, so an organization admin who may view an app
|
|
25
|
+
* they did not create is never handed a control the server would
|
|
26
|
+
* refuse (both belong to the creator; stigmer/stigmer#1257).
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
29
|
const REDACTED = "***REDACTED***";
|
|
@@ -39,9 +46,15 @@ const APP: OAuthApp = create(OAuthAppSchema, {
|
|
|
39
46
|
},
|
|
40
47
|
});
|
|
41
48
|
|
|
42
|
-
function renderPanel(
|
|
49
|
+
function renderPanel(
|
|
50
|
+
update: ReturnType<typeof vi.fn>,
|
|
51
|
+
checkMyPermission: ReturnType<typeof vi.fn> = vi.fn(async () => ({
|
|
52
|
+
isAuthorized: true,
|
|
53
|
+
})),
|
|
54
|
+
) {
|
|
43
55
|
const client = {
|
|
44
56
|
oauthapp: { update },
|
|
57
|
+
iamPolicy: { checkMyPermission },
|
|
45
58
|
} as never;
|
|
46
59
|
return render(
|
|
47
60
|
<StigmerContext.Provider value={client}>
|
|
@@ -57,7 +70,7 @@ describe("OAuthAppDetailPanel save payload", () => {
|
|
|
57
70
|
const update = vi.fn(async (_input: OAuthAppInput) => APP);
|
|
58
71
|
renderPanel(update);
|
|
59
72
|
|
|
60
|
-
fireEvent.click(screen.
|
|
73
|
+
fireEvent.click(await screen.findByRole("button", { name: "Edit" }));
|
|
61
74
|
const provider = await screen.findByLabelText("Provider");
|
|
62
75
|
fireEvent.change(provider, { target: { value: "github-enterprise" } });
|
|
63
76
|
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
@@ -83,7 +96,7 @@ describe("OAuthAppDetailPanel save payload", () => {
|
|
|
83
96
|
const update = vi.fn(async (_input: OAuthAppInput) => APP);
|
|
84
97
|
renderPanel(update);
|
|
85
98
|
|
|
86
|
-
fireEvent.click(screen.
|
|
99
|
+
fireEvent.click(await screen.findByRole("button", { name: "Edit" }));
|
|
87
100
|
const secret = await screen.findByLabelText("Client secret");
|
|
88
101
|
fireEvent.change(secret, { target: { value: "new-secret" } });
|
|
89
102
|
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
@@ -92,3 +105,38 @@ describe("OAuthAppDetailPanel save payload", () => {
|
|
|
92
105
|
expect(update.mock.calls[0]![0].clientSecret).toBe("new-secret");
|
|
93
106
|
});
|
|
94
107
|
});
|
|
108
|
+
|
|
109
|
+
describe("OAuthAppDetailPanel edit and delete gates", () => {
|
|
110
|
+
it("offers Edit only with can_edit and Delete only with can_delete on the app", async () => {
|
|
111
|
+
const checkMyPermission = vi.fn(async (input: CheckMyPermissionInput) => ({
|
|
112
|
+
isAuthorized: input.relation === "can_delete",
|
|
113
|
+
}));
|
|
114
|
+
renderPanel(vi.fn(), checkMyPermission);
|
|
115
|
+
|
|
116
|
+
await screen.findByRole("button", { name: "Delete" });
|
|
117
|
+
expect(screen.queryByRole("button", { name: "Edit" })).toBeNull();
|
|
118
|
+
const asked = checkMyPermission.mock.calls.map(
|
|
119
|
+
([input]) =>
|
|
120
|
+
`${input.resource?.kind}:${input.resource?.id}:${input.relation}`,
|
|
121
|
+
);
|
|
122
|
+
expect(asked.sort()).toEqual([
|
|
123
|
+
"oauth_app:oa-1:can_delete",
|
|
124
|
+
"oauth_app:oa-1:can_edit",
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("an admin who did not create the app reads it with neither control", async () => {
|
|
129
|
+
const checkMyPermission = vi.fn(async (_input: CheckMyPermissionInput) => ({
|
|
130
|
+
isAuthorized: false,
|
|
131
|
+
}));
|
|
132
|
+
renderPanel(vi.fn(), checkMyPermission);
|
|
133
|
+
|
|
134
|
+
await waitFor(() => expect(checkMyPermission).toHaveBeenCalledTimes(2));
|
|
135
|
+
// The configuration stays readable; only the change affordances are withheld.
|
|
136
|
+
expect(screen.getByRole("heading", { name: "github" })).toBeTruthy();
|
|
137
|
+
await waitFor(() =>
|
|
138
|
+
expect(screen.queryByRole("button", { name: "Edit" })).toBeNull(),
|
|
139
|
+
);
|
|
140
|
+
expect(screen.queryByRole("button", { name: "Delete" })).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth-app copy shared by more than one surface, so the settings section
|
|
3
|
+
* and anything else that lists an organization's OAuth apps say the same
|
|
4
|
+
* thing.
|
|
5
|
+
*
|
|
6
|
+
* OAuth apps are administrative infrastructure: an app carries its
|
|
7
|
+
* organization's vendor credentials and endpoints, so its creator and the
|
|
8
|
+
* organization's admins see it and other members may not see it at all
|
|
9
|
+
* (stigmer/stigmer#1257). An empty list is therefore not evidence that none
|
|
10
|
+
* is configured, so a caller who may not create OAuth apps is told who
|
|
11
|
+
* manages them instead of being told that nothing exists.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Shown in place of the empty state to a caller who may not create OAuth apps. */
|
|
15
|
+
export const OAUTH_APPS_MANAGED_BY_ADMINS =
|
|
16
|
+
"OAuth apps are managed by your organization's admins.";
|
|
@@ -22,9 +22,10 @@ export interface UseDeleteOAuthAppReturn {
|
|
|
22
22
|
* state.
|
|
23
23
|
*
|
|
24
24
|
* Deletes an OAuth app by its resource ID. Returns the deleted
|
|
25
|
-
* {@link OAuthApp} on success
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* {@link OAuthApp} on success, its client secret redacted like every
|
|
26
|
+
* read, so callers can confirm which app was removed. The deletion is
|
|
27
|
+
* permanent, and the server refuses it while an MCP server's
|
|
28
|
+
* `oauth_app_ref` still resolves to the app.
|
|
28
29
|
*
|
|
29
30
|
* @example
|
|
30
31
|
* ```tsx
|
|
@@ -21,11 +21,13 @@ export interface UseOAuthAppListReturn {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Data hook that fetches
|
|
24
|
+
* Data hook that fetches the {@link OAuthApp} entries of an organization
|
|
25
|
+
* that the caller may view.
|
|
25
26
|
*
|
|
26
|
-
* Returns
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Returns the organization's OAuthApps that a `get` would return to the
|
|
28
|
+
* caller: each app's creator and the organization's admins see it, other
|
|
29
|
+
* members and viewers see none. In practice these are the BYOA OAuth apps
|
|
30
|
+
* created through the "Bring your own app" flow on MCP server detail pages.
|
|
29
31
|
*
|
|
30
32
|
* Pass `null` for `org` to skip fetching (stable no-op). Useful when
|
|
31
33
|
* the active organization has not been resolved yet.
|