- 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 | (name) => name.startsWith("@executor/"), | 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 | const reloadOnPluginChange = () => ({ | 39 | const 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 | }); |
| @@ -13,6 +13,7 @@ import { resolveToolPermission, type ToolPermissionLevel } from "./tool-permissi · 12 unmodified lines | |||
| 13 | import { Badge } from "./ui/badge"; | 13 | import { Badge } from "./ui/badge"; |
| 14 | import { LoadableBlock } from "./loadable"; | 14 | import { LoadableBlock } from "./loadable"; |
| 15 | import { | 15 | import { |
| 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> |
| @@ -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 | <ToolPermissionBadge | 602 | <ToolPermissionBadge |
| 599 | toolPath={props.detail.summary.path} | 603 | toolPath={props.detail.summary.path} |
| 600 | policies={props.policies} | 604 | policies={props.policies} |
| @@ -1,12 +1,10 @@ | |||
| 1 | import type { | 1 | import type { |
| 2 | LocalScopePolicy, | 2 | LocalScopePolicy, |
| 3 | LocalScopePolicyApprovalMode, | ||
| 4 | LocalScopePolicyEffect, | ||
| 5 | } from "@executor/platform-sdk/schema"; | 3 | } from "@executor/platform-sdk/schema"; |
| 6 | 4 | ||
| 7 | import { cn } from "../lib/cn"; | 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 | const matchesGlob = (pattern: string, value: string): boolean => { | 9 | const matchesGlob = (pattern: string, value: string): boolean => { |
| 12 | const escaped = pattern | 10 | const escaped = pattern |
| @@ -21,6 +19,7 @@ const policySpecificity = (policy: LocalScopePolicy): number => · 8 unmodified lines | |||
| 21 | export const resolveToolPermission = ( | 19 | export 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 | ||
| 81 | export const ToolPermissionDot = (props: { | 78 | export 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 | <span | 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 | 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 | ||
| 99 | export const ToolPermissionBadge = (props: { | 102 | export 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 ( |
| @@ -60,6 +60,7 @@ export const SourceInspectionSectionSchema = Schema.Union( · 59 unmodified lines | |||
| 60 | export const SourceInspectionToolListItemSchema = Schema.Struct({ | 60 | export 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 | }); |
| @@ -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?.inputTypePreview | 123 | ...(tool.descriptor.contract?.inputTypePreview |
| 121 | ? { inputTypePreview: tool.descriptor.contract.inputTypePreview } | 124 | ? { inputTypePreview: tool.descriptor.contract.inputTypePreview } |
| 122 | : {}), | 125 | : {}), |
| @@ -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 | } |
| @@ -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 | <SourceToolDetailPanel | 864 | renderSchemaExtras={renderOpenApiToolSchemaExtras} |
| 866 | detail={detail} | ||
| 867 | renderHeaderMeta={renderOpenApiToolHeaderMeta} | ||
| 868 | renderSchemaExtras={renderOpenApiToolSchemaExtras} | ||
| 869 | /> | ||
| 870 | )} | ||
| 871 | /> | 865 | /> |
| 872 | ); | 866 | ); |
| 873 | } | 867 | } |