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>
210 lines
8.8 KiB
C#
210 lines
8.8 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace ClawdDotNet.Core.Config;
|
|
|
|
/// <summary>
|
|
/// #683: A unit — department, team, group — as a node in the organisation tree.
|
|
///
|
|
/// <para><b>Stored in the instance configuration, next to the agents.</b> An agent's tools
|
|
/// already live there; a unit's rules belong beside them, so the truth about an agent is in
|
|
/// one place. Every structural change is written to the audit log by the administration,
|
|
/// which is what the concept asked the database for.</para>
|
|
///
|
|
/// <para><b>No value here overrides an agent at run time</b> (Organisation-Konzept, section
|
|
/// 3). <see cref="Defaults"/> are only the starting values for agents that join;
|
|
/// <see cref="ToolCeiling"/> is checked when an agent's tools are written, never laid over
|
|
/// them while it runs. What stands at the agent is what applies.</para>
|
|
/// </summary>
|
|
public sealed class UnitConfig
|
|
{
|
|
[JsonPropertyName("id")]
|
|
public string Id { get; set; } = "";
|
|
|
|
[JsonPropertyName("name")]
|
|
public string Name { get; set; } = "";
|
|
|
|
/// <summary>The parent unit; <c>null</c> for a top-level unit.</summary>
|
|
[JsonPropertyName("parentId")]
|
|
public string? ParentId { get; set; }
|
|
|
|
/// <summary>
|
|
/// The agent leading this unit — one of its members. May be empty: then the nearest
|
|
/// unit above that has a lead takes over.
|
|
/// </summary>
|
|
[JsonPropertyName("leaderAgentId")]
|
|
public string? LeaderAgentId { get; set; }
|
|
|
|
/// <summary>What the unit exists for — the top of every task chain in it (#714).</summary>
|
|
[JsonPropertyName("goal")]
|
|
public string Goal { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// The tools members of this unit may have at most; <c>null</c> means no ceiling. A
|
|
/// ceiling applies to sub-units as well — the effective ceiling is the intersection
|
|
/// along the path to the root.
|
|
/// </summary>
|
|
[JsonPropertyName("toolCeiling")]
|
|
public List<string>? ToolCeiling { get; set; }
|
|
|
|
/// <summary>Starting values for agents that join this unit.</summary>
|
|
[JsonPropertyName("defaults")]
|
|
public UnitDefaults Defaults { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// #687: What the lead of this unit may approve for the agents below it; <c>null</c> means
|
|
/// nothing — every approval goes to a human. Set only by a human.
|
|
/// </summary>
|
|
[JsonPropertyName("approvals")]
|
|
public UnitApprovals? Approvals { get; set; }
|
|
|
|
/// <summary>
|
|
/// #693: How often the lead of this unit looks at the runs of the agents it leads;
|
|
/// <c>null</c> means no spot checks. Set only by a human — checks cost the lead's runs.
|
|
/// </summary>
|
|
[JsonPropertyName("inspection")]
|
|
public UnitInspection? Inspection { get; set; }
|
|
|
|
/// <summary>
|
|
/// #702: Operations that are put to a blind vote instead of the lead; <c>null</c> means none.
|
|
/// Set only by a human — a vote is a deliberate loosening, not a default.
|
|
/// </summary>
|
|
[JsonPropertyName("votes")]
|
|
public UnitVotes? Votes { get; set; }
|
|
|
|
/// <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>
|
|
/// #702: A vote — several agents check independently and vote blind before something happens.
|
|
/// Not opinions are counted but independent checks enforced: every voter looks at its own data.
|
|
/// </summary>
|
|
public sealed class UnitVotes
|
|
{
|
|
/// <summary>The operations put to the vote: <c>Tool.operation</c>, or <c>Tool</c> for all of it.</summary>
|
|
[JsonPropertyName("operations")]
|
|
public List<string> Operations { get; set; } = [];
|
|
|
|
/// <summary>The agents that vote. Whoever proposes the call does not vote on it.</summary>
|
|
[JsonPropertyName("voters")]
|
|
public List<string> Voters { get; set; } = [];
|
|
|
|
/// <summary>Yes votes from which the call is approved.</summary>
|
|
[JsonPropertyName("approveAt")]
|
|
public int ApproveAt { get; set; } = 3;
|
|
|
|
/// <summary>At this many yes votes or fewer, the call is rejected. Between the two, a human decides.</summary>
|
|
[JsonPropertyName("rejectAt")]
|
|
public int RejectAt { get; set; } = 1;
|
|
|
|
/// <summary>Votes that must be cast for any result; 0 means <see cref="ApproveAt"/>. A missing vote is no yes.</summary>
|
|
[JsonPropertyName("quorum")]
|
|
public int Quorum { get; set; }
|
|
|
|
/// <summary>How long the voters have; after that, what was cast is counted.</summary>
|
|
[JsonPropertyName("hours")]
|
|
public double Hours { get; set; } = 4;
|
|
|
|
/// <summary>Votes a day for the unit — every vote costs a run per voter; beyond, the lead or a human decides.</summary>
|
|
[JsonPropertyName("perDay")]
|
|
public int PerDay { get; set; } = 10;
|
|
|
|
[JsonIgnore]
|
|
public TimeSpan Window => TimeSpan.FromHours(Math.Clamp(Hours, 0.25, 72));
|
|
|
|
[JsonIgnore]
|
|
public int EffectiveQuorum => Quorum > 0 ? Quorum : ApproveAt;
|
|
|
|
/// <summary>Whether the vote covers the operation.</summary>
|
|
public bool Covers(string tool, string? operation)
|
|
=> Operations.Any(o => string.Equals(o, tool, StringComparison.OrdinalIgnoreCase)
|
|
|| (!string.IsNullOrWhiteSpace(operation)
|
|
&& string.Equals(o, $"{tool}.{operation}", StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// #693: The spot checks of a unit's lead. Which run is checked is decided by the run id, not
|
|
/// by chance — the same run gets the same answer on every pass, and nobody can tell from inside
|
|
/// a run whether it will be looked at.
|
|
/// </summary>
|
|
public sealed class UnitInspection
|
|
{
|
|
/// <summary>Share of ordinary runs that are checked, 0 to 1.</summary>
|
|
[JsonPropertyName("rate")]
|
|
public double Rate { get; set; } = 0.1;
|
|
|
|
/// <summary>Share for a new agent, a new model, or the run after a failure.</summary>
|
|
[JsonPropertyName("denseRate")]
|
|
public double DenseRate { get; set; } = 0.5;
|
|
|
|
/// <summary>How long an agent counts as new after its first run.</summary>
|
|
[JsonPropertyName("newDays")]
|
|
public int NewDays { get; set; } = 7;
|
|
|
|
/// <summary>Checks a day for the whole unit — a lead that checks everything doubles the unit's costs.</summary>
|
|
[JsonPropertyName("perDay")]
|
|
public int PerDay { get; set; } = 5;
|
|
}
|
|
|
|
/// <summary>
|
|
/// #687: The limits within which a unit's lead approves instead of the human. They belong to
|
|
/// the unit, not to the lead as a person — otherwise the power moves with an agent that changes
|
|
/// its post, and a vacant lead would have none.
|
|
/// </summary>
|
|
public sealed class UnitApprovals
|
|
{
|
|
/// <summary>
|
|
/// The operations the lead may approve: <c>Tool.operation</c>, or <c>Tool</c> for all of its
|
|
/// operations. A sub-unit can grant no more than every unit above it that grants anything.
|
|
/// </summary>
|
|
[JsonPropertyName("operations")]
|
|
public List<string> Operations { get; set; } = [];
|
|
|
|
/// <summary>Approvals the lead may give in 24 hours; beyond that the human decides.</summary>
|
|
[JsonPropertyName("perDay")]
|
|
public int PerDay { get; set; } = 20;
|
|
|
|
/// <summary>How long the lead has; after that the call is the human's alone.</summary>
|
|
[JsonPropertyName("hours")]
|
|
public double Hours { get; set; } = 4;
|
|
|
|
[JsonIgnore]
|
|
public TimeSpan Window => TimeSpan.FromHours(Math.Clamp(Hours, 0.25, 72));
|
|
|
|
/// <summary>Whether this grant covers the operation.</summary>
|
|
public bool Covers(string tool, string? operation)
|
|
=> Operations.Any(o => string.Equals(o, tool, StringComparison.OrdinalIgnoreCase)
|
|
|| (!string.IsNullOrWhiteSpace(operation)
|
|
&& string.Equals(o, $"{tool}.{operation}", StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// #683: What an agent gets when it joins a unit. <c>null</c> leaves the agent's own value.
|
|
/// Changing these does not touch existing members — writing a value into them is a
|
|
/// separate, previewed step.
|
|
/// </summary>
|
|
public sealed class UnitDefaults
|
|
{
|
|
[JsonPropertyName("model")]
|
|
public string? Model { get; set; }
|
|
|
|
[JsonPropertyName("dailyCostUsd")]
|
|
public decimal? DailyCostUsd { get; set; }
|
|
|
|
[JsonPropertyName("dailyTokens")]
|
|
public long? DailyTokens { get; set; }
|
|
|
|
[JsonPropertyName("whenExhausted")]
|
|
public string? WhenExhausted { get; set; }
|
|
}
|