agent-operator 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/README.md +171 -0
- package/dist/cli/add-wallet.d.ts +1 -0
- package/dist/cli/add-wallet.js +15 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +200 -0
- package/dist/cli/status.d.ts +1 -0
- package/dist/cli/status.js +25 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +57 -0
- package/dist/connectors/base.d.ts +12 -0
- package/dist/connectors/base.js +119 -0
- package/dist/connectors/solana.d.ts +12 -0
- package/dist/connectors/solana.js +91 -0
- package/dist/connectors/tempo.d.ts +12 -0
- package/dist/connectors/tempo.js +102 -0
- package/dist/connectors/types.d.ts +28 -0
- package/dist/connectors/types.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/notifications/engine.d.ts +11 -0
- package/dist/notifications/engine.js +64 -0
- package/dist/notifications/telegram.d.ts +21 -0
- package/dist/notifications/telegram.js +146 -0
- package/dist/notifications/types.d.ts +32 -0
- package/dist/notifications/types.js +1 -0
- package/dist/operator.d.ts +26 -0
- package/dist/operator.js +206 -0
- package/dist/policies/daily-limit.d.ts +1 -0
- package/dist/policies/daily-limit.js +15 -0
- package/dist/policies/engine.d.ts +16 -0
- package/dist/policies/engine.js +24 -0
- package/dist/policies/high-value-gate.d.ts +2 -0
- package/dist/policies/high-value-gate.js +28 -0
- package/dist/policies/low-balance.d.ts +3 -0
- package/dist/policies/low-balance.js +23 -0
- package/dist/policies/new-counterparty.d.ts +2 -0
- package/dist/policies/new-counterparty.js +29 -0
- package/dist/policies/session-cap.d.ts +1 -0
- package/dist/policies/session-cap.js +11 -0
- package/dist/policies/types.d.ts +18 -0
- package/dist/policies/types.js +20 -0
- package/dist/spend/tracker.d.ts +17 -0
- package/dist/spend/tracker.js +101 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# agent-operator
|
|
2
|
+
|
|
3
|
+
Drop-in toolkit for governed, observable AI agent payments.
|
|
4
|
+
|
|
5
|
+
MPP for payments via chain connectors. OWS for authorization + policies. Pluggable notifications for human oversight.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install agent-operator
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# interactive setup — creates agent-operator.json
|
|
17
|
+
npx agent-operator init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { createOperator } from "agent-operator"
|
|
22
|
+
|
|
23
|
+
const operator = await createOperator()
|
|
24
|
+
|
|
25
|
+
await operator.pay("my-agent", {
|
|
26
|
+
service: "https://api.example.com",
|
|
27
|
+
chain: "base",
|
|
28
|
+
maxBudget: 2,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await operator.shutdown()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Config
|
|
35
|
+
|
|
36
|
+
All configuration lives in `agent-operator.json`. The CLI generates it, or create it manually:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"wallets": {
|
|
41
|
+
"my-agent": {
|
|
42
|
+
"walletId": "ows-wallet-id",
|
|
43
|
+
"chains": ["base", "solana", "tempo"],
|
|
44
|
+
"policies": {
|
|
45
|
+
"dailyLimit": 50,
|
|
46
|
+
"sessionCap": 5,
|
|
47
|
+
"newCounterpartyHold": true,
|
|
48
|
+
"lowBalanceThreshold": 10,
|
|
49
|
+
"highValueThreshold": 200
|
|
50
|
+
},
|
|
51
|
+
"notifications": {
|
|
52
|
+
"provider": "telegram",
|
|
53
|
+
"botToken": "YOUR_BOT_TOKEN",
|
|
54
|
+
"chatId": "YOUR_CHAT_ID",
|
|
55
|
+
"alerts": {
|
|
56
|
+
"lowBalance": true,
|
|
57
|
+
"highValueTx": true,
|
|
58
|
+
"newCounterparty": true,
|
|
59
|
+
"policyDenied": true,
|
|
60
|
+
"dailySummary": true,
|
|
61
|
+
"sessionOpened": false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
One agent-operator instance manages multiple wallets. Each wallet has its own chains, policies, and notification provider.
|
|
70
|
+
|
|
71
|
+
## Connectors
|
|
72
|
+
|
|
73
|
+
Each connector wraps MPP for a specific chain. Built-in:
|
|
74
|
+
|
|
75
|
+
- **Tempo** — uses built-in `mppx` session method
|
|
76
|
+
- **Base** — custom MPPX method with OWS EVM signing, USDC balance via RPC
|
|
77
|
+
- **Solana** — wraps `@solana/mpp` with OWS Ed25519 signing, USDC balance via RPC
|
|
78
|
+
|
|
79
|
+
Connectors are resolved automatically from the wallet's `chains` array.
|
|
80
|
+
|
|
81
|
+
## Policies
|
|
82
|
+
|
|
83
|
+
The policy engine runs before every `pay()` call, in order:
|
|
84
|
+
|
|
85
|
+
1. **Session cap** — reject if `maxBudget > sessionCap`
|
|
86
|
+
2. **Daily limit** — reject if today's spend + `maxBudget > dailyLimit`
|
|
87
|
+
3. **High value gate** — if above threshold, request human approval via notification
|
|
88
|
+
4. **New counterparty hold** — if first-time service, request human approval
|
|
89
|
+
5. **Low balance alert** — non-blocking notification if below threshold
|
|
90
|
+
|
|
91
|
+
Spend is tracked locally in `~/.agent-operator/spend.json`.
|
|
92
|
+
|
|
93
|
+
## Notifications
|
|
94
|
+
|
|
95
|
+
Pluggable provider system. Each wallet can use a different provider.
|
|
96
|
+
|
|
97
|
+
### TelegramProvider (built-in)
|
|
98
|
+
|
|
99
|
+
Uses [grammy](https://grammy.dev) with long-polling for approval flows. Included as a dependency — no extra install needed.
|
|
100
|
+
|
|
101
|
+
Approval messages show inline buttons. First response wins. Timeout (default 5 min) = rejected.
|
|
102
|
+
|
|
103
|
+
### Custom Provider
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { NotificationProvider, AgentEvent, ApprovalEvent, ApprovalResult } from "agent-operator"
|
|
107
|
+
|
|
108
|
+
class MyProvider implements NotificationProvider {
|
|
109
|
+
name = "my-platform"
|
|
110
|
+
async start() {}
|
|
111
|
+
async stop() {}
|
|
112
|
+
async send(event: AgentEvent) { /* fire-and-forget alert */ }
|
|
113
|
+
async sendWithApproval(event: ApprovalEvent): Promise<ApprovalResult> {
|
|
114
|
+
const approved = await askUser(event)
|
|
115
|
+
return { approved, approvalId: event.approvalId }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## CLI
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx agent-operator init # interactive setup
|
|
124
|
+
npx agent-operator add-wallet # add wallet to existing config
|
|
125
|
+
npx agent-operator status # show wallets, balances, today's spend
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## API
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const operator = await createOperator() // reads ./agent-operator.json
|
|
132
|
+
const operator = await createOperator(path) // custom config path
|
|
133
|
+
|
|
134
|
+
await operator.pay(walletName, { service, chain, maxBudget })
|
|
135
|
+
await operator.getBalance(walletName, chain)
|
|
136
|
+
await operator.closeSession(walletName, sessionId)
|
|
137
|
+
operator.getWalletNames()
|
|
138
|
+
await operator.shutdown()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## How It Works
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
agent code
|
|
145
|
+
|
|
|
146
|
+
v
|
|
147
|
+
agent-operator SDK (in-process)
|
|
148
|
+
+-- Connectors
|
|
149
|
+
| +-- TempoConnector (mppx session + OWS signing)
|
|
150
|
+
| +-- BaseConnector (custom mppx method + OWS EVM signing)
|
|
151
|
+
| +-- SolanaConnector (@solana/mpp + OWS Ed25519 signing)
|
|
152
|
+
|
|
|
153
|
+
+-- Policy Engine
|
|
154
|
+
| +-- daily-limit, session-cap, high-value-gate
|
|
155
|
+
| +-- new-counterparty-hold, low-balance-alert
|
|
156
|
+
|
|
|
157
|
+
+-- Notification Engine
|
|
158
|
+
+-- TelegramProvider (grammy long-polling)
|
|
159
|
+
+-- [custom providers]
|
|
160
|
+
|
|
161
|
+
No server. No KV. No separate process.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Dependencies
|
|
165
|
+
|
|
166
|
+
| Package | Role |
|
|
167
|
+
|---------|------|
|
|
168
|
+
| `@open-wallet-standard/core` | Wallet management, signing, policies |
|
|
169
|
+
| `mppx` | MPP protocol client (Tempo built-in) |
|
|
170
|
+
| `@solana/mpp` | Solana MPP implementation |
|
|
171
|
+
| `grammy` | Telegram bot for notifications |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function addWallet(): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const CONFIG_FILE = "agent-operator.json";
|
|
4
|
+
export async function addWallet() {
|
|
5
|
+
const configPath = resolve(process.cwd(), CONFIG_FILE);
|
|
6
|
+
if (!existsSync(configPath)) {
|
|
7
|
+
console.log(`No ${CONFIG_FILE} found. Run 'agent-operator init' first.`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
11
|
+
// re-use the init wallet prompt flow
|
|
12
|
+
const { init } = await import("./init.js");
|
|
13
|
+
// the init flow handles adding to existing config
|
|
14
|
+
await init();
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const command = process.argv[2];
|
|
3
|
+
async function main() {
|
|
4
|
+
switch (command) {
|
|
5
|
+
case "init": {
|
|
6
|
+
const { init } = await import("./init.js");
|
|
7
|
+
await init();
|
|
8
|
+
break;
|
|
9
|
+
}
|
|
10
|
+
case "add-wallet": {
|
|
11
|
+
const { addWallet } = await import("./add-wallet.js");
|
|
12
|
+
await addWallet();
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
case "status": {
|
|
16
|
+
const { status } = await import("./status.js");
|
|
17
|
+
await status();
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
default:
|
|
21
|
+
console.log(`agent-operator - governed AI agent payments\n`);
|
|
22
|
+
console.log(`Commands:`);
|
|
23
|
+
console.log(` init Interactive setup — creates agent-operator.json`);
|
|
24
|
+
console.log(` add-wallet Add a wallet to existing config`);
|
|
25
|
+
console.log(` status Show all wallets, balances, today's spend`);
|
|
26
|
+
console.log(`\nUsage: npx agent-operator <command>`);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
main().catch((err) => {
|
|
31
|
+
console.error(err);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function init(): Promise<void>;
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import { writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
const CONFIG_FILE = "agent-operator.json";
|
|
5
|
+
async function promptWallet() {
|
|
6
|
+
const { name } = await prompts({
|
|
7
|
+
type: "text",
|
|
8
|
+
name: "name",
|
|
9
|
+
message: "Wallet name",
|
|
10
|
+
validate: (v) => (v.length > 0 ? true : "Name is required"),
|
|
11
|
+
});
|
|
12
|
+
if (!name)
|
|
13
|
+
return null;
|
|
14
|
+
const { chains } = await prompts({
|
|
15
|
+
type: "multiselect",
|
|
16
|
+
name: "chains",
|
|
17
|
+
message: "Chains (space to select)",
|
|
18
|
+
choices: [
|
|
19
|
+
{ title: "base", value: "base" },
|
|
20
|
+
{ title: "solana", value: "solana" },
|
|
21
|
+
{ title: "tempo", value: "tempo" },
|
|
22
|
+
],
|
|
23
|
+
min: 1,
|
|
24
|
+
});
|
|
25
|
+
if (!chains || chains.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
const policies = await prompts([
|
|
28
|
+
{
|
|
29
|
+
type: "number",
|
|
30
|
+
name: "dailyLimit",
|
|
31
|
+
message: "Daily spend limit ($)",
|
|
32
|
+
initial: 50,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "number",
|
|
36
|
+
name: "sessionCap",
|
|
37
|
+
message: "Session cap ($)",
|
|
38
|
+
initial: 5,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "confirm",
|
|
42
|
+
name: "newCounterpartyHold",
|
|
43
|
+
message: "Hold new counterparties for approval?",
|
|
44
|
+
initial: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "number",
|
|
48
|
+
name: "lowBalanceThreshold",
|
|
49
|
+
message: "Low balance threshold ($)",
|
|
50
|
+
initial: 10,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "number",
|
|
54
|
+
name: "highValueThreshold",
|
|
55
|
+
message: "High value threshold ($)",
|
|
56
|
+
initial: 200,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
const { provider } = await prompts({
|
|
60
|
+
type: "select",
|
|
61
|
+
name: "provider",
|
|
62
|
+
message: "Notification provider",
|
|
63
|
+
choices: [
|
|
64
|
+
{ title: "Telegram", value: "telegram" },
|
|
65
|
+
{ title: "Webhook", value: "webhook" },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
if (provider === undefined)
|
|
69
|
+
return null;
|
|
70
|
+
let notifications;
|
|
71
|
+
if (provider === "telegram") {
|
|
72
|
+
const tg = await prompts([
|
|
73
|
+
{
|
|
74
|
+
type: "password",
|
|
75
|
+
name: "botToken",
|
|
76
|
+
message: "Telegram bot token",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
name: "chatId",
|
|
81
|
+
message: "Telegram chat ID",
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
notifications = {
|
|
85
|
+
provider: "telegram",
|
|
86
|
+
botToken: tg.botToken,
|
|
87
|
+
chatId: tg.chatId,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const wh = await prompts([
|
|
92
|
+
{
|
|
93
|
+
type: "text",
|
|
94
|
+
name: "url",
|
|
95
|
+
message: "Webhook URL",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "password",
|
|
99
|
+
name: "secret",
|
|
100
|
+
message: "Webhook secret (optional)",
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
notifications = {
|
|
104
|
+
provider: "webhook",
|
|
105
|
+
url: wh.url,
|
|
106
|
+
secret: wh.secret || undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// try to create/find OWS wallet
|
|
110
|
+
let walletId = "";
|
|
111
|
+
try {
|
|
112
|
+
const ows = await import("@open-wallet-standard/core");
|
|
113
|
+
console.log(`\nChecking for OWS wallet "${name}"...`);
|
|
114
|
+
try {
|
|
115
|
+
const existing = ows.getWallet(name);
|
|
116
|
+
walletId = existing.id;
|
|
117
|
+
console.log(` → Found existing wallet: ${walletId}`);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
console.log(` → Not found. Creating wallet via OWS...`);
|
|
121
|
+
const created = ows.createWallet(name);
|
|
122
|
+
walletId = created.id;
|
|
123
|
+
console.log(` ✅ Wallet created: ${name} (id: ${walletId})`);
|
|
124
|
+
// generate API key
|
|
125
|
+
try {
|
|
126
|
+
const apiKey = ows.createApiKey(name, [walletId], [], "");
|
|
127
|
+
console.log(` ✅ API key generated: ${apiKey.id}`);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
console.log(` ⚠️ Could not generate API key — set up manually`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
console.log(`\n⚠️ OWS not available. Enter wallet ID manually.`);
|
|
136
|
+
const { id } = await prompts({
|
|
137
|
+
type: "text",
|
|
138
|
+
name: "id",
|
|
139
|
+
message: "OWS wallet ID",
|
|
140
|
+
});
|
|
141
|
+
walletId = id || "PLACEHOLDER";
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
name,
|
|
145
|
+
wallet: {
|
|
146
|
+
walletId,
|
|
147
|
+
chains,
|
|
148
|
+
policies: {
|
|
149
|
+
dailyLimit: policies.dailyLimit,
|
|
150
|
+
sessionCap: policies.sessionCap,
|
|
151
|
+
newCounterpartyHold: policies.newCounterpartyHold,
|
|
152
|
+
lowBalanceThreshold: policies.lowBalanceThreshold,
|
|
153
|
+
highValueThreshold: policies.highValueThreshold,
|
|
154
|
+
},
|
|
155
|
+
notifications,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export async function init() {
|
|
160
|
+
console.log("\nWelcome to agent-operator setup.\n");
|
|
161
|
+
const configPath = resolve(process.cwd(), CONFIG_FILE);
|
|
162
|
+
let config = { wallets: {} };
|
|
163
|
+
if (existsSync(configPath)) {
|
|
164
|
+
const { action } = await prompts({
|
|
165
|
+
type: "select",
|
|
166
|
+
name: "action",
|
|
167
|
+
message: `${CONFIG_FILE} already exists`,
|
|
168
|
+
choices: [
|
|
169
|
+
{ title: "Overwrite", value: "overwrite" },
|
|
170
|
+
{ title: "Add wallet to existing", value: "add" },
|
|
171
|
+
{ title: "Cancel", value: "cancel" },
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
if (action === "cancel")
|
|
175
|
+
return;
|
|
176
|
+
if (action === "add") {
|
|
177
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
let addMore = true;
|
|
181
|
+
while (addMore) {
|
|
182
|
+
const result = await promptWallet();
|
|
183
|
+
if (!result)
|
|
184
|
+
break;
|
|
185
|
+
config.wallets[result.name] = result.wallet;
|
|
186
|
+
const { more } = await prompts({
|
|
187
|
+
type: "confirm",
|
|
188
|
+
name: "more",
|
|
189
|
+
message: "Add another wallet?",
|
|
190
|
+
initial: false,
|
|
191
|
+
});
|
|
192
|
+
addMore = more;
|
|
193
|
+
}
|
|
194
|
+
if (Object.keys(config.wallets).length === 0) {
|
|
195
|
+
console.log("\nNo wallets configured. Exiting.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
199
|
+
console.log(`\n✅ Config written: ${configPath}`);
|
|
200
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function status(): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { getSpendSummary, getCounterparties } from "../spend/tracker.js";
|
|
4
|
+
const CONFIG_FILE = "agent-operator.json";
|
|
5
|
+
export async function status() {
|
|
6
|
+
const configPath = resolve(process.cwd(), CONFIG_FILE);
|
|
7
|
+
if (!existsSync(configPath)) {
|
|
8
|
+
console.log(`No ${CONFIG_FILE} found. Run 'agent-operator init' first.`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
12
|
+
console.log("\n📊 agent-operator status\n");
|
|
13
|
+
for (const [name, wallet] of Object.entries(config.wallets)) {
|
|
14
|
+
const summary = getSpendSummary(name);
|
|
15
|
+
const counterparties = getCounterparties(name);
|
|
16
|
+
console.log(` ${name}`);
|
|
17
|
+
console.log(` ├── wallet: ${wallet.walletId}`);
|
|
18
|
+
console.log(` ├── chains: ${wallet.chains.join(", ")}`);
|
|
19
|
+
console.log(` ├── spent today: $${summary.spent.toFixed(2)}${wallet.policies.dailyLimit ? ` / $${wallet.policies.dailyLimit}` : ""}`);
|
|
20
|
+
console.log(` ├── sessions today: ${summary.sessionCount}`);
|
|
21
|
+
console.log(` ├── known counterparties: ${counterparties.length > 0 ? counterparties.join(", ") : "none"}`);
|
|
22
|
+
console.log(` └── notification: ${wallet.notifications.provider}`);
|
|
23
|
+
console.log();
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PoliciesConfig } from "./policies/types.js";
|
|
2
|
+
import type { AlertsConfig } from "./notifications/types.js";
|
|
3
|
+
export interface WalletConfig {
|
|
4
|
+
walletId: string;
|
|
5
|
+
chains: string[];
|
|
6
|
+
policies: PoliciesConfig;
|
|
7
|
+
notifications: NotificationConfig;
|
|
8
|
+
}
|
|
9
|
+
export interface NotificationConfig {
|
|
10
|
+
provider: string;
|
|
11
|
+
alerts?: AlertsConfig;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface OperatorConfig {
|
|
15
|
+
wallets: Record<string, WalletConfig>;
|
|
16
|
+
connectors?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export declare function loadConfig(configPath?: string): OperatorConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const DEFAULT_ALERTS = {
|
|
4
|
+
lowBalance: true,
|
|
5
|
+
highValueTx: true,
|
|
6
|
+
newCounterparty: true,
|
|
7
|
+
policyDenied: true,
|
|
8
|
+
dailySummary: true,
|
|
9
|
+
sessionOpened: false,
|
|
10
|
+
};
|
|
11
|
+
export function loadConfig(configPath) {
|
|
12
|
+
const filePath = configPath ?? resolve(process.cwd(), "agent-operator.json");
|
|
13
|
+
if (!existsSync(filePath)) {
|
|
14
|
+
throw new Error(`Config not found at ${filePath}. Run 'npx agent-operator init' to create one.`);
|
|
15
|
+
}
|
|
16
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
17
|
+
let config;
|
|
18
|
+
try {
|
|
19
|
+
config = JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`Invalid JSON in ${filePath}`);
|
|
23
|
+
}
|
|
24
|
+
validateConfig(config);
|
|
25
|
+
applyDefaults(config);
|
|
26
|
+
return config;
|
|
27
|
+
}
|
|
28
|
+
function validateConfig(config) {
|
|
29
|
+
if (!config.wallets || typeof config.wallets !== "object") {
|
|
30
|
+
throw new Error("Config must have a 'wallets' object");
|
|
31
|
+
}
|
|
32
|
+
const validChains = ["base", "solana", "tempo"];
|
|
33
|
+
for (const [name, wallet] of Object.entries(config.wallets)) {
|
|
34
|
+
if (!wallet.walletId) {
|
|
35
|
+
throw new Error(`Wallet '${name}' is missing 'walletId'`);
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(wallet.chains) || wallet.chains.length === 0) {
|
|
38
|
+
throw new Error(`Wallet '${name}' must have at least one chain`);
|
|
39
|
+
}
|
|
40
|
+
for (const chain of wallet.chains) {
|
|
41
|
+
if (!validChains.includes(chain)) {
|
|
42
|
+
throw new Error(`Wallet '${name}' has unknown chain '${chain}'. Valid: ${validChains.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!wallet.notifications?.provider) {
|
|
46
|
+
throw new Error(`Wallet '${name}' is missing notification provider`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function applyDefaults(config) {
|
|
51
|
+
for (const wallet of Object.values(config.wallets)) {
|
|
52
|
+
wallet.notifications.alerts = {
|
|
53
|
+
...DEFAULT_ALERTS,
|
|
54
|
+
...wallet.notifications.alerts,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Connector, Session, SessionResult, Balance } from "./types.js";
|
|
2
|
+
export declare class BaseConnector implements Connector {
|
|
3
|
+
chain: string;
|
|
4
|
+
private sessions;
|
|
5
|
+
openSession(params: {
|
|
6
|
+
service: string;
|
|
7
|
+
maxBudget: number;
|
|
8
|
+
walletId: string;
|
|
9
|
+
}): Promise<Session>;
|
|
10
|
+
closeSession(sessionId: string): Promise<SessionResult>;
|
|
11
|
+
getBalance(walletId: string): Promise<Balance>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Mppx } from "mppx/client";
|
|
2
|
+
import { Method, Credential } from "mppx";
|
|
3
|
+
import { signTransaction, getWallet } from "@open-wallet-standard/core";
|
|
4
|
+
// Base chain ID for EIP-155
|
|
5
|
+
const BASE_CHAIN_ID = "eip155:8453";
|
|
6
|
+
export class BaseConnector {
|
|
7
|
+
chain = "base";
|
|
8
|
+
sessions = new Map();
|
|
9
|
+
async openSession(params) {
|
|
10
|
+
const { service, maxBudget, walletId } = params;
|
|
11
|
+
const wallet = getWallet(walletId);
|
|
12
|
+
const baseAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
|
|
13
|
+
if (!baseAccount) {
|
|
14
|
+
throw new Error(`Wallet '${walletId}' has no EVM account for Base`);
|
|
15
|
+
}
|
|
16
|
+
// define a custom charge method for Base
|
|
17
|
+
const baseChargeMethod = Method.from({
|
|
18
|
+
name: "base",
|
|
19
|
+
intent: "charge",
|
|
20
|
+
schema: {
|
|
21
|
+
credential: {
|
|
22
|
+
payload: {
|
|
23
|
+
"~standard": { version: 1 },
|
|
24
|
+
parse: (v) => v,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
request: {
|
|
28
|
+
"~standard": { version: 1 },
|
|
29
|
+
parse: (v) => v,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const clientMethod = Method.toClient(baseChargeMethod, {
|
|
34
|
+
async createCredential({ challenge }) {
|
|
35
|
+
// sign the payment via OWS (policies enforced here)
|
|
36
|
+
const txData = JSON.stringify({
|
|
37
|
+
challengeId: challenge.id,
|
|
38
|
+
from: baseAccount.address,
|
|
39
|
+
amount: challenge.request?.amount,
|
|
40
|
+
chain: "base",
|
|
41
|
+
});
|
|
42
|
+
const result = signTransaction(walletId, BASE_CHAIN_ID, txData);
|
|
43
|
+
return Credential.serialize({
|
|
44
|
+
challenge,
|
|
45
|
+
payload: { signature: result.signature },
|
|
46
|
+
source: `did:pkh:eip155:8453:${baseAccount.address}`,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const mppxClient = Mppx.create({
|
|
51
|
+
methods: [clientMethod],
|
|
52
|
+
});
|
|
53
|
+
// initiate the 402 flow
|
|
54
|
+
const response = await mppxClient.fetch(service, {
|
|
55
|
+
headers: { "X-Max-Budget": String(maxBudget) },
|
|
56
|
+
});
|
|
57
|
+
const sessionId = crypto.randomUUID();
|
|
58
|
+
this.sessions.set(sessionId, {
|
|
59
|
+
sessionId,
|
|
60
|
+
service,
|
|
61
|
+
maxBudget,
|
|
62
|
+
spent: 0,
|
|
63
|
+
mppxClient,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
sessionId,
|
|
67
|
+
chain: this.chain,
|
|
68
|
+
service,
|
|
69
|
+
maxBudget,
|
|
70
|
+
spent: 0,
|
|
71
|
+
status: "active",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async closeSession(sessionId) {
|
|
75
|
+
const session = this.sessions.get(sessionId);
|
|
76
|
+
if (!session) {
|
|
77
|
+
throw new Error(`Session '${sessionId}' not found`);
|
|
78
|
+
}
|
|
79
|
+
this.sessions.delete(sessionId);
|
|
80
|
+
return {
|
|
81
|
+
sessionId,
|
|
82
|
+
totalSpent: session.spent,
|
|
83
|
+
refund: session.maxBudget - session.spent,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async getBalance(walletId) {
|
|
87
|
+
const wallet = getWallet(walletId);
|
|
88
|
+
const baseAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155"));
|
|
89
|
+
if (!baseAccount) {
|
|
90
|
+
return { chain: this.chain, amount: 0, currency: "USDC" };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const { createPublicClient, http, parseAbi, formatUnits } = await import("viem");
|
|
94
|
+
const { base } = await import("viem/chains");
|
|
95
|
+
const client = createPublicClient({
|
|
96
|
+
chain: base,
|
|
97
|
+
transport: http(),
|
|
98
|
+
});
|
|
99
|
+
// USDC on Base
|
|
100
|
+
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
101
|
+
const balance = await client.readContract({
|
|
102
|
+
address: USDC_ADDRESS,
|
|
103
|
+
abi: parseAbi([
|
|
104
|
+
"function balanceOf(address) view returns (uint256)",
|
|
105
|
+
]),
|
|
106
|
+
functionName: "balanceOf",
|
|
107
|
+
args: [baseAccount.address],
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
chain: this.chain,
|
|
111
|
+
amount: Number(formatUnits(balance, 6)),
|
|
112
|
+
currency: "USDC",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { chain: this.chain, amount: 0, currency: "USDC" };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Connector, Session, SessionResult, Balance } from "./types.js";
|
|
2
|
+
export declare class SolanaConnector implements Connector {
|
|
3
|
+
chain: string;
|
|
4
|
+
private sessions;
|
|
5
|
+
openSession(params: {
|
|
6
|
+
service: string;
|
|
7
|
+
maxBudget: number;
|
|
8
|
+
walletId: string;
|
|
9
|
+
}): Promise<Session>;
|
|
10
|
+
closeSession(sessionId: string): Promise<SessionResult>;
|
|
11
|
+
getBalance(walletId: string): Promise<Balance>;
|
|
12
|
+
}
|