@agentkarma/sdk 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/src/index.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @agentkarma/sdk — public entry point.
3
+ *
4
+ * AgentKarma is the reputation layer for autonomous on-chain agents.
5
+ * This SDK lets you query Provider Karma and Consumer Karma, look up
6
+ * ERC-8004 agents on Celo, and gate execution on a local trust policy.
7
+ *
8
+ * Non-routing: this SDK never proxies, signs, or executes transactions on
9
+ * your behalf. It reads from agentkarma.io and answers trust-check questions
10
+ * locally.
11
+ *
12
+ * Quick start:
13
+ *
14
+ * import { createAgentKarmaClient, evaluateTrust } from '@agentkarma/sdk';
15
+ * const ak = createAgentKarmaClient();
16
+ * const snap = await ak.getKarma('Agent5VR…wallet…');
17
+ * const decision = evaluateTrust(snap, { minScore: 60, requireReceiptBacked: true });
18
+ * if (!decision.allowed) console.warn('reject:', decision.reasons);
19
+ */
20
+
21
+ export { createAgentKarmaClient } from './client.js';
22
+ export type { AgentKarmaClient } from './client.js';
23
+
24
+ export {
25
+ AgentKarmaError,
26
+ AgentKarmaValidationError,
27
+ AgentKarmaNotFoundError,
28
+ AgentKarmaRateLimitError,
29
+ AgentKarmaTimeoutError,
30
+ AgentKarmaNetworkError,
31
+ AgentKarmaMalformedResponseError,
32
+ AgentKarmaServerError,
33
+ } from './errors.js';
34
+
35
+ export { evaluateTrust } from './policy.js';
36
+ export type { TrustPolicy, TrustDecision, TrustObserved } from './policy.js';
37
+
38
+ export { buildFeedbackMessage } from './feedback.js';
39
+ export type { BuildFeedbackMessageInput, BuiltFeedbackMessage } from './feedback.js';
40
+
41
+ export type {
42
+ // Primitives
43
+ Chain,
44
+ KarmaFace,
45
+ ConfidenceBadge,
46
+ TrustTier,
47
+ AutonomyLabel,
48
+ SignalTier,
49
+ // Karma snapshot
50
+ KarmaSnapshot,
51
+ KarmaIdentity,
52
+ KarmaFaceData,
53
+ AutonomyData,
54
+ // Dead Man's Switch + Bonding + Surety
55
+ SuccessionStatus,
56
+ SuccessionHeir,
57
+ SuccessionView,
58
+ SuccessionResponse,
59
+ BondStatus,
60
+ BondView,
61
+ BondBlock,
62
+ BondResponse,
63
+ SuretyLabel,
64
+ SuretyView,
65
+ // Celo
66
+ CeloAgentSnapshot,
67
+ CeloAgentRegistration,
68
+ CeloAgentReputation,
69
+ CeloFeedbackRecord,
70
+ // Search
71
+ SearchResult,
72
+ SearchResponse,
73
+ // History
74
+ AgentHistoryResponse,
75
+ AgentHistoryTransaction,
76
+ // Feedback
77
+ FeedbackSummary,
78
+ FeedbackRating,
79
+ FeedbackSubmission,
80
+ FeedbackSubmissionResponse,
81
+ // Client config
82
+ ClientConfig,
83
+ RequestOptions,
84
+ } from './types.js';
package/src/policy.ts ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Local, explainable trust policy evaluation.
3
+ *
4
+ * `evaluateTrust(snapshot, policy)` is the canonical `check_trust_before_execute`
5
+ * helper. Given a karma snapshot from `client.getKarma(wallet)` and a policy
6
+ * config, return whether the trust check passes, the human-readable reasons
7
+ * for the decision, and exactly what was observed on the snapshot.
8
+ *
9
+ * This function NEVER routes, executes, signs, or persists anything. It is a
10
+ * pure function over data. Calling it is free — no network, no side effects.
11
+ */
12
+
13
+ import { AgentKarmaValidationError } from './errors.js';
14
+ import type {
15
+ AutonomyLabel,
16
+ ConfidenceBadge,
17
+ KarmaFace,
18
+ KarmaSnapshot,
19
+ SignalTier,
20
+ SuccessionStatus,
21
+ } from './types.js';
22
+
23
+ /**
24
+ * Succession statuses that mean "the agent is still answering". `declared`
25
+ * counts as live: the will exists but no lapse has been observed yet.
26
+ */
27
+ const LIVE_SUCCESSION_STATUSES: ReadonlySet<SuccessionStatus> = new Set<SuccessionStatus>([
28
+ 'declared',
29
+ 'live',
30
+ ]);
31
+
32
+ export interface TrustPolicy {
33
+ /** Which face to evaluate. Defaults to 'provider'. */
34
+ face?: KarmaFace;
35
+ /** Reject when the chosen face's score is below this value (0-100). */
36
+ minScore?: number;
37
+ /** Accept only these confidence badges on the chosen face. Empty/omitted = accept all. */
38
+ acceptedConfidenceBadges?: ConfidenceBadge[];
39
+ /** Require at least this many on-chain transactions observed. */
40
+ minTxCount?: number;
41
+ /** Require non-null signal in each of these tiers (per the chosen face's tierAggregates). */
42
+ requireTiers?: SignalTier[];
43
+ /** Require at least one Tier-1 receipt-backed signal somewhere on the chosen face. */
44
+ requireReceiptBacked?: boolean;
45
+ /** Reject when the autonomy label matches one of these. (e.g. reject 'agent-like' for human-only flows.) */
46
+ rejectAutonomyLabels?: AutonomyLabel[];
47
+ /** Reject when the autonomy score is below this value. null autonomy is treated as failing this check. */
48
+ minAutonomyScore?: number;
49
+ /**
50
+ * Reject when the wallet has never been observed active. Defaults to false —
51
+ * a wallet with no recorded activity isn't automatically untrustworthy; the
52
+ * face's score and confidence badge already encode that. Flip on when you
53
+ * specifically want a "must have shown up before" gate.
54
+ */
55
+ requireSeen?: boolean;
56
+
57
+ // ── Dead Man's Switch (succession) gates ──────────────────────────────────
58
+ /**
59
+ * Require a live succession plan. Passes only when the snapshot carries a
60
+ * succession block whose derived status is `declared` or `live`. A missing
61
+ * succession block fails this gate (no plan = not live). OBSERVE-ONLY: this
62
+ * reads AK's recorded liveness; AK never receives a real heartbeat.
63
+ */
64
+ requireLiveSuccession?: boolean;
65
+ /**
66
+ * Reject when the succession plan has lapsed or is lapsing — a strong signal
67
+ * the agent may be abandoned. A missing succession block does NOT trip this
68
+ * (no plan ≠ lapsed); use `requireLiveSuccession` to demand a plan.
69
+ */
70
+ rejectLapsed?: boolean;
71
+
72
+ // ── Agent Bonding gates ───────────────────────────────────────────────────
73
+ /**
74
+ * Require at least one currently-active (open) bond on the agent. Borrowed
75
+ * capital lifts confidence, NOT the trust ceiling — this gate is about
76
+ * presence of skin-in-the-game, evaluated independently of score.
77
+ */
78
+ requireBonded?: boolean;
79
+ /**
80
+ * Require the total USDC currently bonded (open bonds) to be at least this.
81
+ * Demo bonds are EXCLUDED from this total — borrowed-on-paper capital must
82
+ * not satisfy a real-money gate.
83
+ */
84
+ minBondedUSDC?: number;
85
+ /**
86
+ * Reject when the agent has a recent bond failure (`resolved_failure`). A
87
+ * blown bond is a real negative delivery signal. Demo bonds are ignored.
88
+ */
89
+ rejectRecentBondFailure?: boolean;
90
+ }
91
+
92
+ export interface TrustObserved {
93
+ face: KarmaFace;
94
+ providerScore: number | null;
95
+ consumerScore: number | null;
96
+ confidenceBadge: ConfidenceBadge | null;
97
+ txCount: number;
98
+ /** Per-tier presence (`true` = signal present for the chosen face). */
99
+ tiers: Record<`tier${SignalTier}`, boolean>;
100
+ autonomyScore: number | null;
101
+ autonomyLabel: AutonomyLabel | null;
102
+ lastActive: string | null;
103
+ /** Derived succession status, or null when no plan was declared. */
104
+ successionStatus: SuccessionStatus | null;
105
+ /** Whether the agent has at least one currently-active (open) bond. */
106
+ bonded: boolean;
107
+ /** Total USDC across open bonds, EXCLUDING demo bonds. */
108
+ activeBondedUsdc: number;
109
+ /** Whether the agent has a resolved-failure bond (demo bonds excluded). */
110
+ hasRecentBondFailure: boolean;
111
+ }
112
+
113
+ export interface TrustDecision {
114
+ /** Final allow/deny. False ⇒ at least one reason is populated. */
115
+ allowed: boolean;
116
+ /**
117
+ * Human-readable rejection reasons. Empty array when `allowed === true`.
118
+ * Each reason references the policy field it tripped, so callers can
119
+ * surface them to operators or log them.
120
+ */
121
+ reasons: string[];
122
+ /** Snapshot of what was observed, for logging / audit. */
123
+ observed: TrustObserved;
124
+ }
125
+
126
+ /**
127
+ * Evaluate a karma snapshot against a trust policy. Never throws on policy
128
+ * mismatch — only throws on malformed inputs.
129
+ */
130
+ export function evaluateTrust(snapshot: KarmaSnapshot, policy: TrustPolicy = {}): TrustDecision {
131
+ if (!snapshot || typeof snapshot !== 'object') {
132
+ throw new AgentKarmaValidationError('snapshot is required');
133
+ }
134
+
135
+ const face: KarmaFace = policy.face ?? 'provider';
136
+ if (face !== 'provider' && face !== 'consumer') {
137
+ throw new AgentKarmaValidationError(`policy.face must be 'provider' or 'consumer'`);
138
+ }
139
+
140
+ const faceData = face === 'provider' ? snapshot.provider : snapshot.consumer;
141
+ const reasons: string[] = [];
142
+
143
+ const providerScore = snapshot.provider?.score ?? null;
144
+ const consumerScore = snapshot.consumer?.score ?? null;
145
+ const confidenceBadge = faceData?.confidenceBadge ?? null;
146
+ const txCount = typeof snapshot.txCount === 'number' ? snapshot.txCount : 0;
147
+
148
+ const tiers: TrustObserved['tiers'] = {
149
+ tier1: hasTier(faceData?.tierAggregates, 1),
150
+ tier2: hasTier(faceData?.tierAggregates, 2),
151
+ tier3: hasTier(faceData?.tierAggregates, 3),
152
+ tier4: hasTier(faceData?.tierAggregates, 4),
153
+ };
154
+
155
+ // Score gate. When the face has no data at all (hasSignal === false AND
156
+ // score is 0/null), treat it as a rejection if a minimum was demanded.
157
+ if (policy.minScore != null) {
158
+ const observedScore = face === 'provider' ? providerScore : consumerScore;
159
+ if (observedScore == null || observedScore < policy.minScore) {
160
+ reasons.push(
161
+ `policy.minScore=${policy.minScore} not met (${face} score ${observedScore ?? 'null'})`,
162
+ );
163
+ }
164
+ }
165
+
166
+ // Confidence badge allowlist.
167
+ if (policy.acceptedConfidenceBadges && policy.acceptedConfidenceBadges.length > 0) {
168
+ if (!confidenceBadge || !policy.acceptedConfidenceBadges.includes(confidenceBadge)) {
169
+ reasons.push(
170
+ `policy.acceptedConfidenceBadges does not include observed badge ${confidenceBadge ?? 'null'}`,
171
+ );
172
+ }
173
+ }
174
+
175
+ // Minimum transaction count — useful "must have shown up enough times" gate.
176
+ if (policy.minTxCount != null && txCount < policy.minTxCount) {
177
+ reasons.push(`policy.minTxCount=${policy.minTxCount} not met (observed txCount ${txCount})`);
178
+ }
179
+
180
+ // Per-tier presence requirement.
181
+ if (policy.requireTiers && policy.requireTiers.length > 0) {
182
+ for (const tier of policy.requireTiers) {
183
+ if (!tiers[`tier${tier}`]) {
184
+ reasons.push(`policy.requireTiers includes Tier ${tier}, which has no signal on ${face} face`);
185
+ }
186
+ }
187
+ }
188
+
189
+ // Receipt-backed shorthand.
190
+ if (policy.requireReceiptBacked && !tiers.tier1) {
191
+ reasons.push(`policy.requireReceiptBacked: ${face} face has no Tier 1 signal`);
192
+ }
193
+
194
+ // Autonomy gates.
195
+ const autonomyScore = snapshot.autonomy?.score ?? null;
196
+ const autonomyLabel = snapshot.autonomy?.label ?? null;
197
+ if (policy.rejectAutonomyLabels && policy.rejectAutonomyLabels.length > 0) {
198
+ if (autonomyLabel && policy.rejectAutonomyLabels.includes(autonomyLabel)) {
199
+ reasons.push(`policy.rejectAutonomyLabels includes observed label ${autonomyLabel}`);
200
+ }
201
+ }
202
+ if (policy.minAutonomyScore != null) {
203
+ if (autonomyScore == null || autonomyScore < policy.minAutonomyScore) {
204
+ reasons.push(
205
+ `policy.minAutonomyScore=${policy.minAutonomyScore} not met (observed ${autonomyScore ?? 'null'})`,
206
+ );
207
+ }
208
+ }
209
+
210
+ // Liveness.
211
+ if (policy.requireSeen && !snapshot.lastActive) {
212
+ reasons.push('policy.requireSeen: wallet has no recorded activity');
213
+ }
214
+
215
+ // ── Dead Man's Switch (succession) observations + gates ───────────────────
216
+ const successionStatus = snapshot.succession?.status ?? null;
217
+ if (policy.requireLiveSuccession) {
218
+ if (!successionStatus) {
219
+ reasons.push('policy.requireLiveSuccession: no succession plan declared');
220
+ } else if (!LIVE_SUCCESSION_STATUSES.has(successionStatus)) {
221
+ reasons.push(
222
+ `policy.requireLiveSuccession: succession status is ${successionStatus} (not declared/live)`,
223
+ );
224
+ }
225
+ }
226
+ if (policy.rejectLapsed && (successionStatus === 'lapsed' || successionStatus === 'lapsing')) {
227
+ reasons.push(`policy.rejectLapsed: succession status is ${successionStatus}`);
228
+ }
229
+
230
+ // ── Agent Bonding observations + gates ────────────────────────────────────
231
+ // Borrowed capital lifts confidence, NEVER the trust ceiling — these gates are
232
+ // about presence/magnitude of skin-in-the-game, kept orthogonal to score.
233
+ const openBonds = snapshot.bond?.open ?? [];
234
+ const resolvedBonds = snapshot.bond?.resolved ?? [];
235
+ const bonded = openBonds.length > 0;
236
+ // Demo bonds are excluded from the real-money total — paper capital must not
237
+ // satisfy a minBondedUSDC gate.
238
+ const activeBondedUsdc = openBonds.reduce(
239
+ (sum, b) => sum + (!b.isDemo && b.currency === 'USDC' ? b.amount : 0),
240
+ 0,
241
+ );
242
+ const hasRecentBondFailure = resolvedBonds.some(
243
+ (b) => !b.isDemo && b.status === 'resolved_failure',
244
+ );
245
+
246
+ if (policy.requireBonded && !bonded) {
247
+ reasons.push('policy.requireBonded: agent has no active bond');
248
+ }
249
+ if (policy.minBondedUSDC != null && activeBondedUsdc < policy.minBondedUSDC) {
250
+ reasons.push(
251
+ `policy.minBondedUSDC=${policy.minBondedUSDC} not met (active non-demo USDC bonded ${activeBondedUsdc})`,
252
+ );
253
+ }
254
+ if (policy.rejectRecentBondFailure && hasRecentBondFailure) {
255
+ reasons.push('policy.rejectRecentBondFailure: agent has a resolved-failure bond');
256
+ }
257
+
258
+ return {
259
+ allowed: reasons.length === 0,
260
+ reasons,
261
+ observed: {
262
+ face,
263
+ providerScore,
264
+ consumerScore,
265
+ confidenceBadge,
266
+ txCount,
267
+ tiers,
268
+ autonomyScore,
269
+ autonomyLabel,
270
+ lastActive: snapshot.lastActive ?? null,
271
+ successionStatus,
272
+ bonded,
273
+ activeBondedUsdc,
274
+ hasRecentBondFailure,
275
+ },
276
+ };
277
+ }
278
+
279
+ function hasTier(
280
+ tierAggregates: Partial<Record<`tier${SignalTier}`, number | null>> | null | undefined,
281
+ tier: SignalTier,
282
+ ): boolean {
283
+ if (!tierAggregates) return false;
284
+ const v = tierAggregates[`tier${tier}`];
285
+ return v != null;
286
+ }