@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.
Files changed (47) hide show
  1. package/execution/McpToolDetail.d.ts +4 -1
  2. package/execution/McpToolDetail.d.ts.map +1 -1
  3. package/execution/McpToolDetail.js +6 -2
  4. package/execution/McpToolDetail.js.map +1 -1
  5. package/execution/ToolArgsView.d.ts.map +1 -1
  6. package/execution/ToolArgsView.js +9 -4
  7. package/execution/ToolArgsView.js.map +1 -1
  8. package/execution/tool-categories.d.ts.map +1 -1
  9. package/execution/tool-categories.js +6 -4
  10. package/execution/tool-categories.js.map +1 -1
  11. package/oauth-app/OAuthAppDetailPanel.d.ts +5 -1
  12. package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -1
  13. package/oauth-app/OAuthAppDetailPanel.js +10 -5
  14. package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
  15. package/oauth-app/OAuthAppListPanel.d.ts +11 -3
  16. package/oauth-app/OAuthAppListPanel.d.ts.map +1 -1
  17. package/oauth-app/OAuthAppListPanel.js +4 -4
  18. package/oauth-app/OAuthAppListPanel.js.map +1 -1
  19. package/oauth-app/copy.d.ts +15 -0
  20. package/oauth-app/copy.d.ts.map +1 -0
  21. package/oauth-app/copy.js +15 -0
  22. package/oauth-app/copy.js.map +1 -0
  23. package/oauth-app/useDeleteOAuthApp.d.ts +4 -3
  24. package/oauth-app/useDeleteOAuthApp.d.ts.map +1 -1
  25. package/oauth-app/useDeleteOAuthApp.js +4 -3
  26. package/oauth-app/useDeleteOAuthApp.js.map +1 -1
  27. package/oauth-app/useOAuthAppList.d.ts +6 -4
  28. package/oauth-app/useOAuthAppList.d.ts.map +1 -1
  29. package/oauth-app/useOAuthAppList.js +6 -4
  30. package/oauth-app/useOAuthAppList.js.map +1 -1
  31. package/package.json +5 -5
  32. package/settings/OAuthAppsSection.d.ts +9 -1
  33. package/settings/OAuthAppsSection.d.ts.map +1 -1
  34. package/settings/OAuthAppsSection.js +17 -3
  35. package/settings/OAuthAppsSection.js.map +1 -1
  36. package/src/execution/McpToolDetail.tsx +6 -2
  37. package/src/execution/ToolArgsView.tsx +9 -4
  38. package/src/execution/__tests__/secret-args.test.tsx +81 -0
  39. package/src/execution/tool-categories.ts +6 -3
  40. package/src/oauth-app/OAuthAppDetailPanel.tsx +39 -24
  41. package/src/oauth-app/OAuthAppListPanel.tsx +12 -3
  42. package/src/oauth-app/__tests__/OAuthAppDetailPanel.test.tsx +57 -9
  43. package/src/oauth-app/copy.ts +16 -0
  44. package/src/oauth-app/useDeleteOAuthApp.ts +4 -3
  45. package/src/oauth-app/useOAuthAppList.ts +6 -4
  46. package/src/settings/OAuthAppsSection.tsx +21 -3
  47. 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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,qBAAqB,CAevB"}
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 all {@link OAuthApp} entries for an organization.
7
+ * Data hook that fetches the {@link OAuthApp} entries of an organization
8
+ * that the caller may view.
8
9
  *
9
- * Returns every OAuthApp whose `metadata.org` matches the input. In
10
- * practice these are the BYOA OAuth apps created through the "Bring
11
- * your own app" flow on MCP server detail pages.
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;;;;;;;;;;;;;;;;;GAiBG;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"}
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.29.0",
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.29.0",
41
- "@stigmer/theme": "3.29.0",
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.29.0",
65
- "@stigmer/sdk": "3.29.0",
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
- /** Settings section for organization OAuth app credentials. */
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":"AAgBA,+DAA+D;AAC/D,wBAAgB,gBAAgB,4CAmF/B"}
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
- /** Settings section for organization OAuth app credentials. */
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":"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"}
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 keys = Object.keys(args);
266
- if (keys.length > 0) {
267
- const val = args[keys[0]];
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 keys = Object.keys(toolCall.args);
419
- if (keys.length > 0) {
420
- const val = toolCall.args[keys[0]];
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" and "Delete" buttons.
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
- <button
227
- type="button"
228
- onClick={enterEdit}
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
- Edit
236
- </button>
237
- <button
238
- type="button"
239
- onClick={() => {
240
- clearDeleteError();
241
- setConfirmingDelete(true);
242
- }}
243
- className={cn(
244
- "stg:shrink-0 stg:rounded-md stg:px-2.5 stg:py-1.5 stg:text-xs stg:font-medium",
245
- "stg:text-destructive-muted stg:hover:text-destructive stg:hover:bg-destructive-subtle",
246
- "stg:transition-colors",
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
- Delete
250
- </button>
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 a list of {@link OAuthApp} resources owned by an
32
- * organization.
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
- No OAuth apps configured yet.
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
- * Regression suite for the full-spec-replace wipe bug: the panel must
15
- * spread `toOAuthAppUpdateInput` and override only what it edits, so a
16
- * spec field the form does not know about survives the save. The secret
17
- * has its own contract: an empty secret input sends the fetched value —
18
- * the server's redaction marker — which the update pipeline treats as
19
- * "keep the stored secret" (oss#395 pins the marker/ciphertext boundary).
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(update: ReturnType<typeof vi.fn>) {
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.getByRole("button", { name: "Edit" }));
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.getByRole("button", { name: "Edit" }));
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 so callers can confirm which app was
26
- * removed. The deletion is permanent — any MCP server overrides
27
- * referencing this app will lose their binding.
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 all {@link OAuthApp} entries for an organization.
24
+ * Data hook that fetches the {@link OAuthApp} entries of an organization
25
+ * that the caller may view.
25
26
  *
26
- * Returns every OAuthApp whose `metadata.org` matches the input. In
27
- * practice these are the BYOA OAuth apps created through the "Bring
28
- * your own app" flow on MCP server detail pages.
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.