@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.
@@ -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
+ }