Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions agent-governance-dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,84 @@ engine.LoadCedar(
""");
```

### Action-Bound Approval Chains

`require_approval` decisions can be routed through the ADR-0030 approval protocol. The protocol binds approval to the exact agent, subject, operation, target, parameters, policy version, and chain version. It persists pending requests, records hash-linked entries, fails closed on timeout or malformed responses, and revalidates and consumes a terminal allow immediately before execution.

```csharp
using AgentGovernance.Approvals;

var store = new InMemoryApprovalStore();
var audit = new InMemoryApprovalAuditSink();
var chain = new ApprovalChain
{
ChainId = "production-writes",
Version = "3",
Stages = new[]
{
new ApprovalStage
{
StageIndex = 0,
AllowedIdentities = new[] { "did:web:example.com:users:alice" }
}
}
};
var coordinator = new ApprovalCoordinator(
chain,
store,
new ApprovalCoordinatorOptions
{
PolicyRuleId = "production-db-writes",
PolicyVersion = "2026.07.17",
RequestTtl = TimeSpan.FromMinutes(10),
AuditSink = audit
});

var binding = new ActionBinding
{
Operation = "tool.invoke",
AgentId = "did:agent:123",
SubjectId = "user-456",
Target = new ActionTarget
{
ToolName = "sql_execute",
ToolSchemaVersion = "2",
Resource = "prod-db"
},
Parameters = new Dictionary<string, object?>
{
["statement"] = "UPDATE accounts SET status = ? WHERE id = ?",
["values"] = new object[] { "closed", 42 }
}
};

var opened = coordinator.OpenRequest(binding);

// Call SubmitEntry only after the caller has authenticated this principal.
coordinator.SubmitEntry(
opened.Request.ApprovalRequestId,
stageIndex: 0,
new ApprovalVote
{
ApproverKind = ApproverKind.Human,
ApproverIdentity = "did:web:example.com:users:alice",
IdentityAssurance = "oidc",
Decision = ApprovalEntryDecision.Allow,
ReasonCode = "reviewed-production-change"
});

var execution = coordinator.ValidateForExecution(
opened.Request.ApprovalRequestId,
binding);

if (!execution.Allowed)
{
throw new InvalidOperationException(execution.ReasonCode);
}
```

For remote workflows, attach a `WebhookApprover` to an `ApprovalStage`. Its versioned payload carries the request and action digests, policy and chain versions, and expiry. Approve responses are accepted only when a supplied `WebhookResponseVerifier` returns an identity established through authenticated transport or a verified assertion. LLM entries are advisory and cannot authorize or deny execution. A chain with no required non-advisory stage fails closed with `no_required_approval_stage`; this intentionally avoids the vacuous-allow behavior in the current Python implementation and matches the safer Go parity behavior.

### Rate Limiting

Sliding window rate limiter integrated into the policy engine:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace AgentGovernance.Approvals;

/// <summary>Lifecycle events emitted by the approval protocol.</summary>
public enum ApprovalAuditEventType
{
/// <summary>A policy decision suspended execution.</summary>
PolicyDecision,

/// <summary>An approval request was opened.</summary>
ApprovalRequested,

/// <summary>An approval chain entry was appended.</summary>
ApprovalChainEntry,

/// <summary>An approval request reached a terminal resolution.</summary>
ApprovalResolved,

/// <summary>An approval request expired.</summary>
ApprovalExpired,

/// <summary>An approval request was cancelled.</summary>
ApprovalCancelled,

/// <summary>An allow resolution was consumed.</summary>
ApprovalConsumed,

/// <summary>Execution was released after successful validation.</summary>
ExecutionAllowed,

/// <summary>Execution was denied during validation.</summary>
ExecutionDenied
}

/// <summary>A structured, linked approval audit record.</summary>
public sealed record ApprovalAuditEvent
{
/// <summary>The audit event type.</summary>
public required ApprovalAuditEventType Type { get; init; }

/// <summary>The UTC event time.</summary>
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;

/// <summary>The acting agent identifier.</summary>
public string? AgentId { get; init; }

/// <summary>The linked policy decision identifier.</summary>
public string? PolicyDecisionId { get; init; }

/// <summary>The linked approval request identifier.</summary>
public string? ApprovalRequestId { get; init; }

/// <summary>The linked approval resolution identifier.</summary>
public string? ApprovalResolutionId { get; init; }

/// <summary>The linked approval chain entry identifier.</summary>
public string? ChainEntryId { get; init; }

/// <summary>The bound action digest.</summary>
public string? ActionDigest { get; init; }

/// <summary>The bound policy version.</summary>
public string? PolicyVersion { get; init; }

/// <summary>The bound approval chain version.</summary>
public string? ApprovalChainVersion { get; init; }

/// <summary>The machine-readable event reason.</summary>
public string? ReasonCode { get; init; }
}

/// <summary>Receives structured approval protocol audit records.</summary>
public interface IApprovalAuditSink
{
/// <summary>Persists one approval audit record.</summary>
void Write(ApprovalAuditEvent auditEvent);
}

/// <summary>A thread-safe in-memory approval audit sink for tests and local embedding.</summary>
public sealed class InMemoryApprovalAuditSink : IApprovalAuditSink
{
private readonly object _sync = new();
private readonly List<ApprovalAuditEvent> _events = new();

/// <inheritdoc />
public void Write(ApprovalAuditEvent auditEvent)
{
ArgumentNullException.ThrowIfNull(auditEvent);
lock (_sync)
{
_events.Add(auditEvent);
}
}

/// <summary>Returns an immutable snapshot of recorded events.</summary>
public IReadOnlyList<ApprovalAuditEvent> GetEvents()
{
lock (_sync)
{
return _events.ToList().AsReadOnly();
}
}
}
Loading
Loading