mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-19 18:35:18 +02:00
add session and always permission scopes for command execution
Session-scoped permissions are stored in the run log and rebuilt by the state-builder, scoping them to a single run. Always-scoped permissions persist to security.json. The backend derives command names from the run log instead of receiving them from the frontend. Uses regex-based command parsing with subshell/parenthesis support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1f85bcf8ae
commit
53d48ab4f3
8 changed files with 126 additions and 38 deletions
|
|
@ -1570,9 +1570,14 @@ function App() {
|
|||
}
|
||||
}, [runId, isStopping, stopClickedAt])
|
||||
|
||||
const handlePermissionResponse = useCallback(async (toolCallId: string, subflow: string[], response: 'approve' | 'deny') => {
|
||||
const handlePermissionResponse = useCallback(async (
|
||||
toolCallId: string,
|
||||
subflow: string[],
|
||||
response: 'approve' | 'deny',
|
||||
scope?: 'once' | 'session' | 'always',
|
||||
) => {
|
||||
if (!runId) return
|
||||
|
||||
|
||||
// Optimistically update the UI immediately
|
||||
setPermissionResponses(prev => {
|
||||
const next = new Map(prev)
|
||||
|
|
@ -1584,11 +1589,11 @@ function App() {
|
|||
next.delete(toolCallId)
|
||||
return next
|
||||
})
|
||||
|
||||
|
||||
try {
|
||||
await window.ipc.invoke('runs:authorizePermission', {
|
||||
runId,
|
||||
authorization: { subflow, toolCallId, response }
|
||||
authorization: { subflow, toolCallId, response, scope }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to authorize permission:', error)
|
||||
|
|
@ -3057,6 +3062,8 @@ function App() {
|
|||
<PermissionRequest
|
||||
toolCall={permRequest.toolCall}
|
||||
onApprove={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve')}
|
||||
onApproveSession={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')}
|
||||
onApproveAlways={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')}
|
||||
onDeny={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')}
|
||||
isProcessing={isActive && isProcessing}
|
||||
response={response}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,14 @@
|
|||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AlertTriangleIcon, CheckCircleIcon, CheckIcon, XCircleIcon, XIcon } from "lucide-react";
|
||||
import { AlertTriangleIcon, CheckCircleIcon, CheckIcon, ChevronDownIcon, XCircleIcon, XIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { ToolCallPart } from "@x/shared/dist/message.js";
|
||||
import z from "zod";
|
||||
|
|
@ -11,6 +17,8 @@ import z from "zod";
|
|||
export type PermissionRequestProps = ComponentProps<"div"> & {
|
||||
toolCall: z.infer<typeof ToolCallPart>;
|
||||
onApprove?: () => void;
|
||||
onApproveSession?: () => void;
|
||||
onApproveAlways?: () => void;
|
||||
onDeny?: () => void;
|
||||
isProcessing?: boolean;
|
||||
response?: 'approve' | 'deny' | null;
|
||||
|
|
@ -20,6 +28,8 @@ export const PermissionRequest = ({
|
|||
className,
|
||||
toolCall,
|
||||
onApprove,
|
||||
onApproveSession,
|
||||
onApproveAlways,
|
||||
onDeny,
|
||||
isProcessing = false,
|
||||
response = null,
|
||||
|
|
@ -117,16 +127,40 @@ export const PermissionRequest = ({
|
|||
</div>
|
||||
{!isResponded && (
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onApprove}
|
||||
disabled={isProcessing}
|
||||
className="flex-1"
|
||||
>
|
||||
<CheckIcon className="size-4" />
|
||||
Approve
|
||||
</Button>
|
||||
<div className="flex flex-1 items-center">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onApprove}
|
||||
disabled={isProcessing}
|
||||
className={cn("flex-1", command && "rounded-r-none")}
|
||||
>
|
||||
<CheckIcon className="size-4" />
|
||||
Approve
|
||||
</Button>
|
||||
{command && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
disabled={isProcessing}
|
||||
className="rounded-l-none border-l border-l-primary-foreground/20 px-1.5"
|
||||
>
|
||||
<ChevronDownIcon className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={onApproveSession}>
|
||||
Allow for Session
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onApproveAlways}>
|
||||
Always Allow
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ interface ChatSidebarProps {
|
|||
pendingAskHumanRequests?: ChatTabViewState['pendingAskHumanRequests']
|
||||
allPermissionRequests?: ChatTabViewState['allPermissionRequests']
|
||||
permissionResponses?: ChatTabViewState['permissionResponses']
|
||||
onPermissionResponse?: (toolCallId: string, subflow: string[], response: PermissionResponse) => void
|
||||
onPermissionResponse?: (toolCallId: string, subflow: string[], response: PermissionResponse, scope?: 'once' | 'session' | 'always') => void
|
||||
onAskHumanResponse?: (toolCallId: string, subflow: string[], response: string) => void
|
||||
isToolOpenForTab?: (tabId: string, toolId: string) => boolean
|
||||
onToolOpenChangeForTab?: (tabId: string, toolId: string, open: boolean) => void
|
||||
|
|
@ -450,6 +450,8 @@ export function ChatSidebar({
|
|||
<PermissionRequest
|
||||
toolCall={permRequest.toolCall}
|
||||
onApprove={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve')}
|
||||
onApproveSession={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')}
|
||||
onApproveAlways={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')}
|
||||
onDeny={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')}
|
||||
isProcessing={isActive && isProcessing}
|
||||
response={response}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue