- 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
Unified · Split
| @@ -34,12 +34,12 @@ const workspaceExecutorPackages = Object.keys(webPackage.dependencies ?? {}).fil · 33 unmodified lines | ||
| 34 | 34 | (name) => name.startsWith("@executor/"), |
| 35 | 35 | ); |
| 36 | 36 | |
| 37 | −const pluginWorkspaceSegment = `${path.sep}plugins${path.sep}`; | |
| 37 | +const pluginsWorkspaceRoot = path.resolve(import.meta.dirname, "../../plugins") + path.sep; | |
| 38 | 38 | |
| 39 | 39 | const reloadOnPluginChange = () => ({ |
| 40 | 40 | name: "executor-reload-on-plugin-change", |
| 41 | 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 | 43 | return; |
| 44 | 44 | } |
| 45 | 45 | |
| @@ -91,6 +91,9 @@ export default defineConfig({ · 45 unmodified lines | ||
| 91 | 91 | ignored: [ |
| 92 | 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 | }); |
| @@ -13,6 +13,7 @@ import { resolveToolPermission, type ToolPermissionLevel } from "./tool-permissi · 12 unmodified lines | ||
| 13 | 13 | import { Badge } from "./ui/badge"; |
| 14 | 14 | import { LoadableBlock } from "./loadable"; |
| 15 | 15 | import { |
| 16 | + SourceToolDetailPanel, | |
| 16 | 17 | SourceToolModelWorkbench, |
| 17 | 18 | type ToolPermissionChangeRequest, |
| 18 | 19 | } from "./source-tool-workbench"; |
| @@ -32,6 +33,8 @@ export const SourceToolExplorer = (props: { · 13 unmodified lines | ||
| 32 | 33 | actions?: ReactNode; |
| 33 | 34 | summary?: ReactNode; |
| 34 | 35 | policies?: ReadonlyArray<LocalScopePolicy>; |
| 36 | + renderHeaderMeta?: (detail: SourceInspectionToolDetail) => ReactNode; | |
| 37 | + renderSchemaExtras?: (detail: SourceInspectionToolDetail) => ReactNode; | |
| 35 | 38 | renderDetail?: (detail: SourceInspectionToolDetail) => ReactNode; |
| 36 | 39 | }) => { |
| 37 | 40 | const inspection = useSourceInspection(props.sourceId); |
| @@ -171,7 +174,20 @@ export const SourceToolExplorer = (props: { · 133 unmodified lines | ||
| 171 | 174 | }); |
| 172 | 175 | }} |
| 173 | 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 | 192 | </div> |
| 177 | 193 | </div> |
| @@ -455,8 +455,12 @@ const SourceToolListItem = (props: { · 454 unmodified lines | ||
| 455 | 455 | {highlightMatch(label, props.search)} |
| 456 | 456 | </span> |
| 457 | 457 | {props.tool.method && <MethodBadge method={props.tool.method} />} |
| 458 | − {props.policies && props.policies.length > 0 && ( | |
| 459 | − <ToolPermissionDot toolPath={props.tool.path} policies={props.policies} /> | |
| 458 | + {props.policies && ( | |
| 459 | + <ToolPermissionDot | |
| 460 | + toolPath={props.tool.path} | |
| 461 | + policies={props.policies} | |
| 462 | + interaction={props.tool.interaction} | |
| 463 | + /> | |
| 460 | 464 | )} |
| 461 | 465 | </Button> |
| 462 | 466 | ); |
| @@ -594,7 +598,7 @@ export const SourceToolDetailPanel = (props: SourceToolDetailPanelProps) => { · 131 unmodified lines | ||
| 594 | 598 | {props.detail.summary.method && ( |
| 595 | 599 | <MethodBadge method={props.detail.summary.method} /> |
| 596 | 600 | )} |
| 597 | − {props.policies && props.policies.length > 0 && ( | |
| 601 | + {props.policies && ( | |
| 598 | 602 | <ToolPermissionBadge |
| 599 | 603 | toolPath={props.detail.summary.path} |
| 600 | 604 | policies={props.policies} |
| @@ -1,12 +1,10 @@ | ||
| 1 | 1 | import type { |
| 2 | 2 | LocalScopePolicy, |
| 3 | − LocalScopePolicyApprovalMode, | |
| 4 | − LocalScopePolicyEffect, | |
| 5 | 3 | } from "@executor/platform-sdk/schema"; |
| 6 | 4 | |
| 7 | 5 | import { cn } from "../lib/cn"; |
| 8 | 6 | |
| 9 | −export type ToolPermissionLevel = "auto-run" | "requires-approval" | "denied" | "unknown"; | |
| 7 | +export type ToolPermissionLevel = "auto-run" | "requires-approval" | "denied"; | |
| 10 | 8 | |
| 11 | 9 | const matchesGlob = (pattern: string, value: string): boolean => { |
| 12 | 10 | const escaped = pattern |
| @@ -21,6 +19,7 @@ const policySpecificity = (policy: LocalScopePolicy): number => · 8 unmodified lines | ||
| 21 | 19 | export const resolveToolPermission = ( |
| 22 | 20 | toolPath: string, |
| 23 | 21 | policies: ReadonlyArray<LocalScopePolicy>, |
| 22 | + toolInteraction?: "auto" | "required", | |
| 24 | 23 | ): { |
| 25 | 24 | level: ToolPermissionLevel; |
| 26 | 25 | matchedPolicy: LocalScopePolicy | null; |
| @@ -38,7 +37,10 @@ export const resolveToolPermission = ( · 11 unmodified lines | ||
| 38 | 37 | |
| 39 | 38 | const matched = matching[0]; |
| 40 | 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 | 46 | if (matched.effect === "deny") { |
| @@ -71,42 +73,43 @@ const permissionStyles: Record< · 26 unmodified lines | ||
| 71 | 73 | dotClass: "bg-red-500", |
| 72 | 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 | |
| 81 | 78 | export const ToolPermissionDot = (props: { |
| 82 | 79 | toolPath: string; |
| 83 | 80 | policies: ReadonlyArray<LocalScopePolicy>; |
| 81 | + interaction?: "auto" | "required"; | |
| 84 | 82 | className?: string; |
| 85 | 83 | }) => { |
| 86 | − const { level } = resolveToolPermission(props.toolPath, props.policies); | |
| 87 | − if (level === "unknown") return null; | |
| 88 | − | |
| 84 | + const { level } = resolveToolPermission(props.toolPath, props.policies, props.interaction); | |
| 89 | 85 | const style = permissionStyles[level]; |
| 90 | 86 | |
| 91 | 87 | return ( |
| 92 | 88 | <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 | 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 | |
| 99 | 102 | export const ToolPermissionBadge = (props: { |
| 100 | 103 | toolPath: string; |
| 101 | 104 | policies: ReadonlyArray<LocalScopePolicy>; |
| 105 | + interaction?: "auto" | "required"; | |
| 102 | 106 | className?: string; |
| 103 | 107 | }) => { |
| 104 | 108 | const { level, matchedPolicy } = resolveToolPermission( |
| 105 | 109 | props.toolPath, |
| 106 | 110 | props.policies, |
| 111 | + props.interaction, | |
| 107 | 112 | ); |
| 108 | − if (level === "unknown") return null; | |
| 109 | − | |
| 110 | 113 | const style = permissionStyles[level]; |
| 111 | 114 | |
| 112 | 115 | return ( |
| @@ -60,6 +60,7 @@ export const SourceInspectionSectionSchema = Schema.Union( · 59 unmodified lines | ||
| 60 | 60 | export const SourceInspectionToolListItemSchema = Schema.Struct({ |
| 61 | 61 | path: Schema.String, |
| 62 | 62 | method: Schema.NullOr(Schema.String), |
| 63 | + interaction: Schema.optional(Schema.Literal("auto", "required")), | |
| 63 | 64 | inputTypePreview: Schema.optional(Schema.String), |
| 64 | 65 | outputTypePreview: Schema.optional(Schema.String), |
| 65 | 66 | }); |
| @@ -117,6 +117,9 @@ const inspectionToolListItemFromTool = ( · 116 unmodified lines | ||
| 117 | 117 | ): SourceInspectionToolListItem => ({ |
| 118 | 118 | path: tool.path, |
| 119 | 119 | method: executableDetails(tool).method, |
| 120 | + ...(tool.descriptor.interaction | |
| 121 | + ? { interaction: tool.descriptor.interaction } | |
| 122 | + : {}), | |
| 120 | 123 | ...(tool.descriptor.contract?.inputTypePreview |
| 121 | 124 | ? { inputTypePreview: tool.descriptor.contract.inputTypePreview } |
| 122 | 125 | : {}), |
| @@ -22,7 +22,6 @@ import { · 21 unmodified lines | ||
| 22 | 22 | Input, |
| 23 | 23 | Label, |
| 24 | 24 | Select, |
| 25 | − SourceToolDetailPanel, | |
| 26 | 25 | SourceToolExplorer, |
| 27 | 26 | Textarea, |
| 28 | 27 | parseSourceToolExplorerSearch, |
| @@ -715,12 +714,7 @@ function GraphqlDetailExplorer(props: { · 686 unmodified lines | ||
| 715 | 714 | )} |
| 716 | 715 | </> |
| 717 | 716 | )} |
| 718 | − renderDetail={(detail) => ( | |
| 719 | − <SourceToolDetailPanel | |
| 720 | − detail={detail} | |
| 721 | − renderHeaderMeta={renderGraphqlToolHeaderMeta} | |
| 722 | − /> | |
| 723 | − )} | |
| 717 | + renderHeaderMeta={renderGraphqlToolHeaderMeta} | |
| 724 | 718 | /> |
| 725 | 719 | ); |
| 726 | 720 | } |
| @@ -20,7 +20,6 @@ import { · 19 unmodified lines | ||
| 20 | 20 | Input, |
| 21 | 21 | Label, |
| 22 | 22 | Select, |
| 23 | − SourceToolDetailPanel, | |
| 24 | 23 | SourceToolExplorer, |
| 25 | 24 | parseSourceToolExplorerSearch, |
| 26 | 25 | type SourceToolExplorerSearch, |
| @@ -861,13 +860,8 @@ function OpenApiSourceDetailPageReady(props: { · 834 unmodified lines | ||
| 861 | 860 | )} |
| 862 | 861 | </> |
| 863 | 862 | )} |
| 864 | − renderDetail={(detail) => ( | |
| 865 | − <SourceToolDetailPanel | |
| 866 | − detail={detail} | |
| 867 | − renderHeaderMeta={renderOpenApiToolHeaderMeta} | |
| 868 | − renderSchemaExtras={renderOpenApiToolSchemaExtras} | |
| 869 | − /> | |
| 870 | − )} | |
| 863 | + renderHeaderMeta={renderOpenApiToolHeaderMeta} | |
| 864 | + renderSchemaExtras={renderOpenApiToolSchemaExtras} | |
| 871 | 865 | /> |
| 872 | 866 | ); |
| 873 | 867 | } |