Stufe 2 (#764): Einmal-Anmeldung fuer die Huelle ueber den Manager-Socket

Der Manager hoert zusaetzlich auf einem lokalen Socket (0600 bzw. Pipe nur
fuer den eigenen Benutzer). Dort gibt es nur /api/shell/*, ueber TCP gibt
es diese Pfade nicht. Ein Einmal-Code (256 Bit, einmal, hoechstens 60 s)
wird in der WebUI aus dem Fragment gegen das Token getauscht und sofort
aus der Adresse entfernt. Tests: zweimal, zu spaet, ueber TCP - alle
scheitern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-03 18:44:58 +02:00
co-authored by Claude Opus 5.5
parent c3927292fd
commit 775c4dbc8e
9 changed files with 434 additions and 18 deletions
@@ -121,7 +121,7 @@ public static class InstanceEndpoint
ClearStaleFile(McpSocketPath(instanceDirectory));
}
private static void ClearStaleFile(string path)
internal static void ClearStaleFile(string path)
{
try
{
@@ -152,7 +152,7 @@ public static class InstanceEndpoint
RestrictFile(McpSocketPath(instanceDirectory));
}
private static void RestrictFile(string path)
internal static void RestrictFile(string path)
{
if (OperatingSystem.IsWindows()) return;
@@ -185,7 +185,7 @@ public static class InstanceEndpoint
public static HttpMessageHandler CreateHandler(string instanceDirectory)
=> CreateHandler(SocketPath(instanceDirectory), PipeName(instanceDirectory));
private static HttpMessageHandler CreateHandler(string socketPath, string pipeName)
internal static HttpMessageHandler CreateHandler(string socketPath, string pipeName)
{
if (OperatingSystem.IsWindows())
{
@@ -239,7 +239,7 @@ public static class InstanceEndpoint
/// müssen denselben Pipenamen ergeben. Unter Linux sind es zwei Verzeichnisse und
/// dürfen es nicht.</para>
/// </summary>
private static string Fingerprint(string instanceDirectory)
internal static string Fingerprint(string instanceDirectory)
{
var full = Path.GetFullPath(instanceDirectory).TrimEnd(
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
@@ -0,0 +1,46 @@
namespace ClawdDotNet.Core.Instances;
/// <summary>
/// #764: The instance manager's local socket — the way the shell (ClawdDotNet.Shell) talks to
/// it without the web access token.
///
/// <para><b>Why a socket and not the web port.</b> The shell opens the web interface signed in,
/// through a one-time code. Asking for that code must prove that the asker is the user who runs
/// the manager on this machine — not anybody who reaches the port. The file system says that:
/// the socket is 0600 (Linux) or a pipe for the current user only (Windows), exactly like the
/// instance socket (<see cref="InstanceEndpoint"/>). Over TCP the shell endpoints do not exist.</para>
///
/// <para>It lives next to <c>manager.json</c>: the shell finds the manager the same way it finds
/// its port.</para>
/// </summary>
public static class ManagerEndpoint
{
public const string SocketFileName = "manager-shell.sock";
public static string SocketPath(string configDirectory)
=> Path.Combine(Path.GetFullPath(configDirectory), SocketFileName);
/// <summary>Pipe names are machine-wide; a fingerprint of the folder keeps two installations apart.</summary>
public static string PipeName(string configDirectory)
=> "clawd-manager-" + InstanceEndpoint.Fingerprint(configDirectory);
public static HttpClient CreateClient(string configDirectory)
=> new(InstanceEndpoint.CreateHandler(SocketPath(configDirectory), PipeName(configDirectory)))
{
BaseAddress = new Uri("http://manager")
};
/// <summary>A socket file left by a hard end would block the next bind (see <see cref="InstanceEndpoint.ClearStale"/>).</summary>
public static void ClearStale(string configDirectory)
{
if (OperatingSystem.IsWindows()) return;
InstanceEndpoint.ClearStaleFile(SocketPath(configDirectory));
}
/// <summary>0600 after binding; on Windows the pipe's CurrentUserOnly carries the same promise.</summary>
public static void Restrict(string configDirectory)
{
if (OperatingSystem.IsWindows()) return;
InstanceEndpoint.RestrictFile(SocketPath(configDirectory));
}
}