Skip to content

Commit 4c288c0

Browse files
Merge branch 'main' into feat/rust-go-publishing
2 parents c98b48e + e21bd05 commit 4c288c0

File tree

14 files changed

+1301
-5
lines changed

14 files changed

+1301
-5
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ jobs:
178178
'pydantic','pyyaml','cryptography','pynacl','click','rich',
179179
'httpx','aiohttp','fastapi','uvicorn','structlog','numpy',
180180
'scipy','openai','anthropic','langchain','crewai',
181+
'streamlit','plotly','pandas','networkx','aioredis',
182+
'langchain-openai','langchain-core','python-dotenv',
183+
'agent-primitives',
181184
}
182185
bad = []
183186
for nb in glob.glob('**/*.ipynb', recursive=True):
@@ -219,10 +222,10 @@ jobs:
219222
# Only flag if actions/checkout has ref: pointing to head (unsafe)
220223
# Uses awk to check checkout blocks specifically, not unrelated lines
221224
if awk '/actions\/checkout/{found=1} found && /ref:.*head\.(ref|sha)/{print; exit 1}' "$f" 2>/dev/null; then
225+
echo "OK: $f (pull_request_target, base-only checkout)"
226+
else
222227
echo "UNSAFE: $f checks out PR head in pull_request_target context"
223228
UNSAFE=1
224-
else
225-
echo "OK: $f (pull_request_target, base-only checkout)"
226229
fi
227230
fi
228231
done
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
using System.Text.Json;
7+
8+
namespace AgentGovernance.Audit;
9+
10+
/// <summary>
11+
/// Represents a single entry in the hash-chain audit log.
12+
/// Each entry contains a SHA-256 hash linking it to the previous entry,
13+
/// forming a tamper-evident chain.
14+
/// </summary>
15+
public sealed class AuditEntry
16+
{
17+
/// <summary>
18+
/// The sequence number of this entry in the log (zero-based).
19+
/// </summary>
20+
public long Seq { get; init; }
21+
22+
/// <summary>
23+
/// UTC timestamp of when this entry was recorded.
24+
/// </summary>
25+
public DateTimeOffset Timestamp { get; init; }
26+
27+
/// <summary>
28+
/// The decentralised identifier of the agent that performed the action.
29+
/// </summary>
30+
public required string AgentId { get; init; }
31+
32+
/// <summary>
33+
/// The action that was performed (e.g., "tool_call", "policy_check").
34+
/// </summary>
35+
public required string Action { get; init; }
36+
37+
/// <summary>
38+
/// The governance decision for this action (e.g., "allow", "deny").
39+
/// </summary>
40+
public required string Decision { get; init; }
41+
42+
/// <summary>
43+
/// The SHA-256 hash of the previous entry, or an empty string for the genesis entry.
44+
/// </summary>
45+
public string PreviousHash { get; init; } = string.Empty;
46+
47+
/// <summary>
48+
/// The SHA-256 hash of this entry, computed over the concatenation of
49+
/// <see cref="Seq"/>, <see cref="Timestamp"/>, <see cref="AgentId"/>,
50+
/// <see cref="Action"/>, <see cref="Decision"/>, and <see cref="PreviousHash"/>.
51+
/// </summary>
52+
public string Hash { get; init; } = string.Empty;
53+
}
54+
55+
/// <summary>
56+
/// A tamper-evident, hash-chain audit logger for governance actions.
57+
/// Each logged entry includes a SHA-256 hash linking to the previous entry,
58+
/// enabling full chain integrity verification.
59+
/// <para>
60+
/// This class is thread-safe. All mutations are protected by a lock.
61+
/// </para>
62+
/// </summary>
63+
public sealed class AuditLogger
64+
{
65+
private readonly List<AuditEntry> _entries = new();
66+
private readonly object _lock = new();
67+
68+
/// <summary>
69+
/// Returns the number of entries in the audit log.
70+
/// </summary>
71+
public int Count
72+
{
73+
get
74+
{
75+
lock (_lock)
76+
{
77+
return _entries.Count;
78+
}
79+
}
80+
}
81+
82+
/// <summary>
83+
/// Appends a new entry to the hash-chain audit log.
84+
/// The entry's hash is computed from its fields and the previous entry's hash.
85+
/// </summary>
86+
/// <param name="agentId">The agent's decentralised identifier.</param>
87+
/// <param name="action">The action performed.</param>
88+
/// <param name="decision">The governance decision.</param>
89+
/// <returns>The newly created <see cref="AuditEntry"/>.</returns>
90+
public AuditEntry Log(string agentId, string action, string decision)
91+
{
92+
ArgumentException.ThrowIfNullOrWhiteSpace(agentId);
93+
ArgumentException.ThrowIfNullOrWhiteSpace(action);
94+
ArgumentException.ThrowIfNullOrWhiteSpace(decision);
95+
96+
lock (_lock)
97+
{
98+
var seq = _entries.Count;
99+
var timestamp = DateTimeOffset.UtcNow;
100+
var previousHash = seq == 0 ? string.Empty : _entries[seq - 1].Hash;
101+
102+
var hash = ComputeHash(seq, timestamp, agentId, action, decision, previousHash);
103+
104+
var entry = new AuditEntry
105+
{
106+
Seq = seq,
107+
Timestamp = timestamp,
108+
AgentId = agentId,
109+
Action = action,
110+
Decision = decision,
111+
PreviousHash = previousHash,
112+
Hash = hash
113+
};
114+
115+
_entries.Add(entry);
116+
return entry;
117+
}
118+
}
119+
120+
/// <summary>
121+
/// Validates the integrity of the entire hash chain.
122+
/// Recomputes every hash and verifies that each entry's <see cref="AuditEntry.PreviousHash"/>
123+
/// matches the preceding entry's <see cref="AuditEntry.Hash"/>.
124+
/// </summary>
125+
/// <returns><c>true</c> if the chain is intact; otherwise <c>false</c>.</returns>
126+
public bool Verify()
127+
{
128+
lock (_lock)
129+
{
130+
for (var i = 0; i < _entries.Count; i++)
131+
{
132+
var entry = _entries[i];
133+
134+
// Verify previous-hash linkage.
135+
var expectedPrevHash = i == 0 ? string.Empty : _entries[i - 1].Hash;
136+
if (entry.PreviousHash != expectedPrevHash)
137+
{
138+
return false;
139+
}
140+
141+
// Recompute and verify this entry's hash.
142+
var recomputed = ComputeHash(
143+
entry.Seq, entry.Timestamp, entry.AgentId,
144+
entry.Action, entry.Decision, entry.PreviousHash);
145+
146+
if (entry.Hash != recomputed)
147+
{
148+
return false;
149+
}
150+
}
151+
152+
return true;
153+
}
154+
}
155+
156+
/// <summary>
157+
/// Returns audit entries, optionally filtered by agent ID and/or action.
158+
/// </summary>
159+
/// <param name="agentId">When provided, only entries for this agent are returned.</param>
160+
/// <param name="action">When provided, only entries with this action are returned.</param>
161+
/// <returns>A read-only list of matching entries.</returns>
162+
public IReadOnlyList<AuditEntry> GetEntries(string? agentId = null, string? action = null)
163+
{
164+
lock (_lock)
165+
{
166+
IEnumerable<AuditEntry> query = _entries;
167+
168+
if (agentId is not null)
169+
{
170+
query = query.Where(e => e.AgentId == agentId);
171+
}
172+
173+
if (action is not null)
174+
{
175+
query = query.Where(e => e.Action == action);
176+
}
177+
178+
return query.ToList().AsReadOnly();
179+
}
180+
}
181+
182+
/// <summary>
183+
/// Serializes the entire audit log to a JSON string.
184+
/// </summary>
185+
/// <returns>A JSON array of all audit entries.</returns>
186+
public string ExportJson()
187+
{
188+
lock (_lock)
189+
{
190+
return JsonSerializer.Serialize(_entries, JsonOptions);
191+
}
192+
}
193+
194+
private static readonly JsonSerializerOptions JsonOptions = new()
195+
{
196+
WriteIndented = true,
197+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
198+
};
199+
200+
/// <summary>
201+
/// Computes the SHA-256 hash for an audit entry from its constituent fields.
202+
/// </summary>
203+
private static string ComputeHash(
204+
long seq, DateTimeOffset timestamp, string agentId,
205+
string action, string decision, string previousHash)
206+
{
207+
var payload = $"{seq}|{timestamp:O}|{agentId}|{action}|{decision}|{previousHash}";
208+
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(payload));
209+
return Convert.ToHexString(bytes).ToLowerInvariant();
210+
}
211+
}

