Standalone per-card browser model/security/view modules for HUX-01..10 plus node+pytest suites that read the hux.v1 contract schemas directly. Reconciled drift found on integration: the activity model now accepts all 32 hux.event.v1 kinds (delegation.*, memory.suppressed, memory.retrieval_removed, budget.exhausted, side_effect.*), the autonomy model carries the external_side_effect capability, and the foundation boundary test now asserts the shipped static HUX surface exists on disk and that images never bake activated HUX_FLAGS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
166 lines
5.9 KiB
TypeScript
166 lines
5.9 KiB
TypeScript
/** Accessible HUX-10 privacy status and explicit conversation controls. */
|
|
|
|
import {useEffect, useState} from "react";
|
|
|
|
import {privacyControls, privacyEnabled} from "./model.ts";
|
|
import type {
|
|
PrivacyClient,
|
|
PrivacyControl,
|
|
PrivacySnapshot,
|
|
} from "./types.ts";
|
|
|
|
interface PrivacyCenterProps {
|
|
flags?: Iterable<string>;
|
|
client: PrivacyClient;
|
|
snapshot: PrivacySnapshot | null;
|
|
disabled?: boolean;
|
|
onUpdated(snapshot: PrivacySnapshot, action: PrivacyControl): void;
|
|
}
|
|
|
|
const CONTROL_LABELS: Readonly<Record<PrivacyControl, string>> = {
|
|
enable_no_store: "Use no-store here",
|
|
disable_no_store: "Use standard retention",
|
|
forget_this_conversation: "Forget this conversation",
|
|
switch_to_private: "Switch to Private mode",
|
|
disable_memory_here: "Disable memory here",
|
|
dismiss: "Dismiss notice",
|
|
};
|
|
|
|
function readableTime(timestamp: string): string {
|
|
return timestamp.replace("T", " ").replace("Z", " UTC");
|
|
}
|
|
|
|
/** Render nothing unless both HUX-11 and HUX-10 are explicitly enabled. */
|
|
export function PrivacyCenter(props: PrivacyCenterProps) {
|
|
const [busy, setBusy] = useState<PrivacyControl | null>(null);
|
|
const [error, setError] = useState("");
|
|
const [confirmForget, setConfirmForget] = useState(false);
|
|
const enabled = privacyEnabled(props.flags) && props.client?.enabled();
|
|
const snapshot = props.snapshot;
|
|
|
|
useEffect(() => {
|
|
setBusy(null);
|
|
setError("");
|
|
setConfirmForget(false);
|
|
}, [snapshot?.revision]);
|
|
|
|
if (!enabled || !snapshot) return null;
|
|
const controls = privacyControls(snapshot);
|
|
const ordinaryControls = controls.filter((control) =>
|
|
control !== "forget_this_conversation" && control !== "dismiss");
|
|
|
|
async function apply(action: PrivacyControl): Promise<void> {
|
|
if (busy || props.disabled) return;
|
|
setBusy(action);
|
|
setError("");
|
|
try {
|
|
const updated = await props.client.control(snapshot, action);
|
|
props.onUpdated(updated, action);
|
|
} catch (_) {
|
|
setError("The privacy change could not be verified. Nothing was changed in this view.");
|
|
} finally {
|
|
setBusy(null);
|
|
setConfirmForget(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section className="hux-privacy" aria-labelledby="hux-privacy-heading">
|
|
<header className="hux-privacy-heading">
|
|
<div>
|
|
<span className="hux-privacy-kicker">Conversation privacy</span>
|
|
<h2 id="hux-privacy-heading">What Hermes keeps</h2>
|
|
</div>
|
|
<span className={`hux-privacy-state ${snapshot.noStore ? "is-private" : ""}`}
|
|
aria-live="polite">
|
|
{snapshot.noStore ? "No-store active" : "Standard retention"}
|
|
</span>
|
|
</header>
|
|
|
|
{snapshot.notice && (
|
|
<aside className="hux-privacy-notice" aria-labelledby="hux-privacy-notice-title">
|
|
<h3 id="hux-privacy-notice-title">Sensitive-topic protection</h3>
|
|
<p>{snapshot.notice.text}</p>
|
|
{snapshot.notice.controls.includes("dismiss") && (
|
|
<button type="button" disabled={Boolean(busy) || props.disabled}
|
|
onClick={() => void apply("dismiss")}>Dismiss notice</button>
|
|
)}
|
|
</aside>
|
|
)}
|
|
|
|
<dl className="hux-privacy-retention" aria-label="Effective retention">
|
|
<div>
|
|
<dt>Conversation</dt>
|
|
<dd>{snapshot.noStore ? "Session only; not retained afterward" :
|
|
"Retained under the workspace policy"}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Memory</dt>
|
|
<dd>{snapshot.memoryEnabled ? "Ask before saving sensitive facts" :
|
|
"Memory writes disabled here"}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Sensitive context</dt>
|
|
<dd>{snapshot.retention.topicContextDecayDays === null ?
|
|
"Not retained beyond this session" :
|
|
`Minimized within ${snapshot.retention.topicContextDecayDays} days`}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>After the topic changes</dt>
|
|
<dd>Previous sensitive details are not repeated</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Across surfaces</dt>
|
|
<dd>{snapshot.retention.crossSurfaceSharing === "never" ?
|
|
"Never shared" : "Same owner only"}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Retention audit</dt>
|
|
<dd>Every {snapshot.retention.auditIntervalDays} days; next{" "}
|
|
<time dateTime={snapshot.retention.nextAuditAt}>
|
|
{readableTime(snapshot.retention.nextAuditAt)}
|
|
</time>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
<div className="hux-privacy-controls" aria-busy={Boolean(busy)}>
|
|
{ordinaryControls.map((control) => (
|
|
<button key={control} type="button" disabled={Boolean(busy) || props.disabled}
|
|
onClick={() => void apply(control)}>
|
|
{CONTROL_LABELS[control]}
|
|
</button>
|
|
))}
|
|
{controls.includes("forget_this_conversation") && !confirmForget && (
|
|
<button type="button" className="hux-privacy-danger"
|
|
disabled={Boolean(busy) || props.disabled} onClick={() => setConfirmForget(true)}>
|
|
Forget this conversation
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{confirmForget && (
|
|
<div className="hux-privacy-confirm" role="group"
|
|
aria-labelledby="hux-privacy-confirm-title">
|
|
<strong id="hux-privacy-confirm-title">Remove this conversation and its saved context?</strong>
|
|
<span>This cannot be undone.</span>
|
|
<div>
|
|
<button type="button" className="hux-privacy-danger"
|
|
disabled={Boolean(busy) || props.disabled}
|
|
onClick={() => void apply("forget_this_conversation")}>
|
|
Confirm forget
|
|
</button>
|
|
<button type="button" disabled={Boolean(busy)}
|
|
onClick={() => setConfirmForget(false)}>Cancel</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="hux-privacy-status" role="status" aria-live="polite">
|
|
{busy ? "Applying privacy control…" : ""}
|
|
</div>
|
|
{error && <p className="hux-privacy-error" role="alert">{error}</p>}
|
|
</section>
|
|
);
|
|
}
|