mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-12 07:10:09 +00:00
security: refine workspace trust controls
This commit is contained in:
@@ -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’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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user