Skip to content
Merged
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
36 changes: 35 additions & 1 deletion packages/agent-mesh/sdks/typescript/src/trust.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import * as fs from 'fs';
import { AgentIdentity } from './identity';
import {
TrustConfig,
Expand Down Expand Up @@ -27,15 +28,20 @@ interface AgentTrustState {
* with configurable decay.
*/
export class TrustManager {
private readonly config: Required<TrustConfig>;
private readonly config: Required<Omit<TrustConfig, 'persistPath'>>;
private readonly agents: Map<string, AgentTrustState> = new Map();
private readonly persistPath?: string;

constructor(config?: TrustConfig) {
this.config = {
initialScore: config?.initialScore ?? 0.5,
decayFactor: config?.decayFactor ?? 0.95,
thresholds: { ...DEFAULT_THRESHOLDS, ...config?.thresholds },
};
this.persistPath = config?.persistPath;
if (this.persistPath) {
this.loadFromDisk();
}
}

/** Verify a peer agent's identity and return a trust result. */
Expand Down Expand Up @@ -83,6 +89,7 @@ export class TrustManager {
state.successes += 1;
state.score = Math.min(1, state.score + reward);
state.lastUpdate = Date.now();
this.saveToDisk();
}

/** Record a failed interaction with an agent. */
Expand All @@ -92,6 +99,7 @@ export class TrustManager {
state.failures += 1;
state.score = Math.max(0, state.score - penalty);
state.lastUpdate = Date.now();
this.saveToDisk();
}

// ── Private helpers ──
Expand Down Expand Up @@ -133,4 +141,30 @@ export class TrustManager {
if (total === 0) return this.config.initialScore;
return Math.round((state.successes / total) * 1000) / 1000;
}

private saveToDisk(): void {
if (!this.persistPath) return;
try {
const data: Record<string, AgentTrustState> = {};
for (const [key, value] of this.agents) {
data[key] = value;
}
fs.writeFileSync(this.persistPath, JSON.stringify(data), 'utf-8');
} catch {
// best-effort: ignore write errors
}
}

private loadFromDisk(): void {
if (!this.persistPath) return;
try {
const raw = fs.readFileSync(this.persistPath, 'utf-8');
const data = JSON.parse(raw) as Record<string, AgentTrustState>;
for (const [key, value] of Object.entries(data)) {
this.agents.set(key, value);
}
} catch {
// best-effort: ignore missing or corrupt files
}
}
}
2 changes: 2 additions & 0 deletions packages/agent-mesh/sdks/typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface TrustConfig {
trusted: number;
verified: number;
};
/** Optional file path for persisting trust scores across restarts */
persistPath?: string;
}

export type TrustTier = 'Untrusted' | 'Provisional' | 'Trusted' | 'Verified';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as crypto from 'crypto';
import { TrustManager } from '../src/trust';

function tempFilePath(): string {
return path.join(os.tmpdir(), `trust-${crypto.randomUUID()}.json`);
}

afterEach(() => {
// cleanup is best-effort; individual tests also clean up
});

describe('TrustManager persistence', () => {
it('persists scores across instances', () => {
const file = tempFilePath();
try {
const tm1 = new TrustManager({ initialScore: 0.5, persistPath: file });
tm1.recordSuccess('agent-a', 0.1);
tm1.recordFailure('agent-b', 0.2);

// Second instance should load persisted state
const tm2 = new TrustManager({ initialScore: 0.5, persistPath: file });
expect(tm2.getTrustScore('agent-a').overall).toBeGreaterThan(0.5);
expect(tm2.getTrustScore('agent-b').overall).toBeLessThan(0.5);
} finally {
try { fs.unlinkSync(file); } catch { /* ignore */ }
}
});

it('handles missing file gracefully', () => {
const file = tempFilePath(); // does not exist
expect(() => {
const tm = new TrustManager({ initialScore: 0.5, persistPath: file });
expect(tm.getTrustScore('agent-x').overall).toBe(0.5);
}).not.toThrow();
});

it('handles corrupt file gracefully', () => {
const file = tempFilePath();
try {
fs.writeFileSync(file, '<<<not json>>>', 'utf-8');
expect(() => {
const tm = new TrustManager({ initialScore: 0.5, persistPath: file });
expect(tm.getTrustScore('agent-x').overall).toBe(0.5);
}).not.toThrow();
} finally {
try { fs.unlinkSync(file); } catch { /* ignore */ }
}
});

it('does not persist when persistPath is not set', () => {
const tm = new TrustManager({ initialScore: 0.5 });
tm.recordSuccess('agent-a', 0.1);
// No error, no file created — just verifying the code path works
expect(tm.getTrustScore('agent-a').overall).toBeGreaterThan(0.5);
});
});
Loading