import { Bot } from "lucide-react"; import { useEffect, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { museDetectorCleanupAttestationSchema } from "@paperclipai/shared"; import type { MuseConnection, MusePairing, MuseAttestStopInput, MuseDetectorCleanupAttestationInput } from "@paperclipai/shared"; import { agentsApi } from "@/api/agents"; import { useMuseConnection, useMuseConnectionState } from "@/hooks/useMuseConnection"; import { queryKeys } from "@/lib/queryKeys"; import { formatDateTime } from "@/lib/utils"; import { AgentSetupPrompt } from "./AgentSetupPrompt"; import { MuseConnectionChecks } from "./new-agent/ExternalAgentInviteContent"; import { Input } from "./ui/input"; import { Button } from "./ui/button"; export interface MuseConnectionDetailsProps { connection: MuseConnection; pairing?: MusePairing | null; busy?: boolean; error?: string; offline?: boolean; onTest: () => void; onRepair: () => void; onPause: () => void; onDisconnect: () => void; onRefresh: () => void; onAttest: (input: MuseAttestStopInput) => void; onAttestDetectorCleanup?: (input: MuseDetectorCleanupAttestationInput) => void; } /** The settings view uses the same connection evidence as the invitation. */ export function MuseConnectionDetails({ connection, pairing, busy, error, offline = false, onTest, onRepair, onPause, onDisconnect, onRefresh, onAttest, onAttestDetectorCleanup }: MuseConnectionDetailsProps) { const b = connection.binding; const [attestation, setAttestation] = useState(null); const [confirmed, setConfirmed] = useState(false); const [detectorSnapshot, setDetectorSnapshot] = useState | null>(null); const [detectorHookId, setDetectorHookId] = useState(""); const [observationDigest, setObservationDigest] = useState(""); const [detectorObservedAt, setDetectorObservedAt] = useState(""); const detectorObservedAtValid = museDetectorCleanupAttestationSchema.shape.observation.shape.observedAt.safeParse(detectorObservedAt).success; const [detectorConfirmed, setDetectorConfirmed] = useState(false); const detectorIdentity = JSON.stringify([b?.id, b?.generation, b?.revision, b?.qualification?.id, b?.cleanup.hookScript, b?.cleanup.attestationEligible, offline]); useEffect(() => { setDetectorSnapshot(null); setObservationDigest(""); setDetectorObservedAt(""); setDetectorHookId(""); setDetectorConfirmed(false); }, [detectorIdentity]); const boundaryIdentity = JSON.stringify(b?.stop.boundary); const stopBindingRevision = b?.stop.bindingRevision; useEffect(() => { setAttestation(null); setConfirmed(false); }, [boundaryIdentity, stopBindingRevision]); const live = !!b && b.status !== "revoked"; const state = useMuseConnectionState(connection, { offline }); const testing = state.testing; const configure = !offline && connection.enabled && connection.canConfigureConnection && !!connection.publicOrigin; const timestamp = (value: string | null) => value ? formatDateTime(value, { includeSeconds: true }) : "Not observed"; return

Muse uses its own tools and permissions. Paperclip coordinates company tasks and questions. Provider usage and cost are unavailable.

{!connection.enabled &&

Muse is disabled in experimental settings. Connection cleanup and stop evidence remain available.

} {!connection.publicOrigin &&

An authenticated instance with a public HTTPS URL is required to connect Muse.

} {b && <>
{[ ["Last persisted public receiver contact", timestamp(b.lastReceiverContactAt)], ["Authenticated worker activity", timestamp(b.lastWorkerActivityAt)], ["Last verified reply", timestamp(b.lastVerifiedReplyAt)], ["Credential status", b.credentialState], ["Refresh credential expires", timestamp(b.refreshExpiresAt)], ["Client version", b.clientVersion ?? "Not reported"], ["Live assignments", String(b.liveAssignments)], ["Uncertain native operations", String(b.uncertainOperations)], ["Pending answers", String(b.pendingInputs)], ["Usage and cost", "Unavailable"], ].map(([label, value]) =>
{label}
{value}
)}

Receiver contact is persisted in batches and may lag by up to {Math.ceil(b.contactPersistenceLagMs / 1000)} seconds. Public receiver checks are unverified. Only an authenticated reply confirms Muse can work.

{b.status === "revoked" &&

Disconnected. Paperclip credentials and execution authority were revoked.

} {(b.cleanup.pending || b.status === "revoked") &&
{"detectorRemovalRequested" in b.cleanup && b.cleanup.detectorRemovalRequested === true && !b.cleanup.detectorRemoved &&

Detector removal requested; completion is not confirmed.

}

Detector removal: {b.cleanup.detectorEvidence?.source === "operator_runtime_attestation" ? "operator-attested" : b.cleanup.detectorRemoved ? "acknowledged" : "not confirmed"}. Worker quiescence: {b.cleanup.workerQuiescenceReported ? "reported" : "not confirmed"}.

{b.cleanup.authenticatedAfterRevocation === false &&

After disconnect, the receiver disables itself when it next reaches Paperclip. Cleanup cannot be confirmed through the revoked connection.

} {b.cleanup.expiresAt &&

Cleanup capability expires {formatDateTime(b.cleanup.expiresAt)}.

} {b.cleanup.detectorEvidence &&

Recorded {timestamp(b.cleanup.detectorEvidence.recordedAt)}: {b.cleanup.detectorEvidence.source === "operator_runtime_attestation" ? "operator attestation from a supported runtime hook listing" : "authenticated worker report"}. This is not independent provider confirmation.

} {b.cleanup.attestationEligible && onAttestDetectorCleanup && !detectorSnapshot && }

Detector removal, worker stopping, and sandbox cleanup are separate. Paperclip cannot verify every private Muse tool or stop all personal Muse conversations.

} {b.stop.status !== "none" &&

{b.stop.status === "operator_attested" ? "You attested that this assignment’s Muse worker stopped." : b.stop.status === "worker_reported" ? "Muse reported that this assignment’s worker is quiescent. Independent private effects remain unconfirmed." : "Paperclip fenced this assignment’s authority. Its external stop cannot be confirmed."}

{b.stop.nativeEffectsUnknown &&

Native effect outcomes remain unknown and block replacement work. Worker attestation does not resolve or replay those effects.

} {b.stop.boundary && stopBindingRevision == null && b.stop.status !== "operator_attested" &&

Stop binding revision is unavailable. Refresh to inspect this assignment before attesting.

} {b.stop.boundary && b.stop.status !== "operator_attested" && !attestation && }
} } {detectorSnapshot &&

Record an observation of this exact detector

{detectorSnapshot.hookScript}

Use Muse’s supported {"hooks.list({ include_disabled: true })"} after revocation to verify this exact registered script is absent. Disabled checks, silence, or a removal request are insufficient. The retained hook ID and observation are your report. Paperclip does not verify installation, query Muse, or remove the hook.

Use the digest of the secret-free runtime observation. Do not paste the listing, a URL, credentials, or private tool output.

Enter the actual runtime listing time, not the time you submit this form. Paperclip checks it against revocation and the qualification deadline.

} {attestation &&

Confirm the worker for this exact assignment stopped

{Object.entries(attestation.boundary).map(([key, value]) =>
{key}
{value === null ? "None" : String(value)}
)}
} {pairing && !b?.paired &&
}} description="Paste into your current Muse conversation. Complete Connect Paperclip if Muse asks, then approve the Paperclip hostname." />

One-use setup expires {formatDateTime(pairing.expiresAt)}. Muse keeps the connection credential in its secure vault. Hostname permission applies to future requests to this Paperclip hostname.

}
{live && } {connection.agentStatus !== "paused" && connection.agentStatus !== "terminated" && } {live && }
{error &&

{error}

}
; } export function MuseRunnerConnection({ companyId, agentId }: { companyId?: string; agentId?: string }) { const cache = useQueryClient(); const muse = useMuseConnection(companyId, agentId); const pause = useMutation({ mutationFn: () => agentsApi.pause(agentId!, companyId), onSuccess: () => { void muse.refresh(); void cache.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId!) }); if (companyId) void cache.invalidateQueries({ queryKey: queryKeys.agents.list(companyId) }); } }); if (!agentId) return

Save the agent, then connect your personal Muse here.

; const error = muse.state.error ?? muse.pair.error ?? muse.verify.error ?? muse.revoke.error ?? muse.attest.error ?? muse.attestDetector.error ?? pause.error; if (muse.identity.failed) return

Unable to confirm your account. Refresh to inspect this Muse connection.

; if (!muse.state.data) return

{muse.state.error ? "Unable to load the Muse connection." : "Loading Muse connection…"}

{error &&

{error.message}

}
; return { if (muse.binding) muse.verify.mutate(muse.binding); }} onRepair={muse.repair} onPause={() => pause.mutate()} onDisconnect={() => { if (muse.binding) muse.revoke.mutate(muse.binding); }} onRefresh={() => { void muse.state.refetch(); }} onAttest={input => muse.attest.mutate(input)} onAttestDetectorCleanup={input => muse.attestDetector.mutate(input)} />; }