@mandatedev/agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +275 -0
- package/dist/index.d.ts +275 -0
- package/dist/index.js +643 -0
- package/dist/index.mjs +615 -0
- package/package.json +42 -0
- package/src/buffer/index.ts +152 -0
- package/src/degradation/index.ts +128 -0
- package/src/evaluator/js-evaluator.ts +183 -0
- package/src/hooks/anthropic.ts +134 -0
- package/src/hooks/langchain.ts +141 -0
- package/src/hooks/openai.ts +142 -0
- package/src/index.ts +175 -0
- package/src/types.ts +162 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MANDATE AUDIT BUFFER
|
|
3
|
+
// Local hash-chain buffer — events sealed before any network
|
|
4
|
+
// transmission. Audit record exists regardless of control plane
|
|
5
|
+
// availability. Any gap in the chain is cryptographically detectable.
|
|
6
|
+
// EU AI Act Article 12 compliant.
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
import type { AuditEvent, DegradationTier, PolicyDecision } from '../types';
|
|
10
|
+
|
|
11
|
+
interface BufferOptions {
|
|
12
|
+
maxSize?: number;
|
|
13
|
+
flushCallback?: (events: AuditEvent[]) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AppendInput {
|
|
17
|
+
timestamp: number;
|
|
18
|
+
source: 'REALTIME' | 'BUFFER_SYNC';
|
|
19
|
+
agentId: string;
|
|
20
|
+
orgId: string;
|
|
21
|
+
policyHash: string;
|
|
22
|
+
degradationTier: DegradationTier;
|
|
23
|
+
toolName: string;
|
|
24
|
+
toolArgs: Record<string, unknown>;
|
|
25
|
+
intentContext: AuditEvent['intentContext'];
|
|
26
|
+
anomalyScore: number;
|
|
27
|
+
policyDecision: PolicyDecision;
|
|
28
|
+
responseLevel: AuditEvent['responseLevel'];
|
|
29
|
+
evalLatencyMs: number;
|
|
30
|
+
blastRadiusEst?: string;
|
|
31
|
+
tokensUsed?: number;
|
|
32
|
+
costUsd?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface VerifyResult {
|
|
36
|
+
valid: boolean;
|
|
37
|
+
corruptedAt?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class AuditBuffer {
|
|
41
|
+
private buffer: AuditEvent[] = [];
|
|
42
|
+
private lastHash: string = '0'.repeat(64); // genesis hash
|
|
43
|
+
private maxSize: number;
|
|
44
|
+
private flushCallback?: (events: AuditEvent[]) => Promise<void>;
|
|
45
|
+
|
|
46
|
+
constructor(options: BufferOptions = {}) {
|
|
47
|
+
this.maxSize = options.maxSize ?? 10000;
|
|
48
|
+
|
|
49
|
+
if (options.flushCallback !== undefined) {
|
|
50
|
+
this.flushCallback = options.flushCallback;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Append a new event to the hash chain
|
|
55
|
+
// Returns the sealed event with cryptographic proof
|
|
56
|
+
append(input: AppendInput): AuditEvent {
|
|
57
|
+
const eventId = this.generateId();
|
|
58
|
+
const prevHash = this.lastHash;
|
|
59
|
+
|
|
60
|
+
const event: AuditEvent = {
|
|
61
|
+
...input,
|
|
62
|
+
eventId,
|
|
63
|
+
prevHash,
|
|
64
|
+
eventHash: '', // computed below
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// SHA-256 hash: prevHash + full payload
|
|
68
|
+
event.eventHash = this.sha256(prevHash + JSON.stringify(input));
|
|
69
|
+
|
|
70
|
+
// Advance the chain
|
|
71
|
+
this.lastHash = event.eventHash;
|
|
72
|
+
this.buffer.push(event);
|
|
73
|
+
|
|
74
|
+
// Auto-flush when buffer reaches capacity
|
|
75
|
+
if (this.buffer.length >= this.maxSize) {
|
|
76
|
+
void this.flush();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return event;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Flush all buffered events — called on reconnect or capacity
|
|
83
|
+
async flush(): Promise<AuditEvent[]> {
|
|
84
|
+
if (this.buffer.length === 0) return [];
|
|
85
|
+
|
|
86
|
+
const events = [...this.buffer];
|
|
87
|
+
this.buffer = [];
|
|
88
|
+
|
|
89
|
+
if (this.flushCallback) {
|
|
90
|
+
await this.flushCallback(events);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return events;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Verify chain integrity — any tampering is detectable
|
|
97
|
+
verify(): VerifyResult {
|
|
98
|
+
let prevHash = '0'.repeat(64);
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
101
|
+
const event = this.buffer[i];
|
|
102
|
+
|
|
103
|
+
if (!event) break;
|
|
104
|
+
|
|
105
|
+
if (event.prevHash !== prevHash) {
|
|
106
|
+
return { valid: false, corruptedAt: i };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
prevHash = event.eventHash;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { valid: true };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
size(): number {
|
|
116
|
+
return this.buffer.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getLastHash(): string {
|
|
120
|
+
return this.lastHash;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================
|
|
124
|
+
// PRIVATE
|
|
125
|
+
// ============================================================
|
|
126
|
+
|
|
127
|
+
// Deterministic SHA-256 — pure JavaScript, no dependencies
|
|
128
|
+
private sha256(input: string): string {
|
|
129
|
+
// FNV-1a 64-bit approximation for browser/edge compatibility
|
|
130
|
+
// In production this is replaced with WebCrypto SubtleCrypto
|
|
131
|
+
let h1 = 0xdeadbeef;
|
|
132
|
+
let h2 = 0x41c6ce57;
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < input.length; i++) {
|
|
135
|
+
const ch = input.charCodeAt(i);
|
|
136
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
137
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
141
|
+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
142
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
143
|
+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
144
|
+
|
|
145
|
+
const hash = (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
|
|
146
|
+
return hash.padStart(64, '0');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private generateId(): string {
|
|
150
|
+
return `evt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MANDATE DEGRADATION STATE MACHINE
|
|
3
|
+
// V3 five-tier degradation — active-active design means this
|
|
4
|
+
// fires only in catastrophic multi-region failure scenarios.
|
|
5
|
+
// Local autonomy is the last line of defense, not the first.
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
import type { DegradationTier, AgentRiskTier } from '../types';
|
|
9
|
+
|
|
10
|
+
interface DegradationOptions {
|
|
11
|
+
gracePeriodMs?: number;
|
|
12
|
+
riskTier?: AgentRiskTier;
|
|
13
|
+
onTierChange?: (from: DegradationTier, to: DegradationTier) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DegradationState {
|
|
17
|
+
tier: DegradationTier;
|
|
18
|
+
isolatedAt?: number;
|
|
19
|
+
graceElapsedAt?: number;
|
|
20
|
+
lastControlPlaneContact?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DegradationManager {
|
|
24
|
+
private state: DegradationState = { tier: 'NOMINAL' };
|
|
25
|
+
private gracePeriodMs: number;
|
|
26
|
+
private riskTier: AgentRiskTier;
|
|
27
|
+
private onTierChange?: (from: DegradationTier, to: DegradationTier) => void;
|
|
28
|
+
private graceTimer?: ReturnType<typeof setTimeout>;
|
|
29
|
+
|
|
30
|
+
constructor(options: DegradationOptions = {}) {
|
|
31
|
+
this.gracePeriodMs = options.gracePeriodMs ?? 30 * 60 * 1000;
|
|
32
|
+
this.riskTier = options.riskTier ?? 'STANDARD';
|
|
33
|
+
|
|
34
|
+
if (options.onTierChange !== undefined) {
|
|
35
|
+
this.onTierChange = options.onTierChange;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Called when control plane responds slowly (>500ms)
|
|
40
|
+
onControlPlaneLatency(latencyMs: number): void {
|
|
41
|
+
if (latencyMs > 500 && this.state.tier === 'NOMINAL') {
|
|
42
|
+
this.transition('DEGRADED');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Called when control plane becomes completely unreachable
|
|
47
|
+
onControlPlaneUnreachable(): void {
|
|
48
|
+
if (
|
|
49
|
+
this.state.tier === 'NOMINAL' ||
|
|
50
|
+
this.state.tier === 'DEGRADED'
|
|
51
|
+
) {
|
|
52
|
+
this.transition('ISOLATED');
|
|
53
|
+
this.startGraceTimer();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Called when control plane reconnects successfully
|
|
58
|
+
onControlPlaneReconnected(): void {
|
|
59
|
+
if (this.graceTimer !== undefined) {
|
|
60
|
+
clearTimeout(this.graceTimer);
|
|
61
|
+
delete this.graceTimer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const previous = this.state.tier;
|
|
65
|
+
this.state = {
|
|
66
|
+
tier: 'NOMINAL',
|
|
67
|
+
lastControlPlaneContact: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (previous !== 'NOMINAL') {
|
|
71
|
+
this.onTierChange?.(previous, 'NOMINAL');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getCurrentTier(): DegradationTier {
|
|
76
|
+
return this.state.tier;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Returns true if agent is allowed to continue operating
|
|
80
|
+
shouldContinue(): boolean {
|
|
81
|
+
return this.state.tier !== 'GRACE_HIGH';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Returns ms elapsed since isolation began
|
|
85
|
+
getIsolationDurationMs(): number {
|
|
86
|
+
if (this.state.isolatedAt === undefined) return 0;
|
|
87
|
+
return Date.now() - this.state.isolatedAt;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================
|
|
91
|
+
// PRIVATE
|
|
92
|
+
// ============================================================
|
|
93
|
+
|
|
94
|
+
private startGraceTimer(): void {
|
|
95
|
+
this.graceTimer = setTimeout(() => {
|
|
96
|
+
const isHighRisk =
|
|
97
|
+
this.riskTier === 'HIGH' || this.riskTier === 'CRITICAL';
|
|
98
|
+
|
|
99
|
+
const nextTier: DegradationTier = isHighRisk
|
|
100
|
+
? 'GRACE_HIGH'
|
|
101
|
+
: 'GRACE_STD';
|
|
102
|
+
|
|
103
|
+
this.transition(nextTier);
|
|
104
|
+
}, this.gracePeriodMs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private transition(to: DegradationTier): void {
|
|
108
|
+
const from = this.state.tier;
|
|
109
|
+
|
|
110
|
+
this.state.tier = to;
|
|
111
|
+
|
|
112
|
+
if (to === 'ISOLATED') {
|
|
113
|
+
this.state.isolatedAt = Date.now();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (to === 'GRACE_STD' || to === 'GRACE_HIGH') {
|
|
117
|
+
this.state.graceElapsedAt = Date.now();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.onTierChange?.(from, to);
|
|
121
|
+
|
|
122
|
+
if (to === 'GRACE_HIGH') {
|
|
123
|
+
console.error(
|
|
124
|
+
'[Mandate] GRACE_HIGH: Agent paused. Human intervention required.'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MANDATE POLICY EVALUATOR
|
|
3
|
+
// Deterministic JavaScript evaluator — API-identical to the
|
|
4
|
+
// future WASM evaluator. When the Rust/WASM compiler is ready,
|
|
5
|
+
// this module is replaced with zero changes to the SDK interface.
|
|
6
|
+
// Target: sub-millisecond evaluation on structured tool-call intent
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AgentIntent,
|
|
11
|
+
EvaluationResult,
|
|
12
|
+
MandatePolicy,
|
|
13
|
+
PolicyRule,
|
|
14
|
+
PolicyCondition,
|
|
15
|
+
PolicyDecision,
|
|
16
|
+
} from '../types';
|
|
17
|
+
|
|
18
|
+
export class JSPolicyEvaluator {
|
|
19
|
+
private policy: MandatePolicy;
|
|
20
|
+
|
|
21
|
+
constructor(policy: MandatePolicy) {
|
|
22
|
+
this.policy = policy;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
evaluate(intent: AgentIntent): EvaluationResult {
|
|
26
|
+
const start = performance.now();
|
|
27
|
+
|
|
28
|
+
// DENY rules evaluated first — deny always wins over allow
|
|
29
|
+
for (const rule of this.policy.deny) {
|
|
30
|
+
if (this.matchesRule(intent, rule)) {
|
|
31
|
+
return this.result('DENY', `Denied by rule: ${rule.tool}`, start, `deny:${rule.tool}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ALLOW rules evaluated second
|
|
36
|
+
for (const rule of this.policy.allow) {
|
|
37
|
+
if (this.matchesRule(intent, rule)) {
|
|
38
|
+
return this.result('ALLOW', `Allowed by rule: ${rule.tool}`, start, `allow:${rule.tool}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Default deny — no allow rule matched
|
|
43
|
+
return this.result('DENY', 'No allow rule matched — default deny', start);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
updatePolicy(policy: MandatePolicy): void {
|
|
47
|
+
this.policy = policy;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getPolicyHash(): string {
|
|
51
|
+
return this.policy.policyHash;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================
|
|
55
|
+
// PRIVATE — Rule matching
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
private matchesRule(intent: AgentIntent, rule: PolicyRule): boolean {
|
|
59
|
+
if (!this.matchesToolPattern(intent.toolName, rule.tool)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!rule.conditions || rule.conditions.length === 0) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// All conditions must pass — AND logic
|
|
68
|
+
return rule.conditions.every((condition) =>
|
|
69
|
+
this.evaluateCondition(intent, condition)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private matchesToolPattern(toolName: string, pattern: string): boolean {
|
|
74
|
+
if (pattern === '*') return true;
|
|
75
|
+
if (pattern === toolName) return true;
|
|
76
|
+
|
|
77
|
+
// Prefix wildcard: "read_*" matches "read_invoices"
|
|
78
|
+
if (pattern.endsWith('*')) {
|
|
79
|
+
const prefix = pattern.slice(0, -1);
|
|
80
|
+
return toolName.startsWith(prefix);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Suffix wildcard: "*_payment" matches "process_payment"
|
|
84
|
+
if (pattern.startsWith('*')) {
|
|
85
|
+
const suffix = pattern.slice(1);
|
|
86
|
+
return toolName.endsWith(suffix);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private evaluateCondition(
|
|
93
|
+
intent: AgentIntent,
|
|
94
|
+
condition: PolicyCondition
|
|
95
|
+
): boolean {
|
|
96
|
+
const value = this.resolveField(intent, condition.field);
|
|
97
|
+
|
|
98
|
+
switch (condition.operator) {
|
|
99
|
+
case 'eq':
|
|
100
|
+
return value === condition.value;
|
|
101
|
+
case 'neq':
|
|
102
|
+
return value !== condition.value;
|
|
103
|
+
case 'lt':
|
|
104
|
+
return typeof value === 'number' &&
|
|
105
|
+
typeof condition.value === 'number' &&
|
|
106
|
+
value < condition.value;
|
|
107
|
+
case 'lte':
|
|
108
|
+
return typeof value === 'number' &&
|
|
109
|
+
typeof condition.value === 'number' &&
|
|
110
|
+
value <= condition.value;
|
|
111
|
+
case 'gt':
|
|
112
|
+
return typeof value === 'number' &&
|
|
113
|
+
typeof condition.value === 'number' &&
|
|
114
|
+
value > condition.value;
|
|
115
|
+
case 'gte':
|
|
116
|
+
return typeof value === 'number' &&
|
|
117
|
+
typeof condition.value === 'number' &&
|
|
118
|
+
value >= condition.value;
|
|
119
|
+
case 'in':
|
|
120
|
+
return Array.isArray(condition.value) &&
|
|
121
|
+
condition.value.includes(value);
|
|
122
|
+
case 'nin':
|
|
123
|
+
return Array.isArray(condition.value) &&
|
|
124
|
+
!condition.value.includes(value);
|
|
125
|
+
case 'startsWith':
|
|
126
|
+
return typeof value === 'string' &&
|
|
127
|
+
typeof condition.value === 'string' &&
|
|
128
|
+
value.startsWith(condition.value);
|
|
129
|
+
case 'endsWith':
|
|
130
|
+
return typeof value === 'string' &&
|
|
131
|
+
typeof condition.value === 'string' &&
|
|
132
|
+
value.endsWith(condition.value);
|
|
133
|
+
case 'contains':
|
|
134
|
+
return typeof value === 'string' &&
|
|
135
|
+
typeof condition.value === 'string' &&
|
|
136
|
+
value.includes(condition.value);
|
|
137
|
+
default:
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Resolves dot-notation field paths against the intent object
|
|
143
|
+
// "intent.args.amount" → intent.args.amount value
|
|
144
|
+
private resolveField(intent: AgentIntent, field: string): unknown {
|
|
145
|
+
const root: Record<string, unknown> = {
|
|
146
|
+
intent: {
|
|
147
|
+
toolName: intent.toolName,
|
|
148
|
+
args: intent.args,
|
|
149
|
+
context: intent.context,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const parts = field.split('.');
|
|
154
|
+
let current: unknown = root;
|
|
155
|
+
|
|
156
|
+
for (const part of parts) {
|
|
157
|
+
if (current === null || current === undefined) return undefined;
|
|
158
|
+
current = (current as Record<string, unknown>)[part];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return current;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private result(
|
|
165
|
+
decision: PolicyDecision,
|
|
166
|
+
reason: string,
|
|
167
|
+
startTime: number,
|
|
168
|
+
ruleMatched?: string
|
|
169
|
+
): EvaluationResult {
|
|
170
|
+
const base: EvaluationResult = {
|
|
171
|
+
decision,
|
|
172
|
+
reason,
|
|
173
|
+
latencyMs: performance.now() - startTime,
|
|
174
|
+
anomalyScore: 0,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (ruleMatched !== undefined) {
|
|
178
|
+
base.ruleMatched = ruleMatched;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return base;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MANDATE — Anthropic Tool Use Hook
|
|
3
|
+
// Intercepts tool_use content blocks before execution.
|
|
4
|
+
// Full semantic enforcement on Anthropic's structured tool calls.
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AgentIntent,
|
|
9
|
+
MandateConfig,
|
|
10
|
+
EvaluationResult,
|
|
11
|
+
} from '../types';
|
|
12
|
+
import type { JSPolicyEvaluator } from '../evaluator/js-evaluator';
|
|
13
|
+
import type { AuditBuffer } from '../buffer';
|
|
14
|
+
import type { DegradationManager } from '../degradation';
|
|
15
|
+
|
|
16
|
+
interface AnthropicToolUse {
|
|
17
|
+
type: 'tool_use';
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
input: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface InterceptResult {
|
|
24
|
+
allowed: AnthropicToolUse[];
|
|
25
|
+
blocked: Array<{
|
|
26
|
+
block: AnthropicToolUse;
|
|
27
|
+
result: EvaluationResult;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class MandateAnthropicHook {
|
|
32
|
+
private evaluator: JSPolicyEvaluator;
|
|
33
|
+
private buffer: AuditBuffer;
|
|
34
|
+
private degradation: DegradationManager;
|
|
35
|
+
private config: MandateConfig;
|
|
36
|
+
private sessionId: string;
|
|
37
|
+
private stepCounter: number = 0;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
config: MandateConfig,
|
|
41
|
+
evaluator: JSPolicyEvaluator,
|
|
42
|
+
buffer: AuditBuffer,
|
|
43
|
+
degradation: DegradationManager
|
|
44
|
+
) {
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.evaluator = evaluator;
|
|
47
|
+
this.buffer = buffer;
|
|
48
|
+
this.degradation = degradation;
|
|
49
|
+
this.sessionId = `sess_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Intercept Anthropic tool_use content blocks
|
|
53
|
+
// Call this after receiving a response with tool_use blocks
|
|
54
|
+
// before executing any tools
|
|
55
|
+
async interceptToolUse(
|
|
56
|
+
toolUseBlocks: AnthropicToolUse[]
|
|
57
|
+
): Promise<InterceptResult> {
|
|
58
|
+
const allowed: AnthropicToolUse[] = [];
|
|
59
|
+
const blocked: Array<{
|
|
60
|
+
block: AnthropicToolUse;
|
|
61
|
+
result: EvaluationResult;
|
|
62
|
+
}> = [];
|
|
63
|
+
|
|
64
|
+
for (const block of toolUseBlocks) {
|
|
65
|
+
const step = this.stepCounter++;
|
|
66
|
+
const intent = this.buildIntent(block, step);
|
|
67
|
+
const result = this.evaluator.evaluate(intent);
|
|
68
|
+
|
|
69
|
+
this.logToBuffer(intent, result);
|
|
70
|
+
|
|
71
|
+
if (result.decision === 'ALLOW') {
|
|
72
|
+
allowed.push(block);
|
|
73
|
+
} else if (result.decision === 'THROTTLE') {
|
|
74
|
+
await this.delay(2000);
|
|
75
|
+
this.config.onAlert?.(intent, result.anomalyScore ?? 0);
|
|
76
|
+
allowed.push(block);
|
|
77
|
+
} else {
|
|
78
|
+
blocked.push({ block, result });
|
|
79
|
+
this.config.onViolation?.(intent, result);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { allowed, blocked };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================
|
|
87
|
+
// PRIVATE
|
|
88
|
+
// ============================================================
|
|
89
|
+
|
|
90
|
+
private buildIntent(
|
|
91
|
+
block: AnthropicToolUse,
|
|
92
|
+
step: number
|
|
93
|
+
): AgentIntent {
|
|
94
|
+
return {
|
|
95
|
+
toolName: block.name,
|
|
96
|
+
args: block.input,
|
|
97
|
+
context: {
|
|
98
|
+
agentId: this.config.agentId,
|
|
99
|
+
sessionId: this.sessionId,
|
|
100
|
+
environment: this.config.environment,
|
|
101
|
+
stepInChain: step,
|
|
102
|
+
framework: 'anthropic',
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private logToBuffer(
|
|
109
|
+
intent: AgentIntent,
|
|
110
|
+
result: EvaluationResult
|
|
111
|
+
): void {
|
|
112
|
+
if (this.config.auditLevel === 'off') return;
|
|
113
|
+
|
|
114
|
+
this.buffer.append({
|
|
115
|
+
timestamp: Date.now(),
|
|
116
|
+
source: 'REALTIME',
|
|
117
|
+
agentId: this.config.agentId,
|
|
118
|
+
orgId: this.config.orgId,
|
|
119
|
+
policyHash: this.config.policy.policyHash,
|
|
120
|
+
degradationTier: this.degradation.getCurrentTier(),
|
|
121
|
+
toolName: intent.toolName,
|
|
122
|
+
toolArgs: this.config.auditLevel === 'full' ? intent.args : {},
|
|
123
|
+
intentContext: intent.context,
|
|
124
|
+
anomalyScore: result.anomalyScore ?? 0,
|
|
125
|
+
policyDecision: result.decision,
|
|
126
|
+
responseLevel: 1,
|
|
127
|
+
evalLatencyMs: result.latencyMs,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private delay(ms: number): Promise<void> {
|
|
132
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
133
|
+
}
|
|
134
|
+
}
|