History

Show resolved permission level on every tool in source views

- Add interaction field to SourceInspectionToolListItem so the API returns each tool's intrinsic interaction mode (auto/required) - Resolve actual permission level for every tool (no more "unknown" or "default") using the tool's interaction mode as fallback when no policy matches - Migrate OpenAPI and GraphQL plugins from renderDetail override to renderHeaderMeta/renderSchemaExtras props so policies and permission controls render in all plugin detail views - Fix Vite HMR for workspace packages: the reloadOnPluginChange handler was matching any path containing /plugins/ (including the react client's src/plugins/ subfolder), changed to only match the top-level plugins/ workspace directory - Enable polling in Vite watcher for reliable WSL2 file change detection

Rhys Sullivan committed 7 months ago

Commit 5ddc2861 parent 500303a8 files · +55 −37Browse files

Unified · Split

8 files
  1. apps/web/vite.config.ts +5 −2
  2. packages/clients/react/src/plugins/components/source-tool-explorer.tsx +17 −1
  3. packages/clients/react/src/plugins/components/source-tool-workbench.tsx +7 −3
  4. packages/clients/react/src/plugins/components/tool-permission-badge.tsx +19 −16
  5. packages/platform/sdk/src/schema/models/source-inspection.ts +1 −0
  6. packages/platform/sdk/src/sources/inspection.ts +3 −0
  7. plugins/graphql/react/components.tsx +1 −7
  8. plugins/openapi/react/components.tsx +2 −8
apps/web/vite.config.ts+5−2
@@ -34,12 +34,12 @@ const workspaceExecutorPackages = Object.keys(webPackage.dependencies ?? {}).fil · 33 unmodified lines
34 (name) => name.startsWith("@executor/"),34 (name) => name.startsWith("@executor/"),
35);35);
36 36
37const pluginWorkspaceSegment = `${path.sep}plugins${path.sep}`;37const pluginsWorkspaceRoot = path.resolve(import.meta.dirname, "../../plugins") + path.sep;
38 38
39const reloadOnPluginChange = () => ({39const reloadOnPluginChange = () => ({
40 name: "executor-reload-on-plugin-change",40 name: "executor-reload-on-plugin-change",
41 handleHotUpdate({ file, server }: { file: string; server: { hot: { send: (payload: { type: "full-reload" }) => void } } }) {41 handleHotUpdate({ file, server }: { file: string; server: { hot: { send: (payload: { type: "full-reload" }) => void } } }) {
42 if (!file.includes(pluginWorkspaceSegment)) {42 if (!file.startsWith(pluginsWorkspaceRoot)) {
43 return;43 return;
44 }44 }
45 45
@@ -91,6 +91,9 @@ export default defineConfig({ · 45 unmodified lines
91 ignored: [91 ignored: [
92 "!**/node_modules/@executor/**",92 "!**/node_modules/@executor/**",
93 ],93 ],
94 // WSL2 inotify doesn't reliably detect changes through symlinked workspace packages
95 usePolling: true,
96 interval: 500,
94 },97 },
95 },98 },
96});99});
packages/clients/react/src/plugins/components/source-tool-explorer.tsx+17−1
@@ -13,6 +13,7 @@ import { resolveToolPermission, type ToolPermissionLevel } from "./tool-permissi · 12 unmodified lines
13import { Badge } from "./ui/badge";13import { Badge } from "./ui/badge";
14import { LoadableBlock } from "./loadable";14import { LoadableBlock } from "./loadable";
15import {15import {
16 SourceToolDetailPanel,
16 SourceToolModelWorkbench,17 SourceToolModelWorkbench,
17 type ToolPermissionChangeRequest,18 type ToolPermissionChangeRequest,
18} from "./source-tool-workbench";19} from "./source-tool-workbench";
@@ -32,6 +33,8 @@ export const SourceToolExplorer = (props: { · 13 unmodified lines
32 actions?: ReactNode;33 actions?: ReactNode;
33 summary?: ReactNode;34 summary?: ReactNode;
34 policies?: ReadonlyArray<LocalScopePolicy>;35 policies?: ReadonlyArray<LocalScopePolicy>;
36 renderHeaderMeta?: (detail: SourceInspectionToolDetail) => ReactNode;
37 renderSchemaExtras?: (detail: SourceInspectionToolDetail) => ReactNode;
35 renderDetail?: (detail: SourceInspectionToolDetail) => ReactNode;38 renderDetail?: (detail: SourceInspectionToolDetail) => ReactNode;
36}) => {39}) => {
37 const inspection = useSourceInspection(props.sourceId);40 const inspection = useSourceInspection(props.sourceId);
@@ -171,7 +174,20 @@ export const SourceToolExplorer = (props: { · 133 unmodified lines
171 });174 });
172 }}175 }}
173 sourceId={props.sourceId}176 sourceId={props.sourceId}
174 renderDetail={props.renderDetail}177 renderDetail={props.renderDetail ?? (
178 (props.renderHeaderMeta || props.renderSchemaExtras)
179 ? (toolDetail) => (
180 <SourceToolDetailPanel
181 detail={toolDetail}
182 policies={policies}
183 onSetPermission={handleSetPermission}
184 permissionPending={permissionPending}
185 renderHeaderMeta={props.renderHeaderMeta}
186 renderSchemaExtras={props.renderSchemaExtras}
187 />
188 )
189 : undefined
190 )}
175 />191 />
176 </div>192 </div>
177 </div>193 </div>
packages/clients/react/src/plugins/components/source-tool-workbench.tsx+7−3
@@ -455,8 +455,12 @@ const SourceToolListItem = (props: { · 454 unmodified lines
455 {highlightMatch(label, props.search)}455 {highlightMatch(label, props.search)}
456 </span>456 </span>
457 {props.tool.method && <MethodBadge method={props.tool.method} />}457 {props.tool.method && <MethodBadge method={props.tool.method} />}
458 {props.policies && props.policies.length > 0 && (458 {props.policies && (
459 <ToolPermissionDot toolPath={props.tool.path} policies={props.policies} />459 <ToolPermissionDot
460 toolPath={props.tool.path}
461 policies={props.policies}
462 interaction={props.tool.interaction}
463 />
460 )}464 )}
461 </Button>465 </Button>
462 );466 );
@@ -594,7 +598,7 @@ export const SourceToolDetailPanel = (props: SourceToolDetailPanelProps) => { · 131 unmodified lines
594 {props.detail.summary.method && (598 {props.detail.summary.method && (
595 <MethodBadge method={props.detail.summary.method} />599 <MethodBadge method={props.detail.summary.method} />
596 )}600 )}
597 {props.policies && props.policies.length > 0 && (601 {props.policies && (
598 <ToolPermissionBadge602 <ToolPermissionBadge
599 toolPath={props.detail.summary.path}603 toolPath={props.detail.summary.path}
600 policies={props.policies}604 policies={props.policies}
packages/clients/react/src/plugins/components/tool-permission-badge.tsx+19−16
@@ -1,12 +1,10 @@
1import type {1import type {
2 LocalScopePolicy,2 LocalScopePolicy,
3 LocalScopePolicyApprovalMode,
4 LocalScopePolicyEffect,
5} from "@executor/platform-sdk/schema";3} from "@executor/platform-sdk/schema";
6 4
7import { cn } from "../lib/cn";5import { cn } from "../lib/cn";
8 6
9export type ToolPermissionLevel = "auto-run" | "requires-approval" | "denied" | "unknown";7export type ToolPermissionLevel = "auto-run" | "requires-approval" | "denied";
10 8
11const matchesGlob = (pattern: string, value: string): boolean => {9const matchesGlob = (pattern: string, value: string): boolean => {
12 const escaped = pattern10 const escaped = pattern
@@ -21,6 +19,7 @@ const policySpecificity = (policy: LocalScopePolicy): number => · 8 unmodified lines
21export const resolveToolPermission = (19export const resolveToolPermission = (
22 toolPath: string,20 toolPath: string,
23 policies: ReadonlyArray<LocalScopePolicy>,21 policies: ReadonlyArray<LocalScopePolicy>,
22 toolInteraction?: "auto" | "required",
24): {23): {
25 level: ToolPermissionLevel;24 level: ToolPermissionLevel;
26 matchedPolicy: LocalScopePolicy | null;25 matchedPolicy: LocalScopePolicy | null;
@@ -38,7 +37,10 @@ export const resolveToolPermission = ( · 11 unmodified lines
38 37
39 const matched = matching[0];38 const matched = matching[0];
40 if (!matched) {39 if (!matched) {
41 return { level: "unknown", matchedPolicy: null };40 // No explicit policy — resolve from the tool's intrinsic interaction mode
41 const defaultLevel: ToolPermissionLevel =
42 toolInteraction === "auto" ? "auto-run" : "requires-approval";
43 return { level: defaultLevel, matchedPolicy: null };
42 }44 }
43 45
44 if (matched.effect === "deny") {46 if (matched.effect === "deny") {
@@ -71,42 +73,43 @@ const permissionStyles: Record< · 26 unmodified lines
71 dotClass: "bg-red-500",73 dotClass: "bg-red-500",
72 textClass: "text-red-600 dark:text-red-400",74 textClass: "text-red-600 dark:text-red-400",
73 },75 },
74 unknown: {
75 label: "",
76 dotClass: "bg-muted-foreground/30",
77 textClass: "text-muted-foreground/50",
78 },
79};76};
80 77
81export const ToolPermissionDot = (props: {78export const ToolPermissionDot = (props: {
82 toolPath: string;79 toolPath: string;
83 policies: ReadonlyArray<LocalScopePolicy>;80 policies: ReadonlyArray<LocalScopePolicy>;
81 interaction?: "auto" | "required";
84 className?: string;82 className?: string;
85}) => {83}) => {
86 const { level } = resolveToolPermission(props.toolPath, props.policies);84 const { level } = resolveToolPermission(props.toolPath, props.policies, props.interaction);
87 if (level === "unknown") return null;
88
89 const style = permissionStyles[level];85 const style = permissionStyles[level];
90 86
91 return (87 return (
92 <span88 <span
93 className={cn("size-1.5 shrink-0 rounded-full", style.dotClass, props.className)}89 className={cn(
90 "inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-px text-[9px] font-medium leading-none",
91 style.textClass,
92 props.className,
93 )}
94 title={style.label}94 title={style.label}
95 />95 >
96 <span className={cn("size-1.5 rounded-full", style.dotClass)} />
97 {style.label}
98 </span>
96 );99 );
97};100};
98 101
99export const ToolPermissionBadge = (props: {102export const ToolPermissionBadge = (props: {
100 toolPath: string;103 toolPath: string;
101 policies: ReadonlyArray<LocalScopePolicy>;104 policies: ReadonlyArray<LocalScopePolicy>;
105 interaction?: "auto" | "required";
102 className?: string;106 className?: string;
103}) => {107}) => {
104 const { level, matchedPolicy } = resolveToolPermission(108 const { level, matchedPolicy } = resolveToolPermission(
105 props.toolPath,109 props.toolPath,
106 props.policies,110 props.policies,
111 props.interaction,
107 );112 );
108 if (level === "unknown") return null;
109
110 const style = permissionStyles[level];113 const style = permissionStyles[level];
111 114
112 return (115 return (
packages/platform/sdk/src/schema/models/source-inspection.ts+1−0
@@ -60,6 +60,7 @@ export const SourceInspectionSectionSchema = Schema.Union( · 59 unmodified lines
60export const SourceInspectionToolListItemSchema = Schema.Struct({60export const SourceInspectionToolListItemSchema = Schema.Struct({
61 path: Schema.String,61 path: Schema.String,
62 method: Schema.NullOr(Schema.String),62 method: Schema.NullOr(Schema.String),
63 interaction: Schema.optional(Schema.Literal("auto", "required")),
63 inputTypePreview: Schema.optional(Schema.String),64 inputTypePreview: Schema.optional(Schema.String),
64 outputTypePreview: Schema.optional(Schema.String),65 outputTypePreview: Schema.optional(Schema.String),
65});66});
packages/platform/sdk/src/sources/inspection.ts+3−0
@@ -117,6 +117,9 @@ const inspectionToolListItemFromTool = ( · 116 unmodified lines
117): SourceInspectionToolListItem => ({117): SourceInspectionToolListItem => ({
118 path: tool.path,118 path: tool.path,
119 method: executableDetails(tool).method,119 method: executableDetails(tool).method,
120 ...(tool.descriptor.interaction
121 ? { interaction: tool.descriptor.interaction }
122 : {}),
120 ...(tool.descriptor.contract?.inputTypePreview123 ...(tool.descriptor.contract?.inputTypePreview
121 ? { inputTypePreview: tool.descriptor.contract.inputTypePreview }124 ? { inputTypePreview: tool.descriptor.contract.inputTypePreview }
122 : {}),125 : {}),
plugins/graphql/react/components.tsx+1−7
@@ -22,7 +22,6 @@ import { · 21 unmodified lines
22 Input,22 Input,
23 Label,23 Label,
24 Select,24 Select,
25 SourceToolDetailPanel,
26 SourceToolExplorer,25 SourceToolExplorer,
27 Textarea,26 Textarea,
28 parseSourceToolExplorerSearch,27 parseSourceToolExplorerSearch,
@@ -715,12 +714,7 @@ function GraphqlDetailExplorer(props: { · 686 unmodified lines
715 )}714 )}
716 </>715 </>
717 )}716 )}
718 renderDetail={(detail) => (717 renderHeaderMeta={renderGraphqlToolHeaderMeta}
719 <SourceToolDetailPanel
720 detail={detail}
721 renderHeaderMeta={renderGraphqlToolHeaderMeta}
722 />
723 )}
724 />718 />
725 );719 );
726}720}
plugins/openapi/react/components.tsx+2−8
@@ -20,7 +20,6 @@ import { · 19 unmodified lines
20 Input,20 Input,
21 Label,21 Label,
22 Select,22 Select,
23 SourceToolDetailPanel,
24 SourceToolExplorer,23 SourceToolExplorer,
25 parseSourceToolExplorerSearch,24 parseSourceToolExplorerSearch,
26 type SourceToolExplorerSearch,25 type SourceToolExplorerSearch,
@@ -861,13 +860,8 @@ function OpenApiSourceDetailPageReady(props: { · 834 unmodified lines
861 )}860 )}
862 </>861 </>
863 )}862 )}
864 renderDetail={(detail) => (863 renderHeaderMeta={renderOpenApiToolHeaderMeta}
865 <SourceToolDetailPanel864 renderSchemaExtras={renderOpenApiToolSchemaExtras}
866 detail={detail}
867 renderHeaderMeta={renderOpenApiToolHeaderMeta}
868 renderSchemaExtras={renderOpenApiToolSchemaExtras}
869 />
870 )}
871 />865 />
872 );866 );
873}867}