Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/scripts/run-studio-permission-browser.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.

set -euo pipefail

port="${1:?usage: $0 PORT BROWSER [CHANNEL]}"
browser="${2:?usage: $0 PORT BROWSER [CHANNEL]}"
channel="${3:-}"
slug="$browser${channel:+-$channel}"
artifact_dir="logs/playwright-permissions-$slug"
server_log="logs/studio-permissions-$slug.log"
studio_home="${UNSLOTH_STUDIO_HOME:-$HOME/.unsloth/studio}"
set --
if [ -n "${STUDIO_PERMISSION_FRONTEND:-}" ]; then
set -- -f "$STUDIO_PERMISSION_FRONTEND"
fi

mkdir -p "$artifact_dir"
unsloth studio reset-password
UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$port" "$@" \
>"$server_log" 2>&1 &
studio_pid=$!

cleanup() {
kill "$studio_pid" 2>/dev/null || true
wait "$studio_pid" 2>/dev/null || true
}
trap cleanup EXIT

healthy=0
for _ in $(seq 1 180); do
if curl -fs "http://127.0.0.1:$port/api/health" >/dev/null; then
healthy=1
break
fi
if ! kill -0 "$studio_pid" 2>/dev/null; then
tail -100 "$server_log" || true
exit 1
fi
sleep 1
done
if [ "$healthy" -ne 1 ]; then
tail -100 "$server_log" || true
exit 1
fi

old_password=$(cat "$studio_home/auth/.bootstrap_password")
new_password="CIPerm-$(python -c 'import secrets; print(secrets.token_urlsafe(16))')"
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
echo "::add-mask::$old_password"
echo "::add-mask::$new_password"
fi

export BASE_URL="http://127.0.0.1:$port"
export STUDIO_OLD_PW="$old_password"
export STUDIO_NEW_PW="$new_password"
export STUDIO_UI_STRICT=1
export STUDIO_UI_PERMISSION_ONLY=1
export STUDIO_UI_WALL_TIMEOUT_S=240
export STUDIO_PLAYWRIGHT_BROWSER="$browser"
export PW_ART_DIR="$artifact_dir"
if [ -n "$channel" ]; then
export STUDIO_PLAYWRIGHT_CHANNEL="$channel"
else
unset STUDIO_PLAYWRIGHT_CHANNEL || true
fi

python tests/studio/playwright_chat_ui.py
11 changes: 9 additions & 2 deletions .github/workflows/studio-mac-ui-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
- 'install.sh'
- 'pyproject.toml'
- 'tests/studio/**'
- '.github/scripts/run-studio-permission-browser.sh'
- '.github/workflows/studio-mac-ui-smoke.yml'
push:
branches: [main, pip]
Expand Down Expand Up @@ -96,7 +97,7 @@ jobs:
- name: Assert llama.cpp loads on this macOS
run: bash .github/scripts/assert-llama-loads.sh

- name: Install Playwright + Chromium
- name: Install Playwright browsers
# No --with-deps on Mac: that flag installs Linux apt packages.
# GitHub-hosted macos-14 ships the system frameworks Chromium
# needs already.
Expand All @@ -112,7 +113,7 @@ jobs:
# in-script retry recover from any residual flakes.
run: |
pip install 'playwright>=1.55,<1.58'
python -m playwright install chromium
python -m playwright install chromium webkit

- name: Patch Playwright pipeTransport.js to tolerate malformed JSON
# In Playwright 1.55-1.58, pipeTransport.js does
Expand Down Expand Up @@ -244,6 +245,10 @@ jobs:
kill "${STUDIO_PID}" 2>/dev/null || true
sleep 2

- name: Cross-browser permission controls
run: |
bash .github/scripts/run-studio-permission-browser.sh 18895 webkit

