Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Organisation/UnitCredentialsTests.cs
T
RichardandClaude Opus 5.5 9a265888f8 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>
2026-10-01 12:50:27 +02:00

76 lines
3.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Organisation;
using Shouldly;
namespace ClawdDotNet.Core.Tests.Organisation;
/// <summary>
/// #692: A unit holds credentials for its members — needed once, not six times, and the
/// accounts department never gets the marketplace key.
/// </summary>
public sealed class UnitCredentialsTests
{
/// <summary>shop (holds DirectAPI.apiKey, Mail.password) › buying (holds DirectAPI.apiKey); accounts beside it.</summary>
private static OrgChart Org(params AgentConfig[] agents) => new(
[
new UnitConfig
{
Id = "shop", Name = "Shop",
Credentials = new() { ["DirectAPI"] = new() { ["apiKey"] = "shop-key" }, ["Mail"] = new() { ["password"] = "shop-mail" } }
},
new UnitConfig { Id = "buying", Name = "Buying", ParentId = "shop", Credentials = new() { ["directapi"] = new() { ["apiKey"] = "buying-key" } } },
new UnitConfig { Id = "accounts", Name = "Accounts" }
],
agents);
private static AgentConfig Agent(string id, string unit, params (string Tool, Dictionary<string, object?> Settings)[] tools)
=> new() { AgentId = id, UnitId = unit, Tools = tools.ToDictionary(t => t.Tool, t => t.Settings) };
[Fact]
public void A_member_gets_the_nearest_units_credential_for_a_tool_it_has()
{
var buyer = Agent("buyer", "buying", ("DirectAPI", new() { ["baseUrl"] = "https://api.example" }), ("Mail", new()));
var org = Org(buyer);
org.ToolSettingsFor(buyer, "DirectAPI")["apiKey"].ShouldBe("buying-key", "the nearest unit first");
org.ToolSettingsFor(buyer, "DirectAPI")["baseUrl"].ShouldBe("https://api.example");
org.ToolSettingsFor(buyer, "Mail")["password"].ShouldBe("shop-mail", "from the unit above");
}
[Fact]
public void Its_own_value_wins_and_an_empty_one_is_filled()
{
var own = Agent("own", "shop", ("DirectAPI", new() { ["apiKey"] = "own-key" }), ("Mail", new() { ["password"] = "" }));
var org = Org(own);
org.ToolSettingsFor(own, "DirectAPI")["apiKey"].ShouldBe("own-key");
org.ToolSettingsFor(own, "Mail")["password"].ShouldBe("shop-mail");
}
[Fact]
public void No_credential_reaches_another_unit_or_a_tool_the_agent_lacks()
{
var clerk = Agent("clerk", "accounts", ("DirectAPI", new()));
var buyer = Agent("buyer", "buying", ("FileRW", new()));
var org = Org(clerk, buyer);
org.ToolSettingsFor(clerk, "DirectAPI").ContainsKey("apiKey").ShouldBeFalse();
org.ToolSettingsFor(buyer, "DirectAPI").ShouldBeEmpty("a credential grants no tool");
}
[Fact]
public void A_template_never_carries_what_a_unit_holds()
{
var instance = new InstanceConfig();
instance.Units.Add(new UnitConfig { Id = "shop", Name = "Shop", Credentials = new() { ["DirectAPI"] = new() { ["apiKey"] = "shop-key" } } });
var template = OrgTemplates.Export(instance, "Shop", "", _ => null, DateTime.UtcNow);
template.Units[0].Credentials.ShouldBeNull();
template.Credentials.ShouldContain(new CredentialNeed("unit:shop", "DirectAPI", "apiKey"));
template.Units[0].Credentials = new() { ["DirectAPI"] = new() { ["apiKey"] = "smuggled" } };
OrgTemplates.Problems(template).ShouldContain(p => p.Contains("brings credentials"));
}
}