Skip to content

Commit c109af5

Browse files
sharpninjaclaude
andcommitted
add-workspace: run trust validation when marker already exists
When AGENTS-README-FIRST.yaml is already present, skip registration and run signature + health nonce verification instead of just erroring. Extracted ValidateMarkerTrustAsync helper so both the new-registration and already-registered paths share the same trust check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1645eea commit c109af5

1 file changed

Lines changed: 59 additions & 49 deletions

File tree

src/McpServer.Director/Commands/DirectorCommands.cs

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,8 @@ private static Command BuildAddWorkspaceCommand()
446446

447447
if (File.Exists(markerPath))
448448
{
449-
Error($"Workspace is already registered — {markerPath} exists.");
449+
Info($"Workspace already registered — validating trust for {markerPath}...");
450+
await ValidateMarkerTrustAsync(workspacePath, markerPath).ConfigureAwait(true);
450451
return;
451452
}
452453

@@ -542,54 +543,8 @@ private static Command BuildAddWorkspaceCommand()
542543
return;
543544
}
544545

545-
// Step 4: Read the marker and verify trust
546-
var markerClient = McpHttpClient.FromMarkerOnly(workspacePath);
547-
if (markerClient is null)
548-
{
549-
Error("Marker file was created but could not be parsed.");
550-
return;
551-
}
552-
553-
// Step 5: Verify HMAC-SHA256 signature
554-
var lines = File.ReadAllLines(markerPath);
555-
var markerFields = ParseMarkerFields(lines);
556-
if (!VerifyMarkerSignature(markerFields, out var sigError))
557-
{
558-
Error($"Marker signature INVALID: {sigError}");
559-
markerClient.Dispose();
560-
return;
561-
}
562-
563-
Success("Marker signature verified.");
564-
565-
// Step 6: Health nonce echo verification
566-
var nonce = Guid.NewGuid().ToString("N");
567-
try
568-
{
569-
var healthResult = await markerClient.GetAsync<JsonElement>(
570-
$"/health?nonce={Uri.EscapeDataString(nonce)}").ConfigureAwait(true);
571-
var echoedNonce = healthResult.TryGetProperty("nonce", out var nonceProp)
572-
? nonceProp.GetString() : null;
573-
if (!string.Equals(echoedNonce, nonce, StringComparison.Ordinal))
574-
{
575-
Error($"Health nonce mismatch: sent '{nonce}', got '{echoedNonce}'.");
576-
markerClient.Dispose();
577-
return;
578-
}
579-
580-
Success("Health nonce verified.");
581-
}
582-
catch (Exception ex)
583-
{
584-
Error($"Health check failed: {ex.Message}");
585-
markerClient.Dispose();
586-
return;
587-
}
588-
589-
markerClient.Dispose();
590-
Success($"Workspace added and trusted: {workspacePath}");
591-
Info($"Marker: {markerPath}");
592-
Info($"API key: {markerClient.ApiKey[..Math.Min(8, markerClient.ApiKey.Length)]}...");
546+
// Step 4: Validate trust on the new marker
547+
await ValidateMarkerTrustAsync(workspacePath, markerPath).ConfigureAwait(true);
593548
}, s_workspaceOption, nameOption, serverOption);
594549

595550
return cmd;
@@ -640,6 +595,61 @@ private static async Task<bool> WaitForFileAsync(string filePath, TimeSpan timeo
640595
}
641596
}
642597

598+
/// <summary>
599+
/// Reads the marker file, verifies its HMAC-SHA256 signature, and performs
600+
/// a health nonce echo check. Prints success/error via <see cref="CommandHelpers"/>.
601+
/// </summary>
602+
private static async Task ValidateMarkerTrustAsync(string workspacePath, string markerPath)
603+
{
604+
var markerClient = McpHttpClient.FromMarkerOnly(workspacePath);
605+
if (markerClient is null)
606+
{
607+
Error("Marker file could not be parsed.");
608+
return;
609+
}
610+
611+
// Signature verification
612+
var lines = File.ReadAllLines(markerPath);
613+
var markerFields = ParseMarkerFields(lines);
614+
if (!VerifyMarkerSignature(markerFields, out var sigError))
615+
{
616+
Error($"Marker signature INVALID: {sigError}");
617+
markerClient.Dispose();
618+
return;
619+
}
620+
621+
Success("Marker signature verified.");
622+
623+
// Health nonce echo verification
624+
var nonce = Guid.NewGuid().ToString("N");
625+
try
626+
{
627+
var healthResult = await markerClient.GetAsync<JsonElement>(
628+
$"/health?nonce={Uri.EscapeDataString(nonce)}").ConfigureAwait(true);
629+
var echoedNonce = healthResult.TryGetProperty("nonce", out var nonceProp)
630+
? nonceProp.GetString() : null;
631+
if (!string.Equals(echoedNonce, nonce, StringComparison.Ordinal))
632+
{
633+
Error($"Health nonce mismatch: sent '{nonce}', got '{echoedNonce}'.");
634+
markerClient.Dispose();
635+
return;
636+
}
637+
638+
Success("Health nonce verified.");
639+
}
640+
catch (Exception ex)
641+
{
642+
Error($"Health check failed: {ex.Message}");
643+
markerClient.Dispose();
644+
return;
645+
}
646+
647+
Success($"Workspace trusted: {workspacePath}");
648+
Info($"Marker: {markerPath}");
649+
Info($"API key: {markerClient.ApiKey[..Math.Min(8, markerClient.ApiKey.Length)]}...");
650+
markerClient.Dispose();
651+
}
652+
643653
/// <summary>
644654
/// Parses the flat and dotted fields from the marker YAML for signature verification.
645655
/// Returns a dictionary mapping dotted keys (e.g. "endpoints.health") to their string values.

0 commit comments

Comments
 (0)