- name: Reset auth + boot Studio for extra UI tests (port 18897)
run: |
unsloth studio reset-password
Expand Down Expand Up @@ -343,5 +348,7 @@ jobs:
logs/studio_extra.log
logs/install.log
logs/playwright
logs/playwright-permissions-*
logs/playwright_extra
logs/studio-permissions-*.log
retention-days: 7
16 changes: 11 additions & 5 deletions .github/workflows/studio-ui-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ on:
# The Playwright test files themselves -- a PR that ONLY edits
# the test must still trigger UI CI.
- 'tests/studio/**'
- '.github/scripts/run-studio-permission-browser.sh'
- '.github/workflows/studio-ui-smoke.yml'
push:
branches: [main, pip]
Expand Down Expand Up @@ -107,13 +108,10 @@ jobs:
set -o pipefail
bash install.sh --local --no-torch 2>&1 | tee logs/install.log

- name: Install Playwright + Chromium
- name: Install Playwright browsers
run: |
pip install 'playwright>=1.45'
# --with-deps installs the OS-level runtime libs Chromium
# needs (libnss3, libxkbcommon, etc.). About 30 s on a
# warm runner.
python -m playwright install --with-deps chromium
python -m playwright install --with-deps chromium firefox webkit

- name: Reset auth + boot Studio
run: |
Expand Down Expand Up @@ -182,6 +180,12 @@ jobs:
kill "${STUDIO_PID}" 2>/dev/null || true
sleep 2

- name: Cross-browser permission controls
run: |
bash .github/scripts/run-studio-permission-browser.sh 18893 firefox

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the permission helper in UI CI triggers

This new step depends on .github/scripts/run-studio-permission-browser.sh, but the workflow's pull_request.paths list only includes the app/test directories and this YAML, not the new helper script. A PR that changes only the helper can therefore alter the command these browser runs execute without running Studio UI CI, so regressions in the helper won't be caught until another workflow trigger happens; add the helper path to the trigger list.

Useful? React with 👍 / 👎.

bash .github/scripts/run-studio-permission-browser.sh 18893 webkit
bash .github/scripts/run-studio-permission-browser.sh 18893 chromium chrome

# The chat UI test ends by clicking the Shutdown menuitem, which
# leaves the server dead. The extra UI test (Compare / Recipes /
# Export / Studio / Settings) needs a fresh Studio, so we boot a
Expand Down Expand Up @@ -297,6 +301,8 @@ jobs:
logs/install.log
logs/server-logs/
logs/playwright
logs/playwright-permissions-*
logs/playwright_extra
logs/playwright_ime
logs/studio-permissions-*.log
retention-days: 7
7 changes: 7 additions & 0 deletions .github/workflows/studio-windows-ui-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
- 'install.ps1'
- 'pyproject.toml'
- 'tests/studio/**'
- '.github/scripts/run-studio-permission-browser.sh'
- '.github/workflows/studio-windows-ui-smoke.yml'
push:
branches: [main, pip]
Expand Down Expand Up @@ -345,6 +346,10 @@ jobs:
kill "${STUDIO_PID}" 2>/dev/null || true
sleep 2

- name: Edge permission controls
run: |
bash .github/scripts/run-studio-permission-browser.sh 18895 chromium msedge

