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:
@@ -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}");
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user