Werkzeugkasten: Zugangsdaten je Einheit (#692)
A unit holds credentials for its members, by tool and setting - needed once,
not once per agent, and other units never get them.
- UnitConfig.credentials, protected by ConfigSecrets like every secret.
- OrgChart.ToolSettingsFor: an agent's own settings, completed by the nearest
unit up that holds the value; own non-empty values win; no tool is granted.
Used for runs, tool jobs and preflight probes.
- OrgAdministration.SetUnitCredential, PUT /organisation/units/{id}/credentials;
the view lists held settings, never values.
- Templates never carry unit credentials; they are named instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
661807b2d0
commit
9a265888f8
@@ -74,6 +74,15 @@ public sealed class UnitConfig
|
||||
/// <summary>#691: The template the unit came from — its texts and rules are someone else's.</summary>
|
||||
[JsonPropertyName("templateOrigin")]
|
||||
public string? TemplateOrigin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// #692: Credentials the unit holds for its members, by tool and setting — a purchasing
|
||||
/// department with six agents needs the marketplace key once, not six times, and the
|
||||
/// accounts department never gets it. Every value is a secret, stored protected; a member's
|
||||
/// own non-empty value wins, and a credential grants no tool.
|
||||
/// </summary>
|
||||
[JsonPropertyName("credentials")]
|
||||
public Dictionary<string, Dictionary<string, string>>? Credentials { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1853,9 +1853,12 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
AgentConfig agentConfig, string instanceId, string toolName, CancellationToken ct,
|
||||
DelegationChain? chain = null, string? runId = null)
|
||||
{
|
||||
var toolConfig = agentConfig.Tools.TryGetValue(toolName, out var cfg)
|
||||
? cfg.AsReadOnly()
|
||||
: new Dictionary<string, object?>().AsReadOnly();
|
||||
// #692: completed by the credentials the agent's unit holds for the tool.
|
||||
var toolConfig = OrganisationProvider?.Invoke() is { } org
|
||||
? org.ToolSettingsFor(agentConfig, toolName)
|
||||
: agentConfig.Tools.TryGetValue(toolName, out var cfg)
|
||||
? cfg.AsReadOnly()
|
||||
: new Dictionary<string, object?>().AsReadOnly();
|
||||
|
||||
var toolLogger = _loggerFactory.CreateLogger($"ClawdDotNet.Tools.{toolName}.Execution");
|
||||
var baseChain = chain ?? DelegationChain.Root(agentConfig.AgentId);
|
||||
|
||||
@@ -135,6 +135,43 @@ public sealed class OrgChart
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #692: The settings a tool of this agent runs with — its own, completed by the credentials
|
||||
/// its unit and the units above hold for that tool (the nearest first). The agent's own
|
||||
/// non-empty value wins; nothing is added for a tool the agent does not have.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object?> ToolSettingsFor(AgentConfig agent, string toolName)
|
||||
{
|
||||
if (!agent.Tools.TryGetValue(toolName, out var own))
|
||||
return new Dictionary<string, object?>().AsReadOnly();
|
||||
if (UnitOf(agent.AgentId) is not { } unit)
|
||||
return own.AsReadOnly();
|
||||
|
||||
Dictionary<string, object?>? merged = null;
|
||||
foreach (var holder in new[] { unit }.Concat(Ancestors(unit.Id)))
|
||||
{
|
||||
if (holder.Credentials?.FirstOrDefault(c => string.Equals(c.Key, toolName, StringComparison.OrdinalIgnoreCase)).Value is not { } held)
|
||||
continue;
|
||||
foreach (var (key, value) in held)
|
||||
{
|
||||
merged ??= new Dictionary<string, object?>(own, StringComparer.OrdinalIgnoreCase);
|
||||
if (string.IsNullOrEmpty(value) || (merged.TryGetValue(key, out var current) && !IsEmpty(current)))
|
||||
continue;
|
||||
merged[key] = value;
|
||||
}
|
||||
}
|
||||
return (merged ?? own).AsReadOnly();
|
||||
}
|
||||
|
||||
private static bool IsEmpty(object? value) => value switch
|
||||
{
|
||||
null => true,
|
||||
string s => s.Length == 0,
|
||||
System.Text.Json.JsonElement { ValueKind: System.Text.Json.JsonValueKind.String } e => string.IsNullOrEmpty(e.GetString()),
|
||||
System.Text.Json.JsonElement { ValueKind: System.Text.Json.JsonValueKind.Null or System.Text.Json.JsonValueKind.Undefined } => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
/// <summary>The unit this agent leads, if any.</summary>
|
||||
public UnitConfig? LedBy(string agentId)
|
||||
{
|
||||
|
||||
@@ -99,6 +99,9 @@ public sealed record CredentialNeed(
|
||||
|
||||
public static class OrgTemplates
|
||||
{
|
||||
/// <summary>A credential need held by a unit, not an agent: <c>unit:<id></c>.</summary>
|
||||
public const string UnitPrefix = "unit:";
|
||||
|
||||
/// <summary>Longest Identity or Soul text a template may bring — it goes into every prompt.</summary>
|
||||
public const int MaxTextLength = 50_000;
|
||||
|
||||
@@ -121,7 +124,14 @@ public static class OrgTemplates
|
||||
|
||||
foreach (var unit in instance.Units)
|
||||
{
|
||||
template.Units.Add(JsonSerializer.Deserialize<UnitConfig>(JsonSerializer.Serialize(unit))!);
|
||||
var copy = JsonSerializer.Deserialize<UnitConfig>(JsonSerializer.Serialize(unit))!;
|
||||
// #692: what a unit holds for its members stays here — named, never carried.
|
||||
foreach (var (tool, held) in copy.Credentials ?? [])
|
||||
foreach (var key in held.Where(h => h.Value.Length > 0).Select(h => h.Key))
|
||||
template.Credentials.Add(new CredentialNeed(UnitPrefix + unit.Id, tool, key));
|
||||
copy.Credentials = null;
|
||||
copy.TemplateOrigin = null;
|
||||
template.Units.Add(copy);
|
||||
if (doctrineOf(unit.Id) is { Length: > 0 } doctrine)
|
||||
template.Doctrines[unit.Id] = doctrine;
|
||||
}
|
||||
@@ -225,6 +235,8 @@ public static class OrgTemplates
|
||||
problems.Add($"Unit '{u.Id}' appears twice.");
|
||||
if (string.IsNullOrWhiteSpace(u.Name))
|
||||
problems.Add($"Unit '{u.Id}' has no name.");
|
||||
if (u.Credentials?.Values.Any(h => h.Values.Any(v => v.Length > 0)) == true)
|
||||
problems.Add($"Unit '{u.Id}' brings credentials — a template never carries one.");
|
||||
}
|
||||
foreach (var u in template.Units.Where(u => u.ParentId is not null && !units.Contains(u.ParentId)))
|
||||
problems.Add($"Unit '{u.Id}' hangs under '{u.ParentId}', which the template does not have.");
|
||||
|
||||
@@ -63,6 +63,13 @@ public static class ConfigSecrets
|
||||
|
||||
foreach (var agent in config.Agents)
|
||||
Apply(agent, transform);
|
||||
|
||||
// #692: everything a unit holds for its members is a secret, whatever its name.
|
||||
foreach (var unit in config.Units)
|
||||
foreach (var settings in unit.Credentials?.Values ?? Enumerable.Empty<Dictionary<string, string>>())
|
||||
foreach (var key in settings.Keys.ToList())
|
||||
if (settings[key] is { Length: > 0 } value)
|
||||
settings[key] = transform(value) ?? "";
|
||||
}
|
||||
|
||||
// ─── Agent ───
|
||||
|
||||
@@ -159,9 +159,14 @@ public sealed class EngineTaskDispatcher : ITaskDispatcher
|
||||
// Zielagent (zum Wecken) und dessen Tool-Konfiguration/Workspace.
|
||||
var agentId = TaskAssignee.AgentId(task.Assignee);
|
||||
var config = _agents().FirstOrDefault(a => a.AgentId == agentId);
|
||||
var toolConfig = config is not null && config.Tools.TryGetValue(task.ToolName, out var cfg)
|
||||
? (IReadOnlyDictionary<string, object?>)cfg.AsReadOnly()
|
||||
: new Dictionary<string, object?>().AsReadOnly();
|
||||
// #692: completed by the credentials the agent's unit holds for the tool.
|
||||
var toolConfig = config is null
|
||||
? new Dictionary<string, object?>().AsReadOnly()
|
||||
: _engine.OrganisationProvider?.Invoke() is { } org
|
||||
? org.ToolSettingsFor(config, task.ToolName)
|
||||
: config.Tools.TryGetValue(task.ToolName, out var cfg)
|
||||
? (IReadOnlyDictionary<string, object?>)cfg.AsReadOnly()
|
||||
: new Dictionary<string, object?>().AsReadOnly();
|
||||
|
||||
var result = await provider.ExecuteJobAsync(
|
||||
task.JobTypeId, toolConfig, _stateStore, _logger, ct, agentId, config?.WorkspacePath);
|
||||
|
||||
Reference in New Issue
Block a user