- name: Reset auth + boot Studio for extra UI tests (port 18897)
run: |
unsloth studio reset-password
Expand Down Expand Up @@ -402,5 +407,7 @@ jobs:
logs/studio_extra.log
logs/install.log
logs/playwright
logs/playwright-permissions-*
logs/playwright_extra
logs/studio-permissions-*.log
retention-days: 7
11 changes: 5 additions & 6 deletions studio/frontend/src/components/assistant-ui/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1434,14 +1434,13 @@ const Composer: FC<{
const artifactsEnabled = useChatRuntimeStore((s) => s.artifactsEnabled);
const mcpEnabledForChat = useChatRuntimeStore((s) => s.mcpEnabledForChat);
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
const ragDisabled = useRagToolDisabled();
const permissionMode = useChatRuntimeStore((s) => s.permissionMode);
// More than 4 pills: collapse to icons only. Search and Code always show; the
// permission pill shows in every mode except "off" (it renders null there);
// Images, RAG, Canvas and MCP are conditional.
// More than 4 pills: collapse to icons only. Search, Code, and permissions
// always show; Images, RAG, Canvas and MCP are conditional.
const pillsCompact =
2 +
(permissionMode !== "off" ? 1 : 0) +
(ragEnabled ? 1 : 0) +
3 +
(ragEnabled && !ragDisabled ? 1 : 0) +
(supportsBuiltinImageGeneration ? 1 : 0) +
(artifactsEnabled ? 1 : 0) +
(mcpEnabledForChat ? 1 : 0) >
Expand Down
20 changes: 7 additions & 13 deletions studio/frontend/src/features/chat/bypass-permissions-menu-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,12 @@ import {
DropdownMenuSubTrigger,
} from "@/components/ui/dropdown-menu";
import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store";
import { PermissionModeMenuItems } from "./permission-mode-select";
import {
FULL_ACCESS_WARNING,
PermissionModeMenuItems,
} from "./permission-mode-select";

// "Bypass permissions" entry for the composer "+" -> More menu. Like the MCP
// pill, it opens a submenu where the user picks the permission level (Ask for
// approval / Approve for me / Full access). Picking Full access demands the
// danger warning; the other levels apply immediately. The menu closes normally
// on select (no preventDefault) -- the warning dialog lives outside the menu
// (BypassPermissionsConfirmDialog, mounted once at the chat-page root and
// driven by the store), so it survives the menu unmounting and the "+"/More
// popovers don't stay frozen.
// Tool permissions entry for the composer "+" menu.
export function BypassPermissionsMenuItem() {
const permissionMode = useChatRuntimeStore((s) => s.permissionMode);
const setBypassConfirmOpen = useChatRuntimeStore(
Expand All @@ -44,7 +40,7 @@ export function BypassPermissionsMenuItem() {
}
>
<HugeiconsIcon icon={ShieldBanIcon} strokeWidth={2} />
Bypass permissions
Tool permissions
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="unsloth-plus-menu w-[300px]">
<PermissionModeMenuItems
Expand Down Expand Up @@ -75,9 +71,7 @@ export function BypassPermissionsConfirmDialog() {
<AlertDialogHeader>
<AlertDialogTitle>Enable Full access?</AlertDialogTitle>
<AlertDialogDescription>
Full access (Bypass permissions) is dangerous since the AI model
might delete, corrupt your machine, and or cause real world damage
to you or the world - only accept if you are certain
{FULL_ACCESS_WARNING}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
Expand Down
11 changes: 6 additions & 5 deletions studio/frontend/src/features/chat/chat-settings-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2041,13 +2041,14 @@ function ConfirmToolCallsToggle() {
<InfoHint>
When on, every local Unsloth tool call pauses for your approval
before it runs (the "Ask for approval" level). When off, tool calls
run without prompts inside the sandbox (the "Off" level).
run without prompts inside the sandbox (the "Run automatically"
level).
Provider-hosted tools are not gated here.
</InfoHint>
</div>
{permissionMode === "full" ? (
<span className="text-[11px] text-muted-foreground">
Overridden by Full access (Bypass permissions)
Overridden by Full access
</span>
) : null}
</div>
Expand All @@ -2068,11 +2069,11 @@ function BypassPermissionsToggle() {
<div className="flex flex-col gap-2">
<div className="flex min-w-0 items-center gap-1.5">
<span className="whitespace-nowrap text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
Bypass permissions
Tool permissions
</span>
<InfoHint>
How Unsloth approves tool calls before they run. Full access is
dangerous: it disables confirmations and the code sandbox.
Choose how Unsloth approves tool calls before they run. Full access
disables confirmations and the code sandbox.
</InfoHint>
</div>
{/* Full width, styled like the panel selects/preset input. */}
Expand Down
Loading
Loading