security: refine workspace trust controls

This commit is contained in:
Rohit P
2026-07-24 18:44:39 -07:00
parent 8ee0a0d082
commit 3f5ac872ca
11 changed files with 427 additions and 9 deletions
@@ -1,12 +1,15 @@
import { useEffect, useState } from "react";
import {
getSettings,
getTrustedWorkspaces,
setOnboarded,
setPdfSettings,
setScratchBase,
setSessionsPeek,
setWorkspaceTrusted,
type ModelSettings,
type PdfSettings,
type WorkspaceCommandTrust,
} from "../api";
import {
cancelDictationModelDownload,
@@ -424,6 +427,8 @@ function AppearanceSection() {
<FilesCard />
<TrustedWorkspacesCard />
{desktop && (
<div className={CARD + " p-4"}>
<div className={FIELD_LABEL + " mb-2.5"}>Always-on</div>
@@ -461,6 +466,61 @@ function AppearanceSection() {
);
}
function TrustedWorkspacesCard() {
const [workspaces, setWorkspaces] = useState<WorkspaceCommandTrust[] | null>(null);
const refresh = () =>
getTrustedWorkspaces()
.then(setWorkspaces)
.catch(() => setWorkspaces([]));
useEffect(() => {
refresh();
}, []);
const revoke = async (path: string) => {
if (!window.confirm(`Revoke command trust for ${path}?`)) return;
await setWorkspaceTrusted(path, false);
refresh();
};
return (
<div className={CARD + " p-4 mb-4"} data-testid="trusted-workspaces-card">
<div className={FIELD_LABEL}>Trusted workspaces</div>
<div className={FIELD_HELP}>
Trusted projects may manage their command allowances in .coworker/config.toml.
</div>
{workspaces === null ? (
<div className="text-[12px] text-muted mt-3">Loading</div>
) : workspaces.length === 0 ? (
<div className="text-[12px] text-muted mt-3">No workspaces are trusted.</div>
) : (
<div className="mt-3 divide-y divide-line">
{workspaces.map((workspace) => (
<div key={workspace.workspace} className="py-2.5 flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-[12.5px] text-ink break-all">{workspace.workspace}</div>
<div className="text-[11.5px] text-muted mt-0.5">
{workspace.requested_commands.length
? `${workspace.requested_commands.length} project command allowance${workspace.requested_commands.length === 1 ? "" : "s"}`
: "No project command allowances currently declared"}
{!workspace.exists ? " · Folder unavailable" : ""}
</div>
</div>
<button
className="text-[12px] text-red-600 px-2 py-1"
onClick={() => void revoke(workspace.workspace)}
>
Revoke
</button>
</div>
))}
</div>
)}
</div>
);
}
function UpdateInline() {
const [state, setState] = useState<"idle" | "checking" | "none" | "found" | "installing" | "error">("idle");
const [version, setVersion] = useState("");
@@ -0,0 +1,56 @@
import { useState } from "react";
import { setWorkspaceTrusted, type WorkspaceCommandTrust } from "../api";
export function WorkspaceTrustPrompt({
request,
onClose,
}: {
request: WorkspaceCommandTrust;
onClose: () => void;
}) {
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const trust = async () => {
setSaving(true);
setError("");
const result = await setWorkspaceTrusted(request.workspace, true).catch(() => null);
setSaving(false);
if (!result?.ok) {
setError(result?.error || "Could not save workspace trust.");
return;
}
onClose();
};
return (
<div className="gate-overlay" role="dialog" aria-modal="true" aria-labelledby="workspace-trust-title">
<div className="gate max-w-[560px]">
<div className="gate-mark"></div>
<h2 id="workspace-trust-title">Trust this workspace&rsquo;s commands?</h2>
<p className="gate-sub">
This project asks OpenWorker to run the commands below without individual approval.
Trust applies to future configuration changes at this exact folder until you revoke it
in Settings.
</p>
<div className="rounded-lg border border-line bg-paper px-3 py-2.5 max-h-48 overflow-y-auto">
{request.requested_commands.map((command) => (
<code key={command} className="block text-[12.5px] py-1 text-ink">
{command}
</code>
))}
</div>
<div className="text-[11.5px] text-muted mt-2 break-all">{request.workspace}</div>
{error && <div className="gate-error">{error}</div>}
<div className="gate-foot justify-end gap-2">
<button className="btn" onClick={onClose} disabled={saving}>
Keep asking
</button>
<button className="btn primary" onClick={() => void trust()} disabled={saving}>
{saving ? "Saving…" : "Trust workspace"}
</button>
</div>
</div>
</div>
);
}