agentcourt 1.0.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,192 @@
1
+ export type AgentCourtPassport = {
2
+ issuer: string;
3
+ trustLevel: "verified" | "unverified";
4
+ issuedAt: string;
5
+ };
6
+ export type AgentCourtAgent = {
7
+ id: string;
8
+ name: string;
9
+ owner: string;
10
+ ownerAddress?: string;
11
+ strategy: string;
12
+ passport: AgentCourtPassport;
13
+ };
14
+ export type ToolDecision = "ALLOW" | "STOP_TOOL" | "HUMAN_IN_THE_LOOP";
15
+ export type ToolCallEvent = {
16
+ id: string;
17
+ agent: AgentCourtAgent;
18
+ tool: string;
19
+ args: Record<string, unknown>;
20
+ decision: ToolDecision;
21
+ reason: string;
22
+ evidenceHash: string;
23
+ latencyMs: number;
24
+ createdAt: string;
25
+ };
26
+ export type AgentCourtPolicy = {
27
+ maxTradeUsd: number;
28
+ maxDailyUsd: number;
29
+ allowedTools: string[];
30
+ humanApprovalTools: string[];
31
+ sensitivePatterns: Array<{
32
+ name: string;
33
+ pattern: string;
34
+ }>;
35
+ };
36
+ export declare const AGENTCOURT_AGORA_POLICY: AgentCourtPolicy;
37
+ export declare class AgentCourtOrchestrator {
38
+ readonly policy: AgentCourtPolicy;
39
+ readonly rpcUrl: string;
40
+ readonly contractAddress: string;
41
+ private dailySpendUsd;
42
+ constructor(policy?: AgentCourtPolicy, rpcUrl?: string, contractAddress?: string);
43
+ /**
44
+ * Low-dependency JSON-RPC client over fetch
45
+ */
46
+ ethCall(to: string, data: string): Promise<string>;
47
+ /**
48
+ * Reads profile on-chain from the AgentCourt smart contract registry
49
+ */
50
+ queryOnChainProfile(ownerAddress: string): Promise<{
51
+ status: "simulated";
52
+ reason: string;
53
+ id?: undefined;
54
+ owner?: undefined;
55
+ stake?: undefined;
56
+ reputation?: undefined;
57
+ totalViolations?: undefined;
58
+ totalSlashed?: undefined;
59
+ onChainStatus?: undefined;
60
+ rawStatusCode?: undefined;
61
+ error?: undefined;
62
+ } | {
63
+ status: "unregistered";
64
+ id: number;
65
+ reason?: undefined;
66
+ owner?: undefined;
67
+ stake?: undefined;
68
+ reputation?: undefined;
69
+ totalViolations?: undefined;
70
+ totalSlashed?: undefined;
71
+ onChainStatus?: undefined;
72
+ rawStatusCode?: undefined;
73
+ error?: undefined;
74
+ } | {
75
+ status: "verified";
76
+ id: number;
77
+ owner: string;
78
+ stake: number;
79
+ reputation: number;
80
+ totalViolations: number;
81
+ totalSlashed: number;
82
+ onChainStatus: string;
83
+ rawStatusCode: number;
84
+ reason?: undefined;
85
+ error?: undefined;
86
+ } | {
87
+ status: "sandbox";
88
+ error: any;
89
+ reason?: undefined;
90
+ id?: undefined;
91
+ owner?: undefined;
92
+ stake?: undefined;
93
+ reputation?: undefined;
94
+ totalViolations?: undefined;
95
+ totalSlashed?: undefined;
96
+ onChainStatus?: undefined;
97
+ rawStatusCode?: undefined;
98
+ }>;
99
+ callTool(agent: AgentCourtAgent, tool: string, args: Record<string, unknown>, execute?: () => Promise<unknown> | unknown): Promise<{
100
+ event: ToolCallEvent;
101
+ result: unknown;
102
+ }>;
103
+ evaluate(agent: AgentCourtAgent, tool: string, args: Record<string, unknown>, onChainSlashed?: boolean): ToolCallEvent;
104
+ }
105
+ /**
106
+ * The AgoraAgentClient is a zero-dependency client that interfaces with both the Arc Testnet RPC and the Megent Orchestration Gateway
107
+ */
108
+ export declare class AgoraAgentClient {
109
+ readonly endpoint: string;
110
+ readonly agent: AgentCourtAgent | undefined;
111
+ readonly runId: string | undefined;
112
+ readonly rpcUrl: string;
113
+ readonly contractAddress: string;
114
+ constructor({ endpoint, agent, runId, rpcUrl, contractAddress }?: {
115
+ endpoint?: string;
116
+ agent?: AgentCourtAgent;
117
+ runId?: string;
118
+ rpcUrl?: string;
119
+ contractAddress?: string;
120
+ });
121
+ /**
122
+ * Low-dependency JSON-RPC client over fetch
123
+ */
124
+ ethCall(to: string, data: string): Promise<string>;
125
+ /**
126
+ * Queries the AgentCourt contract to verify agent's profile on-chain.
127
+ */
128
+ queryOnChainProfile(ownerAddress: string): Promise<{
129
+ status: "simulated";
130
+ reason: string;
131
+ id?: undefined;
132
+ owner?: undefined;
133
+ stake?: undefined;
134
+ reputation?: undefined;
135
+ totalViolations?: undefined;
136
+ totalSlashed?: undefined;
137
+ onChainStatus?: undefined;
138
+ rawStatusCode?: undefined;
139
+ error?: undefined;
140
+ } | {
141
+ status: "unregistered";
142
+ id: number;
143
+ reason?: undefined;
144
+ owner?: undefined;
145
+ stake?: undefined;
146
+ reputation?: undefined;
147
+ totalViolations?: undefined;
148
+ totalSlashed?: undefined;
149
+ onChainStatus?: undefined;
150
+ rawStatusCode?: undefined;
151
+ error?: undefined;
152
+ } | {
153
+ status: "verified";
154
+ id: number;
155
+ owner: string;
156
+ stake: number;
157
+ reputation: number;
158
+ totalViolations: number;
159
+ totalSlashed: number;
160
+ onChainStatus: string;
161
+ rawStatusCode: number;
162
+ reason?: undefined;
163
+ error?: undefined;
164
+ } | {
165
+ status: "sandbox";
166
+ error: any;
167
+ reason?: undefined;
168
+ id?: undefined;
169
+ owner?: undefined;
170
+ stake?: undefined;
171
+ reputation?: undefined;
172
+ totalViolations?: undefined;
173
+ totalSlashed?: undefined;
174
+ onChainStatus?: undefined;
175
+ rawStatusCode?: undefined;
176
+ }>;
177
+ callTool(tool: string, args: Record<string, unknown>, execute?: (args: Record<string, unknown>) => Promise<unknown> | unknown): Promise<{
178
+ ok: boolean;
179
+ needsApproval: boolean;
180
+ event: any;
181
+ result: null;
182
+ } | {
183
+ ok: boolean;
184
+ event: any;
185
+ result: unknown;
186
+ needsApproval?: undefined;
187
+ }>;
188
+ }
189
+ export declare function createVerifiedAgent(input: Omit<AgentCourtAgent, "passport"> & {
190
+ ownerAddress?: string;
191
+ }): AgentCourtAgent;
192
+ export declare function hashEvidence(value: unknown): string;
package/dist/index.js ADDED
@@ -0,0 +1,323 @@
1
+ import { createHash } from "crypto";
2
+ export const AGENTCOURT_AGORA_POLICY = {
3
+ maxTradeUsd: 2500,
4
+ maxDailyUsd: 10000,
5
+ allowedTools: [
6
+ "market.read",
7
+ "risk.score",
8
+ "arc.quote",
9
+ "arc.transfer_usdc",
10
+ "arc.publish_trace",
11
+ ],
12
+ humanApprovalTools: ["arc.transfer_usdc"],
13
+ sensitivePatterns: [
14
+ { name: "private key", pattern: "private[_ -]?key|seed phrase|mnemonic" },
15
+ { name: "phone", pattern: "\\+?\\d[\\d\\s().-]{7,}\\d" },
16
+ { name: "email", pattern: "[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}" },
17
+ ],
18
+ };
19
+ export class AgentCourtOrchestrator {
20
+ policy;
21
+ rpcUrl;
22
+ contractAddress;
23
+ dailySpendUsd = 0;
24
+ constructor(policy = AGENTCOURT_AGORA_POLICY, rpcUrl = "https://rpc.testnet.arc.network", contractAddress = "0x1a6389aa779BD3C01B7867bB76a9B51f283f9B3a") {
25
+ this.policy = policy;
26
+ this.rpcUrl = rpcUrl;
27
+ this.contractAddress = contractAddress;
28
+ }
29
+ /**
30
+ * Low-dependency JSON-RPC client over fetch
31
+ */
32
+ async ethCall(to, data) {
33
+ try {
34
+ const response = await fetch(this.rpcUrl, {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify({
38
+ jsonrpc: "2.0",
39
+ id: Date.now(),
40
+ method: "eth_call",
41
+ params: [
42
+ { to, data },
43
+ "latest"
44
+ ]
45
+ })
46
+ });
47
+ const result = await response.json();
48
+ if (result.error) {
49
+ throw new Error(result.error.message);
50
+ }
51
+ return result.result;
52
+ }
53
+ catch (error) {
54
+ throw new Error(`RPC Connection failed: ${error.message}`);
55
+ }
56
+ }
57
+ /**
58
+ * Reads profile on-chain from the AgentCourt smart contract registry
59
+ */
60
+ async queryOnChainProfile(ownerAddress) {
61
+ if (!ownerAddress || !ownerAddress.startsWith("0x")) {
62
+ return { status: "simulated", reason: "Invalid owner address" };
63
+ }
64
+ try {
65
+ // 1. Get Agent ID of Owner
66
+ const addressClean = ownerAddress.slice(2).toLowerCase();
67
+ const agentIdData = `0xb3631cc9` + addressClean.padStart(64, "0");
68
+ const agentIdHex = await this.ethCall(this.contractAddress, agentIdData);
69
+ if (!agentIdHex || agentIdHex === "0x" || /^0x0*$/.test(agentIdHex)) {
70
+ return { status: "unregistered", id: 0 };
71
+ }
72
+ const agentId = parseInt(agentIdHex, 16);
73
+ // 2. Get Agent Profile from ID
74
+ const profileData = `0x16b06387` + agentId.toString(16).padStart(64, "0");
75
+ const profileHex = await this.ethCall(this.contractAddress, profileData);
76
+ if (!profileHex || profileHex === "0x") {
77
+ return { status: "unregistered", id: agentId };
78
+ }
79
+ const dataHex = profileHex.startsWith("0x") ? profileHex.slice(2) : profileHex;
80
+ const ownerHex = "0x" + dataHex.substring(24, 64);
81
+ const stake = BigInt("0x" + dataHex.substring(128, 192));
82
+ const reputation = parseInt(dataHex.substring(192, 256), 16);
83
+ const totalViolations = parseInt(dataHex.substring(256, 320), 16);
84
+ const totalSlashed = BigInt("0x" + dataHex.substring(320, 384));
85
+ const statusCode = parseInt(dataHex.substring(384, 448), 16);
86
+ const statuses = ["None", "Active", "Slashed"];
87
+ const status = statuses[statusCode] || "Unknown";
88
+ return {
89
+ status: "verified",
90
+ id: agentId,
91
+ owner: ownerHex,
92
+ stake: Number(stake) / 1e6, // USDC decimals
93
+ reputation,
94
+ totalViolations,
95
+ totalSlashed: Number(totalSlashed) / 1e6,
96
+ onChainStatus: status,
97
+ rawStatusCode: statusCode
98
+ };
99
+ }
100
+ catch (error) {
101
+ return {
102
+ status: "sandbox",
103
+ error: error.message
104
+ };
105
+ }
106
+ }
107
+ async callTool(agent, tool, args, execute) {
108
+ // Check status on-chain
109
+ let onChainSlashed = false;
110
+ const owner = agent.owner || agent.ownerAddress;
111
+ if (owner) {
112
+ const profile = await this.queryOnChainProfile(owner);
113
+ if (profile.status === "verified" && profile.onChainStatus === "Slashed") {
114
+ onChainSlashed = true;
115
+ }
116
+ }
117
+ const event = this.evaluate(agent, tool, args, onChainSlashed);
118
+ if (event.decision !== "ALLOW") {
119
+ return { event, result: null };
120
+ }
121
+ const amount = amountFromArgs(args);
122
+ this.dailySpendUsd += amount;
123
+ const result = execute ? await execute() : { accepted: true };
124
+ return { event, result };
125
+ }
126
+ evaluate(agent, tool, args, onChainSlashed = false) {
127
+ const started = performance.now();
128
+ const amount = amountFromArgs(args);
129
+ const sensitiveFindings = findSensitiveValues(this.policy, args);
130
+ let decision = "ALLOW";
131
+ let reason = "Policy checks passed.";
132
+ if (onChainSlashed) {
133
+ decision = "STOP_TOOL";
134
+ reason = "Agent status on-chain is Slashed. Tool calls permanently frozen.";
135
+ }
136
+ else if (!this.policy.allowedTools.includes(tool)) {
137
+ decision = "STOP_TOOL";
138
+ reason = "Tool is not allowlisted for this agent.";
139
+ }
140
+ else if (agent.passport.trustLevel !== "verified") {
141
+ decision = "STOP_TOOL";
142
+ reason = "Agent passport is missing or unverified.";
143
+ }
144
+ else if (sensitiveFindings.length > 0) {
145
+ decision = "STOP_TOOL";
146
+ reason = `Sensitive data detected: ${sensitiveFindings.join(", ")}.`;
147
+ }
148
+ else if (amount > this.policy.maxTradeUsd) {
149
+ decision = "HUMAN_IN_THE_LOOP";
150
+ reason = `Trade exceeds per-action limit of ${this.policy.maxTradeUsd} USDC.`;
151
+ }
152
+ else if (this.dailySpendUsd + amount > this.policy.maxDailyUsd) {
153
+ decision = "HUMAN_IN_THE_LOOP";
154
+ reason = `Run would exceed daily budget of ${this.policy.maxDailyUsd} USDC.`;
155
+ }
156
+ else if (this.policy.humanApprovalTools.includes(tool) && amount > 1000) {
157
+ decision = "HUMAN_IN_THE_LOOP";
158
+ reason = "High-value Arc settlement requires operator approval.";
159
+ }
160
+ const evidenceHash = hashEvidence({ agentId: agent.id, tool, args, decision, reason });
161
+ return {
162
+ id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
163
+ agent,
164
+ tool,
165
+ args,
166
+ decision,
167
+ reason,
168
+ evidenceHash,
169
+ latencyMs: Number((performance.now() - started).toFixed(2)),
170
+ createdAt: new Date().toISOString(),
171
+ };
172
+ }
173
+ }
174
+ /**
175
+ * The AgoraAgentClient is a zero-dependency client that interfaces with both the Arc Testnet RPC and the Megent Orchestration Gateway
176
+ */
177
+ export class AgoraAgentClient {
178
+ endpoint;
179
+ agent;
180
+ runId;
181
+ rpcUrl;
182
+ contractAddress;
183
+ constructor({ endpoint = "http://localhost:4141", agent, runId, rpcUrl = "https://rpc.testnet.arc.network", contractAddress = "0x1a6389aa779BD3C01B7867bB76a9B51f283f9B3a" } = {}) {
184
+ this.endpoint = endpoint.replace(/\/$/, "");
185
+ this.agent = agent;
186
+ this.runId = runId;
187
+ this.rpcUrl = rpcUrl;
188
+ this.contractAddress = contractAddress;
189
+ }
190
+ /**
191
+ * Low-dependency JSON-RPC client over fetch
192
+ */
193
+ async ethCall(to, data) {
194
+ try {
195
+ const response = await fetch(this.rpcUrl, {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/json" },
198
+ body: JSON.stringify({
199
+ jsonrpc: "2.0",
200
+ id: Date.now(),
201
+ method: "eth_call",
202
+ params: [
203
+ { to, data },
204
+ "latest"
205
+ ]
206
+ })
207
+ });
208
+ const result = await response.json();
209
+ if (result.error) {
210
+ throw new Error(result.error.message);
211
+ }
212
+ return result.result;
213
+ }
214
+ catch (error) {
215
+ throw new Error(`RPC Connection failed: ${error.message}`);
216
+ }
217
+ }
218
+ /**
219
+ * Queries the AgentCourt contract to verify agent's profile on-chain.
220
+ */
221
+ async queryOnChainProfile(ownerAddress) {
222
+ if (!ownerAddress || !ownerAddress.startsWith("0x")) {
223
+ return { status: "simulated", reason: "Invalid owner address" };
224
+ }
225
+ try {
226
+ const addressClean = ownerAddress.slice(2).toLowerCase();
227
+ const agentIdData = `0xb3631cc9` + addressClean.padStart(64, "0");
228
+ const agentIdHex = await this.ethCall(this.contractAddress, agentIdData);
229
+ if (!agentIdHex || agentIdHex === "0x" || /^0x0*$/.test(agentIdHex)) {
230
+ return { status: "unregistered", id: 0 };
231
+ }
232
+ const agentId = parseInt(agentIdHex, 16);
233
+ const profileData = `0x16b06387` + agentId.toString(16).padStart(64, "0");
234
+ const profileHex = await this.ethCall(this.contractAddress, profileData);
235
+ if (!profileHex || profileHex === "0x") {
236
+ return { status: "unregistered", id: agentId };
237
+ }
238
+ const dataHex = profileHex.startsWith("0x") ? profileHex.slice(2) : profileHex;
239
+ const ownerHex = "0x" + dataHex.substring(24, 64);
240
+ const stake = BigInt("0x" + dataHex.substring(128, 192));
241
+ const reputation = parseInt(dataHex.substring(192, 256), 16);
242
+ const totalViolations = parseInt(dataHex.substring(256, 320), 16);
243
+ const totalSlashed = BigInt("0x" + dataHex.substring(320, 384));
244
+ const statusCode = parseInt(dataHex.substring(384, 448), 16);
245
+ const statuses = ["None", "Active", "Slashed"];
246
+ const status = statuses[statusCode] || "Unknown";
247
+ return {
248
+ status: "verified",
249
+ id: agentId,
250
+ owner: ownerHex,
251
+ stake: Number(stake) / 1e6, // USDC
252
+ reputation,
253
+ totalViolations,
254
+ totalSlashed: Number(totalSlashed) / 1e6,
255
+ onChainStatus: status,
256
+ rawStatusCode: statusCode
257
+ };
258
+ }
259
+ catch (error) {
260
+ return {
261
+ status: "sandbox",
262
+ error: error.message
263
+ };
264
+ }
265
+ }
266
+ async callTool(tool, args, execute) {
267
+ let onChainProfile = null;
268
+ const owner = this.agent?.owner || this.agent?.ownerAddress;
269
+ if (this.agent && owner) {
270
+ onChainProfile = await this.queryOnChainProfile(owner);
271
+ }
272
+ else {
273
+ onChainProfile = { status: "sandbox", reason: "No agent owner address provided" };
274
+ }
275
+ const response = await fetch(`${this.endpoint}/api/tool-call`, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({
279
+ runId: this.runId,
280
+ agent: {
281
+ ...this.agent,
282
+ onChainProfile
283
+ },
284
+ tool,
285
+ args
286
+ })
287
+ });
288
+ const event = await response.json();
289
+ if (event.verdict?.action === "STOP_TOOL") {
290
+ return { ok: false, event, result: null };
291
+ }
292
+ if (event.verdict?.action === "HUMAN_IN_THE_LOOP") {
293
+ return { ok: false, needsApproval: true, event, result: null };
294
+ }
295
+ const result = execute ? await execute(args) : { accepted: true };
296
+ return { ok: true, event, result };
297
+ }
298
+ }
299
+ export function createVerifiedAgent(input) {
300
+ return {
301
+ ...input,
302
+ owner: input.owner || input.ownerAddress || `0x${Math.random().toString(16).slice(2).padEnd(40, "0")}`,
303
+ passport: {
304
+ issuer: "agentcourt-arc",
305
+ trustLevel: "verified",
306
+ issuedAt: new Date().toISOString(),
307
+ },
308
+ };
309
+ }
310
+ export function hashEvidence(value) {
311
+ return `0x${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
312
+ }
313
+ function amountFromArgs(args) {
314
+ const value = args.amountUsd ?? args.notionalUsd ?? 0;
315
+ return typeof value === "number" ? value : Number(value) || 0;
316
+ }
317
+ function findSensitiveValues(policy, value) {
318
+ const raw = JSON.stringify(value ?? {});
319
+ return policy.sensitivePatterns
320
+ .map((item) => ({ ...item, regex: new RegExp(item.pattern, "i") }))
321
+ .filter((item) => item.regex.test(raw))
322
+ .map((item) => item.name);
323
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "agentcourt",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready zero-dependency Web3 security gateway and orchestration SDK for AI agents on Arc Network",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "type": "module",
9
+ "scripts": {
10
+ "build": "tsc"
11
+ },
12
+ "keywords": [
13
+ "agents",
14
+ "arc",
15
+ "web3",
16
+ "agentcourt",
17
+ "security",
18
+ "governance"
19
+ ],
20
+ "author": "Megents Team",
21
+ "license": "Apache-2.0",
22
+ "devDependencies": {
23
+ "typescript": "^5.0.0",
24
+ "@types/node": "^20.0.0"
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,416 @@
1
+ import { createHash } from "crypto";
2
+
3
+ export type AgentCourtPassport = {
4
+ issuer: string;
5
+ trustLevel: "verified" | "unverified";
6
+ issuedAt: string;
7
+ };
8
+
9
+ export type AgentCourtAgent = {
10
+ id: string;
11
+ name: string;
12
+ owner: string;
13
+ ownerAddress?: string;
14
+ strategy: string;
15
+ passport: AgentCourtPassport;
16
+ };
17
+
18
+ export type ToolDecision = "ALLOW" | "STOP_TOOL" | "HUMAN_IN_THE_LOOP";
19
+
20
+ export type ToolCallEvent = {
21
+ id: string;
22
+ agent: AgentCourtAgent;
23
+ tool: string;
24
+ args: Record<string, unknown>;
25
+ decision: ToolDecision;
26
+ reason: string;
27
+ evidenceHash: string;
28
+ latencyMs: number;
29
+ createdAt: string;
30
+ };
31
+
32
+ export type AgentCourtPolicy = {
33
+ maxTradeUsd: number;
34
+ maxDailyUsd: number;
35
+ allowedTools: string[];
36
+ humanApprovalTools: string[];
37
+ sensitivePatterns: Array<{ name: string; pattern: string }>;
38
+ };
39
+
40
+ export const AGENTCOURT_AGORA_POLICY: AgentCourtPolicy = {
41
+ maxTradeUsd: 2500,
42
+ maxDailyUsd: 10000,
43
+ allowedTools: [
44
+ "market.read",
45
+ "risk.score",
46
+ "arc.quote",
47
+ "arc.transfer_usdc",
48
+ "arc.publish_trace",
49
+ ],
50
+ humanApprovalTools: ["arc.transfer_usdc"],
51
+ sensitivePatterns: [
52
+ { name: "private key", pattern: "private[_ -]?key|seed phrase|mnemonic" },
53
+ { name: "phone", pattern: "\\+?\\d[\\d\\s().-]{7,}\\d" },
54
+ { name: "email", pattern: "[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}" },
55
+ ],
56
+ };
57
+
58
+ export class AgentCourtOrchestrator {
59
+ private dailySpendUsd = 0;
60
+
61
+ constructor(
62
+ public readonly policy: AgentCourtPolicy = AGENTCOURT_AGORA_POLICY,
63
+ public readonly rpcUrl: string = "https://rpc.testnet.arc.network",
64
+ public readonly contractAddress: string = "0x1a6389aa779BD3C01B7867bB76a9B51f283f9B3a"
65
+ ) {}
66
+
67
+ /**
68
+ * Low-dependency JSON-RPC client over fetch
69
+ */
70
+ async ethCall(to: string, data: string): Promise<string> {
71
+ try {
72
+ const response = await fetch(this.rpcUrl, {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({
76
+ jsonrpc: "2.0",
77
+ id: Date.now(),
78
+ method: "eth_call",
79
+ params: [
80
+ { to, data },
81
+ "latest"
82
+ ]
83
+ })
84
+ });
85
+ const result = await response.json();
86
+ if (result.error) {
87
+ throw new Error(result.error.message);
88
+ }
89
+ return result.result;
90
+ } catch (error: any) {
91
+ throw new Error(`RPC Connection failed: ${error.message}`);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Reads profile on-chain from the AgentCourt smart contract registry
97
+ */
98
+ async queryOnChainProfile(ownerAddress: string) {
99
+ if (!ownerAddress || !ownerAddress.startsWith("0x")) {
100
+ return { status: "simulated" as const, reason: "Invalid owner address" };
101
+ }
102
+
103
+ try {
104
+ // 1. Get Agent ID of Owner
105
+ const addressClean = ownerAddress.slice(2).toLowerCase();
106
+ const agentIdData = `0xb3631cc9` + addressClean.padStart(64, "0");
107
+ const agentIdHex = await this.ethCall(this.contractAddress, agentIdData);
108
+
109
+ if (!agentIdHex || agentIdHex === "0x" || /^0x0*$/.test(agentIdHex)) {
110
+ return { status: "unregistered" as const, id: 0 };
111
+ }
112
+
113
+ const agentId = parseInt(agentIdHex, 16);
114
+
115
+ // 2. Get Agent Profile from ID
116
+ const profileData = `0x16b06387` + agentId.toString(16).padStart(64, "0");
117
+ const profileHex = await this.ethCall(this.contractAddress, profileData);
118
+
119
+ if (!profileHex || profileHex === "0x") {
120
+ return { status: "unregistered" as const, id: agentId };
121
+ }
122
+
123
+ const dataHex = profileHex.startsWith("0x") ? profileHex.slice(2) : profileHex;
124
+ const ownerHex = "0x" + dataHex.substring(24, 64);
125
+ const stake = BigInt("0x" + dataHex.substring(128, 192));
126
+ const reputation = parseInt(dataHex.substring(192, 256), 16);
127
+ const totalViolations = parseInt(dataHex.substring(256, 320), 16);
128
+ const totalSlashed = BigInt("0x" + dataHex.substring(320, 384));
129
+ const statusCode = parseInt(dataHex.substring(384, 448), 16);
130
+
131
+ const statuses = ["None", "Active", "Slashed"];
132
+ const status = statuses[statusCode] || "Unknown";
133
+
134
+ return {
135
+ status: "verified" as const,
136
+ id: agentId,
137
+ owner: ownerHex,
138
+ stake: Number(stake) / 1e6, // USDC decimals
139
+ reputation,
140
+ totalViolations,
141
+ totalSlashed: Number(totalSlashed) / 1e6,
142
+ onChainStatus: status,
143
+ rawStatusCode: statusCode
144
+ };
145
+ } catch (error: any) {
146
+ return {
147
+ status: "sandbox" as const,
148
+ error: error.message
149
+ };
150
+ }
151
+ }
152
+
153
+ async callTool(
154
+ agent: AgentCourtAgent,
155
+ tool: string,
156
+ args: Record<string, unknown>,
157
+ execute?: () => Promise<unknown> | unknown
158
+ ) {
159
+ // Check status on-chain
160
+ let onChainSlashed = false;
161
+ const owner = agent.owner || agent.ownerAddress;
162
+ if (owner) {
163
+ const profile = await this.queryOnChainProfile(owner);
164
+ if (profile.status === "verified" && profile.onChainStatus === "Slashed") {
165
+ onChainSlashed = true;
166
+ }
167
+ }
168
+
169
+ const event = this.evaluate(agent, tool, args, onChainSlashed);
170
+
171
+ if (event.decision !== "ALLOW") {
172
+ return { event, result: null };
173
+ }
174
+
175
+ const amount = amountFromArgs(args);
176
+ this.dailySpendUsd += amount;
177
+ const result = execute ? await execute() : { accepted: true };
178
+ return { event, result };
179
+ }
180
+
181
+ evaluate(
182
+ agent: AgentCourtAgent,
183
+ tool: string,
184
+ args: Record<string, unknown>,
185
+ onChainSlashed: boolean = false
186
+ ): ToolCallEvent {
187
+ const started = performance.now();
188
+ const amount = amountFromArgs(args);
189
+ const sensitiveFindings = findSensitiveValues(this.policy, args);
190
+ let decision: ToolDecision = "ALLOW";
191
+ let reason = "Policy checks passed.";
192
+
193
+ if (onChainSlashed) {
194
+ decision = "STOP_TOOL";
195
+ reason = "Agent status on-chain is Slashed. Tool calls permanently frozen.";
196
+ } else if (!this.policy.allowedTools.includes(tool)) {
197
+ decision = "STOP_TOOL";
198
+ reason = "Tool is not allowlisted for this agent.";
199
+ } else if (agent.passport.trustLevel !== "verified") {
200
+ decision = "STOP_TOOL";
201
+ reason = "Agent passport is missing or unverified.";
202
+ } else if (sensitiveFindings.length > 0) {
203
+ decision = "STOP_TOOL";
204
+ reason = `Sensitive data detected: ${sensitiveFindings.join(", ")}.`;
205
+ } else if (amount > this.policy.maxTradeUsd) {
206
+ decision = "HUMAN_IN_THE_LOOP";
207
+ reason = `Trade exceeds per-action limit of ${this.policy.maxTradeUsd} USDC.`;
208
+ } else if (this.dailySpendUsd + amount > this.policy.maxDailyUsd) {
209
+ decision = "HUMAN_IN_THE_LOOP";
210
+ reason = `Run would exceed daily budget of ${this.policy.maxDailyUsd} USDC.`;
211
+ } else if (this.policy.humanApprovalTools.includes(tool) && amount > 1000) {
212
+ decision = "HUMAN_IN_THE_LOOP";
213
+ reason = "High-value Arc settlement requires operator approval.";
214
+ }
215
+
216
+ const evidenceHash = hashEvidence({ agentId: agent.id, tool, args, decision, reason });
217
+
218
+ return {
219
+ id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
220
+ agent,
221
+ tool,
222
+ args,
223
+ decision,
224
+ reason,
225
+ evidenceHash,
226
+ latencyMs: Number((performance.now() - started).toFixed(2)),
227
+ createdAt: new Date().toISOString(),
228
+ };
229
+ }
230
+ }
231
+
232
+ /**
233
+ * The AgoraAgentClient is a zero-dependency client that interfaces with both the Arc Testnet RPC and the Megent Orchestration Gateway
234
+ */
235
+ export class AgoraAgentClient {
236
+ public readonly endpoint: string;
237
+ public readonly agent: AgentCourtAgent | undefined;
238
+ public readonly runId: string | undefined;
239
+ public readonly rpcUrl: string;
240
+ public readonly contractAddress: string;
241
+
242
+ constructor({
243
+ endpoint = "http://localhost:4141",
244
+ agent,
245
+ runId,
246
+ rpcUrl = "https://rpc.testnet.arc.network",
247
+ contractAddress = "0x1a6389aa779BD3C01B7867bB76a9B51f283f9B3a"
248
+ }: {
249
+ endpoint?: string;
250
+ agent?: AgentCourtAgent;
251
+ runId?: string;
252
+ rpcUrl?: string;
253
+ contractAddress?: string;
254
+ } = {}) {
255
+ this.endpoint = endpoint.replace(/\/$/, "");
256
+ this.agent = agent;
257
+ this.runId = runId;
258
+ this.rpcUrl = rpcUrl;
259
+ this.contractAddress = contractAddress;
260
+ }
261
+
262
+ /**
263
+ * Low-dependency JSON-RPC client over fetch
264
+ */
265
+ async ethCall(to: string, data: string): Promise<string> {
266
+ try {
267
+ const response = await fetch(this.rpcUrl, {
268
+ method: "POST",
269
+ headers: { "Content-Type": "application/json" },
270
+ body: JSON.stringify({
271
+ jsonrpc: "2.0",
272
+ id: Date.now(),
273
+ method: "eth_call",
274
+ params: [
275
+ { to, data },
276
+ "latest"
277
+ ]
278
+ })
279
+ });
280
+ const result = await response.json();
281
+ if (result.error) {
282
+ throw new Error(result.error.message);
283
+ }
284
+ return result.result;
285
+ } catch (error: any) {
286
+ throw new Error(`RPC Connection failed: ${error.message}`);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Queries the AgentCourt contract to verify agent's profile on-chain.
292
+ */
293
+ async queryOnChainProfile(ownerAddress: string) {
294
+ if (!ownerAddress || !ownerAddress.startsWith("0x")) {
295
+ return { status: "simulated" as const, reason: "Invalid owner address" };
296
+ }
297
+
298
+ try {
299
+ const addressClean = ownerAddress.slice(2).toLowerCase();
300
+ const agentIdData = `0xb3631cc9` + addressClean.padStart(64, "0");
301
+ const agentIdHex = await this.ethCall(this.contractAddress, agentIdData);
302
+
303
+ if (!agentIdHex || agentIdHex === "0x" || /^0x0*$/.test(agentIdHex)) {
304
+ return { status: "unregistered" as const, id: 0 };
305
+ }
306
+
307
+ const agentId = parseInt(agentIdHex, 16);
308
+
309
+ const profileData = `0x16b06387` + agentId.toString(16).padStart(64, "0");
310
+ const profileHex = await this.ethCall(this.contractAddress, profileData);
311
+
312
+ if (!profileHex || profileHex === "0x") {
313
+ return { status: "unregistered" as const, id: agentId };
314
+ }
315
+
316
+ const dataHex = profileHex.startsWith("0x") ? profileHex.slice(2) : profileHex;
317
+
318
+ const ownerHex = "0x" + dataHex.substring(24, 64);
319
+ const stake = BigInt("0x" + dataHex.substring(128, 192));
320
+ const reputation = parseInt(dataHex.substring(192, 256), 16);
321
+ const totalViolations = parseInt(dataHex.substring(256, 320), 16);
322
+ const totalSlashed = BigInt("0x" + dataHex.substring(320, 384));
323
+ const statusCode = parseInt(dataHex.substring(384, 448), 16);
324
+
325
+ const statuses = ["None", "Active", "Slashed"];
326
+ const status = statuses[statusCode] || "Unknown";
327
+
328
+ return {
329
+ status: "verified" as const,
330
+ id: agentId,
331
+ owner: ownerHex,
332
+ stake: Number(stake) / 1e6, // USDC
333
+ reputation,
334
+ totalViolations,
335
+ totalSlashed: Number(totalSlashed) / 1e6,
336
+ onChainStatus: status,
337
+ rawStatusCode: statusCode
338
+ };
339
+ } catch (error: any) {
340
+ return {
341
+ status: "sandbox" as const,
342
+ error: error.message
343
+ };
344
+ }
345
+ }
346
+
347
+ async callTool(
348
+ tool: string,
349
+ args: Record<string, unknown>,
350
+ execute?: (args: Record<string, unknown>) => Promise<unknown> | unknown
351
+ ) {
352
+ let onChainProfile = null;
353
+ const owner = this.agent?.owner || this.agent?.ownerAddress;
354
+ if (this.agent && owner) {
355
+ onChainProfile = await this.queryOnChainProfile(owner);
356
+ } else {
357
+ onChainProfile = { status: "sandbox" as const, reason: "No agent owner address provided" };
358
+ }
359
+
360
+ const response = await fetch(`${this.endpoint}/api/tool-call`, {
361
+ method: "POST",
362
+ headers: { "Content-Type": "application/json" },
363
+ body: JSON.stringify({
364
+ runId: this.runId,
365
+ agent: {
366
+ ...this.agent,
367
+ onChainProfile
368
+ },
369
+ tool,
370
+ args
371
+ })
372
+ });
373
+ const event = await response.json();
374
+
375
+ if (event.verdict?.action === "STOP_TOOL") {
376
+ return { ok: false, event, result: null };
377
+ }
378
+
379
+ if (event.verdict?.action === "HUMAN_IN_THE_LOOP") {
380
+ return { ok: false, needsApproval: true, event, result: null };
381
+ }
382
+
383
+ const result = execute ? await execute(args) : { accepted: true };
384
+ return { ok: true, event, result };
385
+ }
386
+ }
387
+
388
+ export function createVerifiedAgent(input: Omit<AgentCourtAgent, "passport"> & { ownerAddress?: string }): AgentCourtAgent {
389
+ return {
390
+ ...input,
391
+ owner: input.owner || input.ownerAddress || `0x${Math.random().toString(16).slice(2).padEnd(40, "0")}`,
392
+ passport: {
393
+ issuer: "agentcourt-arc",
394
+ trustLevel: "verified",
395
+ issuedAt: new Date().toISOString(),
396
+ },
397
+ };
398
+ }
399
+
400
+ export function hashEvidence(value: unknown) {
401
+ return `0x${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
402
+ }
403
+
404
+ function amountFromArgs(args: Record<string, unknown>) {
405
+ const value = args.amountUsd ?? args.notionalUsd ?? 0;
406
+ return typeof value === "number" ? value : Number(value) || 0;
407
+ }
408
+
409
+ function findSensitiveValues(policy: AgentCourtPolicy, value: Record<string, unknown>) {
410
+ const raw = JSON.stringify(value ?? {});
411
+
412
+ return policy.sensitivePatterns
413
+ .map((item) => ({ ...item, regex: new RegExp(item.pattern, "i") }))
414
+ .filter((item) => item.regex.test(raw))
415
+ .map((item) => item.name);
416
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true
12
+ },
13
+ "include": ["src/**/*"]
14
+ }