Support sudo password flow for shell commands

This commit is contained in:
2026-05-20 22:45:19 +08:00
parent 12bed7b6ba
commit 45b70d2800
11 changed files with 546 additions and 87 deletions
+86 -1
View File
@@ -145,7 +145,6 @@ function formatToolCommand(tool, args) {
if (tool === "file_write") return `$ file_write ${args.path || ""}`.trim();
if (tool === "list_dir") return `$ list_dir ${args.path || "."}`.trim();
if (tool === "search_files") return `$ search_files ${args.query || ""}`.trim();
if (tool === "os_update_check") return "$ os_update_check";
return `$ ${tool || "tool"}`;
}
@@ -213,6 +212,41 @@ function appendApprovalTerminal(article, eventPayload) {
terminal?.append(createInlineApprovalActions(payload.approval_id, command));
}
function appendPasswordPrompt(article, eventPayload) {
const payload = eventPayload.payload || eventPayload;
const terminal = article?.querySelector(".tool-terminal.is-error") || article?.querySelector(".tool-terminal");
if (!terminal) return;
const body = terminal.querySelector(".tool-terminal-body");
const status = terminal.querySelector(".tool-terminal-status");
terminal.classList.add("is-waiting");
terminal.dataset.approvalId = payload.approval_id || terminal.dataset.approvalId || "";
terminal.dataset.taskId = eventPayload.task_id || terminal.dataset.taskId || "";
if (status) status.textContent = "password";
if (body) {
body.textContent = [
body.textContent.trim(),
"",
payload.reason || "Sudo password is required to continue.",
].filter(Boolean).join("\n");
}
if (!terminal.querySelector(".tool-password-form")) terminal.append(createPasswordForm());
}
function createPasswordForm() {
const form = document.createElement("form");
form.className = "tool-password-form";
const input = document.createElement("input");
input.type = "password";
input.autocomplete = "current-password";
input.placeholder = "Sudo password";
input.required = true;
const button = document.createElement("button");
button.type = "submit";
button.textContent = "Continue";
form.append(input, button);
return form;
}
function createInlineApprovalActions(approvalId, command) {
const actions = document.createElement("div");
actions.className = "tool-approval-actions";
@@ -267,6 +301,22 @@ async function resolveInlineApproval(button) {
}
}
async function submitToolPassword(form) {
const terminal = form.closest(".tool-terminal");
const article = terminal?.closest(".message");
const taskId = terminal?.dataset.taskId;
const approvalId = terminal?.dataset.approvalId;
const input = form.querySelector("input");
const password = input?.value || "";
if (!terminal || !article || !taskId || !approvalId || !password) return;
form.querySelectorAll("input, button").forEach((item) => {
item.disabled = true;
});
await continueAfterPassword(article, taskId, approvalId, password);
input.value = "";
form.remove();
}
function humanApprovalDecision(action) {
if (action === "allow_once") return "разрешено один раз";
if (action === "allow_forever") return "разрешено навсегда";
@@ -423,6 +473,11 @@ async function handleAssistantStreamEvent(pending, name, data, context) {
appendApprovalTerminal(pending, data);
return;
}
if (name === "tool_password_requested") {
pending.querySelector(".message-meta span").textContent = "password";
appendPasswordPrompt(pending, data);
return;
}
if (name === "content_delta") {
if (!context.contentStarted) {
context.contentStarted = true;
@@ -473,6 +528,30 @@ async function continueAfterInlineApproval(article, taskId, approvalId) {
}
}
async function continueAfterPassword(article, taskId, approvalId, password) {
if (!article || state.running) return;
state.running = true;
document.querySelector("#run").disabled = true;
setStatus("#task-status", taskId, "warn");
const context = {taskId, contentStarted: false};
try {
await streamSse(
`/v1/tasks/${encodeURIComponent(taskId)}/password/stream`,
{approval_id: approvalId, password},
async ({name, data}) => handleAssistantStreamEvent(article, name, data, context),
);
} catch (error) {
setMessagePending(article, error.message);
article.querySelector(".message-meta span").textContent = "failed";
setStatus("#task-status", "failed", "bad");
await refreshEvents(taskId);
} finally {
state.running = false;
document.querySelector("#run").disabled = false;
document.querySelector("#message")?.focus();
}
}
async function refreshConversations() {
const list = document.querySelector("#conversation-list");
if (!list) return;
@@ -611,6 +690,12 @@ function bindChat() {
const button = event.target.closest(".message-reasoning-toggle");
if (button) toggleInlineReasoning(button);
});
document.querySelector("#messages")?.addEventListener("submit", (event) => {
const form = event.target.closest(".tool-password-form");
if (!form) return;
event.preventDefault();
submitToolPassword(form).catch(console.error);
});
document.querySelector("#debug")?.addEventListener("change", (event) => {
document.querySelector("#debug-panel").hidden = !event.target.checked;
});
+32
View File
@@ -635,6 +635,38 @@ dd {
opacity: 0.65;
}
.tool-password-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
padding: 10px 12px 12px;
border-top: 1px solid #1e293b;
background: #111827;
}
.tool-password-form input {
min-width: 0;
border-color: #334155;
background: #0f172a;
color: #f8fafc;
}
.tool-password-form button {
border: 0;
border-radius: 7px;
padding: 8px 10px;
background: #2563eb;
color: #ffffff;
font-size: 12px;
font-weight: 800;
}
.tool-password-form input:disabled,
.tool-password-form button:disabled {
cursor: wait;
opacity: 0.65;
}
.message-meta {
display: flex;
align-items: center;