feat(runner): mint per-run MCP token + emit run-scoped --mcp-config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 16:03:51 +02:00
parent f3052dc5fc
commit 06e3acd5ac
7 changed files with 78 additions and 7 deletions

View File

@@ -8,7 +8,9 @@ public sealed record ClaudeRunConfig(
string? AgentPath,
string? ResumeSessionId,
int? MaxTurns = null,
string? PermissionMode = null
string? PermissionMode = null,
string? McpConfigPath = null,
string? AllowedTools = null
);
public sealed class ClaudeArgsBuilder
@@ -57,6 +59,12 @@ public sealed class ClaudeArgsBuilder
args.Add($"--json-schema {Escape(ResultSchema)}");
if (config.McpConfigPath is not null)
args.Add($"--mcp-config {Escape(config.McpConfigPath)}");
if (config.AllowedTools is not null)
args.Add($"--allowedTools {config.AllowedTools}");
if (config.ResumeSessionId is not null)
args.Add($"--resume {config.ResumeSessionId}");

View File

@@ -1,3 +1,4 @@
using System.Text.Json;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
@@ -19,6 +20,7 @@ public sealed class TaskRunner
private readonly WorkerConfig _cfg;
private readonly ILogger<TaskRunner> _logger;
private readonly ITaskStateService _state;
private readonly TaskRunTokenRegistry _tokens;
public TaskRunner(
IClaudeProcess claude,
@@ -28,7 +30,8 @@ public sealed class TaskRunner
ClaudeArgsBuilder argsBuilder,
WorkerConfig cfg,
ILogger<TaskRunner> logger,
ITaskStateService state)
ITaskStateService state,
TaskRunTokenRegistry tokens)
{
_claude = claude;
_dbFactory = dbFactory;
@@ -38,10 +41,13 @@ public sealed class TaskRunner
_cfg = cfg;
_logger = logger;
_state = state;
_tokens = tokens;
}
public async Task RunAsync(TaskEntity task, string slot, CancellationToken ct)
{
string? mcpToken = null;
string? mcpConfigPath = null;
try
{
ListEntity? list;
@@ -75,6 +81,22 @@ public sealed class TaskRunner
var resolvedConfig = await ResolveConfigAsync(task, listConfig, null, ct);
// Improvement-eligible runs get a per-run MCP identity so the agent can file
// out-of-scope follow-ups via SuggestImprovement. Children and planning runs do not.
if (task.ParentTaskId is null && task.PlanningPhase == PlanningPhase.None)
{
mcpToken = TaskRunTokenRegistry.GenerateToken();
_tokens.Register(mcpToken, task.Id);
Directory.CreateDirectory(_cfg.LogRoot);
mcpConfigPath = Path.Combine(_cfg.LogRoot, $"{task.Id}_mcp.json");
await File.WriteAllTextAsync(mcpConfigPath, BuildRunMcpConfigJson(mcpToken), ct);
resolvedConfig = resolvedConfig with
{
McpConfigPath = mcpConfigPath,
AllowedTools = "mcp__claudedo_run__SuggestImprovement",
};
}
var now = DateTime.UtcNow;
await _state.StartRunningAsync(task.Id, now, ct);
await _broadcaster.TaskStarted(slot, task.Id, now);
@@ -135,6 +157,15 @@ public sealed class TaskRunner
_logger.LogError(ex, "Unhandled exception running task {TaskId}", task.Id);
await MarkFailed(task.Id, task.Title, slot, $"Unhandled error: {ex.Message}");
}
finally
{
if (mcpToken is not null)
{
_tokens.Unregister(mcpToken);
if (mcpConfigPath is not null)
try { File.Delete(mcpConfigPath); } catch { /* best effort */ }
}
}
}
public async Task ContinueAsync(string taskId, string followUpPrompt, string slot, CancellationToken ct)
@@ -396,6 +427,26 @@ public sealed class TaskRunner
}
}
private string BuildRunMcpConfigJson(string token)
{
var payload = new
{
mcpServers = new
{
claudedo_run = new
{
type = "http",
url = $"http://127.0.0.1:{_cfg.SignalRPort}/mcp",
headers = new Dictionary<string, string>
{
["Authorization"] = $"Bearer {token}",
},
},
},
};
return JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
}
private async Task<ClaudeRunConfig> ResolveConfigAsync(
TaskEntity task, ListConfigEntity? listConfig, string? resumeSessionId, CancellationToken ct)
{