packages/agent-governance-dotnet/src/AgentGovernance/Trust/AgentIdentity.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@
55

66
namespace AgentGovernance.Trust;
77

8+
/// <summary>
9+
/// Represents the lifecycle status of an agent identity.
10+
/// </summary>
11+
public enum IdentityStatus
12+
{
13+
/// <summary>The identity is active and can participate in governance operations.</summary>
14+
Active,
15+
16+
/// <summary>The identity is temporarily suspended and cannot sign or verify.</summary>
17+
Suspended,
18+
19+
/// <summary>The identity has been permanently revoked.</summary>
20+
Revoked
21+
}
22+
823
/// <summary>
924
/// Represents an agent identity with cryptographic signing and verification capabilities.
1025
/// <para>
@@ -48,6 +63,60 @@ public sealed class AgentIdentity
4863
/// </summary>
4964
public byte[]? PrivateKey { get; }
5065

66+
/// <summary>
67+
/// The current lifecycle status of this identity.
68+
/// </summary>
69+
public IdentityStatus Status { get; private set; } = IdentityStatus.Active;
70+
71+
/// <summary>
72+
/// Suspends this identity, preventing it from participating in governance operations.
73+
/// A suspended identity can be reactivated.
74+
/// </summary>
75+
/// <exception cref="InvalidOperationException">
76+
/// Thrown when the identity is already revoked.
77+
/// </exception>
78+
public void Suspend()
79+
{
80+
if (Status == IdentityStatus.Revoked)
81+
{
82+
throw new InvalidOperationException(
83+
"Cannot suspend a revoked identity.");
84+
}
85+
86+
Status = IdentityStatus.Suspended;
87+
}
88+
89+
/// <summary>
90+
/// Permanently revokes this identity. A revoked identity cannot be reactivated.
91+
/// </summary>
92+
public void Revoke()
93+
{
94+
Status = IdentityStatus.Revoked;
95+
}
96+
97+
/// <summary>
98+
/// Reactivates a suspended identity, restoring it to active status.
99+
/// </summary>
100+
/// <exception cref="InvalidOperationException">
101+
/// Thrown when the identity is revoked and cannot be reactivated.
102+
/// </exception>
103+
public void Reactivate()
104+
{
105+
if (Status == IdentityStatus.Revoked)
106+
{
107+
throw new InvalidOperationException(
108+
"Cannot reactivate a revoked identity.");
109+
}
110+
111+
Status = IdentityStatus.Active;
112+
}
113+
114+
/// <summary>
115+
/// Returns whether this identity is currently active.
116+
/// </summary>
117+
/// <returns><c>true</c> if the identity status is <see cref="IdentityStatus.Active"/>; otherwise <c>false</c>.</returns>
118+
public bool IsActive() => Status == IdentityStatus.Active;
119+
51120
/// <summary>
52121
/// Initializes a new <see cref="AgentIdentity"/> with the specified DID and key material.
53122
/// </summary>

0 commit comments

Comments
 (0)