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
@@ -330,6 +330,22 @@ public sealed class OrgAdministrationTests : IDisposable
|
||||
Unit("marketing").Votes.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ═══════════════════ credentials per unit (#692) ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public void A_unit_holds_a_credential_until_it_is_removed()
|
||||
{
|
||||
BuildMarketing();
|
||||
|
||||
Org.SetUnitCredential("marketing", "", "apiKey", "x").Ok.ShouldBeFalse();
|
||||
Org.SetUnitCredential("marketing", "WebFetch", "token", "t-1", "shared key").Ok.ShouldBeTrue();
|
||||
Unit("marketing").Credentials!["WebFetch"]["token"].ShouldBe("t-1");
|
||||
Org.Current().ToolSettingsFor(Find("ads"), "WebFetch")["token"].ShouldBe("t-1", "ads sits in amazon, below marketing");
|
||||
|
||||
Org.SetUnitCredential("marketing", "webfetch", "TOKEN", null).Ok.ShouldBeTrue();
|
||||
Unit("marketing").Credentials.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ═══════════════════ templates (#691) ═══════════════════
|
||||
|
||||
private static Core.Organisation.OrgTemplate Shop() => new()
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -281,6 +281,24 @@ public sealed class ConfigSecretsTests : IDisposable
|
||||
agent.Tools["Mail"]["password"]!.ToString().ShouldNotBeNull().ShouldNotContain("mail-passwort");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Credentials_a_unit_holds_are_protected_whatever_their_name()
|
||||
{
|
||||
// #692: everything a unit holds for its members is a secret.
|
||||
var instance = new InstanceConfig();
|
||||
instance.Units.Add(new UnitConfig
|
||||
{
|
||||
Id = "purchasing", Name = "Purchasing",
|
||||
Credentials = new() { ["DirectAPI"] = new() { ["marketplaceKey"] = "mk-123" } }
|
||||
});
|
||||
|
||||
ConfigSecrets.Protect(instance);
|
||||
instance.Units[0].Credentials!["DirectAPI"]["marketplaceKey"].ShouldNotContain("mk-123");
|
||||
|
||||
ConfigSecrets.Unprotect(instance);
|
||||
instance.Units[0].Credentials!["DirectAPI"]["marketplaceKey"].ShouldBe("mk-123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Nicht_geheime_Felder_bleiben_lesbar()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user