triage-ows 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.
Files changed (91) hide show
  1. package/README.md +786 -0
  2. package/bin/triage-ows.js +218 -0
  3. package/bin/triage-policy.js +66 -0
  4. package/bin/triage-server.js +4 -0
  5. package/dashboard-dist/assets/index-Dnhi_dJQ.css +2 -0
  6. package/dashboard-dist/assets/index-g_2MwC-o.js +9 -0
  7. package/dashboard-dist/favicon.svg +7 -0
  8. package/dashboard-dist/index.html +16 -0
  9. package/dist/config.d.ts +32 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +61 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/emitter.d.ts +7 -0
  14. package/dist/emitter.d.ts.map +1 -0
  15. package/dist/emitter.js +41 -0
  16. package/dist/emitter.js.map +1 -0
  17. package/dist/index.d.ts +17 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +55 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/scoring/behavior.d.ts +3 -0
  22. package/dist/scoring/behavior.d.ts.map +1 -0
  23. package/dist/scoring/behavior.js +13 -0
  24. package/dist/scoring/behavior.js.map +1 -0
  25. package/dist/scoring/compliance.d.ts +3 -0
  26. package/dist/scoring/compliance.d.ts.map +1 -0
  27. package/dist/scoring/compliance.js +11 -0
  28. package/dist/scoring/compliance.js.map +1 -0
  29. package/dist/scoring/identity.d.ts +3 -0
  30. package/dist/scoring/identity.d.ts.map +1 -0
  31. package/dist/scoring/identity.js +16 -0
  32. package/dist/scoring/identity.js.map +1 -0
  33. package/dist/scoring/index.d.ts +10 -0
  34. package/dist/scoring/index.d.ts.map +1 -0
  35. package/dist/scoring/index.js +35 -0
  36. package/dist/scoring/index.js.map +1 -0
  37. package/dist/scoring/limits.d.ts +7 -0
  38. package/dist/scoring/limits.d.ts.map +1 -0
  39. package/dist/scoring/limits.js +9 -0
  40. package/dist/scoring/limits.js.map +1 -0
  41. package/dist/scoring/network.d.ts +3 -0
  42. package/dist/scoring/network.d.ts.map +1 -0
  43. package/dist/scoring/network.js +16 -0
  44. package/dist/scoring/network.js.map +1 -0
  45. package/dist/scoring/onchain.d.ts +4 -0
  46. package/dist/scoring/onchain.d.ts.map +1 -0
  47. package/dist/scoring/onchain.js +35 -0
  48. package/dist/scoring/onchain.js.map +1 -0
  49. package/dist/scoring/risk.d.ts +3 -0
  50. package/dist/scoring/risk.d.ts.map +1 -0
  51. package/dist/scoring/risk.js +22 -0
  52. package/dist/scoring/risk.js.map +1 -0
  53. package/dist/server.d.ts +6 -0
  54. package/dist/server.d.ts.map +1 -0
  55. package/dist/server.js +405 -0
  56. package/dist/server.js.map +1 -0
  57. package/dist/store.d.ts +13 -0
  58. package/dist/store.d.ts.map +1 -0
  59. package/dist/store.js +177 -0
  60. package/dist/store.js.map +1 -0
  61. package/dist/types.d.ts +107 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +26 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/webbotauth.d.ts +38 -0
  66. package/dist/webbotauth.d.ts.map +1 -0
  67. package/dist/webbotauth.js +120 -0
  68. package/dist/webbotauth.js.map +1 -0
  69. package/dist/xmtp.d.ts +6 -0
  70. package/dist/xmtp.d.ts.map +1 -0
  71. package/dist/xmtp.js +161 -0
  72. package/dist/xmtp.js.map +1 -0
  73. package/package.json +58 -0
  74. package/policy-template.json +14 -0
  75. package/src/config.ts +86 -0
  76. package/src/emitter.ts +40 -0
  77. package/src/index.ts +18 -0
  78. package/src/scoring/behavior.ts +15 -0
  79. package/src/scoring/compliance.ts +12 -0
  80. package/src/scoring/identity.ts +12 -0
  81. package/src/scoring/index.ts +31 -0
  82. package/src/scoring/limits.ts +10 -0
  83. package/src/scoring/network.ts +18 -0
  84. package/src/scoring/onchain.ts +44 -0
  85. package/src/scoring/risk.ts +25 -0
  86. package/src/server.ts +410 -0
  87. package/src/store.ts +197 -0
  88. package/src/types.ts +137 -0
  89. package/src/webbotauth.ts +128 -0
  90. package/src/xmtp.ts +188 -0
  91. package/triage.config.example.json +22 -0
package/src/store.ts ADDED
@@ -0,0 +1,197 @@
1
+ import type { AgentProfile, TrustBreakdown, PolicyContext } from './types'
2
+ import { computeTrustScore } from './scoring/index'
3
+ import { getSpendingLimits } from './scoring/limits'
4
+ import { refreshOnChainData } from './scoring/onchain'
5
+ import { getConfig } from './config'
6
+
7
+ const agents = new Map<string, AgentProfile>()
8
+
9
+ // --- Helpers ---
10
+
11
+ function todayString(): string {
12
+ return new Date().toISOString().split('T')[0]
13
+ }
14
+
15
+ function yesterdayString(): string {
16
+ const d = new Date()
17
+ d.setDate(d.getDate() - 1)
18
+ return d.toISOString().split('T')[0]
19
+ }
20
+
21
+ function defaultBreakdown(): TrustBreakdown {
22
+ return { identity: 4, onChain: 0, behavior: 0, compliance: 0, network: 0, risk: 0, total: 4 }
23
+ }
24
+
25
+ function recalc(agent: AgentProfile): void {
26
+ const breakdown = computeTrustScore(agent, getAgent)
27
+ agent.breakdown = breakdown
28
+ agent.trustScore = breakdown.total
29
+ agent.tier = getSpendingLimits(breakdown.total).tier.name
30
+ }
31
+
32
+ // --- Core Store Functions ---
33
+
34
+ export function getOrCreateAgent(address: string): AgentProfile {
35
+ if (agents.has(address)) return agents.get(address)!
36
+
37
+ const today = todayString()
38
+ const now = Date.now()
39
+
40
+ const profile: AgentProfile = {
41
+ address,
42
+ isOWSWallet: false,
43
+ worldIdVerified: false,
44
+ webBotAuthVerified: false,
45
+ trustScore: 4,
46
+ tier: 'Restricted',
47
+ breakdown: defaultBreakdown(),
48
+ totalRequests: 0,
49
+ successfulRequests: 0,
50
+ failedRequests: 0,
51
+ dailySpent: 0,
52
+ dailyDate: today,
53
+ consecutiveApprovals: 0,
54
+ consecutiveDenials: 0,
55
+ totalApproved: 0,
56
+ totalDenied: 0,
57
+ humanOverrides: 0,
58
+ counterparties: [],
59
+ requestTimestamps: [],
60
+ lastActive: now,
61
+ consecutiveCleanDays: 0,
62
+ cleanDayDate: today,
63
+ createdAt: now,
64
+ }
65
+
66
+ agents.set(address, profile)
67
+ refreshOnChainData(address).catch(() => {})
68
+ return profile
69
+ }
70
+
71
+ export function getAgent(address: string): AgentProfile | undefined {
72
+ return agents.get(address)
73
+ }
74
+
75
+ export function getAllAgents(): AgentProfile[] {
76
+ return Array.from(agents.values())
77
+ }
78
+
79
+ export function getTopAgents(n: number): AgentProfile[] {
80
+ return getAllAgents().sort((a, b) => b.trustScore - a.trustScore).slice(0, n)
81
+ }
82
+
83
+ export function recordApproval(address: string, txTo: string, amount: number): AgentProfile {
84
+ const agent = getOrCreateAgent(address)
85
+ const now = Date.now()
86
+ const today = todayString()
87
+
88
+ agent.totalRequests++
89
+ agent.successfulRequests++
90
+ agent.totalApproved++
91
+ agent.consecutiveApprovals++
92
+ agent.consecutiveDenials = 0
93
+
94
+ agent.requestTimestamps.push(now)
95
+ if (agent.requestTimestamps.length > 100) agent.requestTimestamps = agent.requestTimestamps.slice(-100)
96
+
97
+ if (!agent.counterparties.includes(txTo)) agent.counterparties.push(txTo)
98
+
99
+ if (agent.dailyDate !== today) {
100
+ agent.dailySpent = 0
101
+ agent.dailyDate = today
102
+ }
103
+ agent.dailySpent += amount
104
+ agent.lastActive = now
105
+
106
+ if (agent.cleanDayDate === today) {
107
+ // already counted today
108
+ } else if (agent.cleanDayDate === yesterdayString()) {
109
+ agent.consecutiveCleanDays++
110
+ } else {
111
+ agent.consecutiveCleanDays = 1
112
+ }
113
+ agent.cleanDayDate = today
114
+
115
+ recalc(agent)
116
+ refreshOnChainData(agent.address).catch(() => {})
117
+ return agent
118
+ }
119
+
120
+ export function recordDenial(
121
+ address: string,
122
+ tx?: PolicyContext['transaction'],
123
+ amount?: number
124
+ ): AgentProfile {
125
+ const agent = getOrCreateAgent(address)
126
+ const now = Date.now()
127
+
128
+ agent.totalRequests++
129
+ agent.failedRequests++
130
+ agent.totalDenied++
131
+ agent.consecutiveDenials++
132
+ agent.consecutiveApprovals = 0
133
+
134
+ agent.requestTimestamps.push(now)
135
+ if (agent.requestTimestamps.length > 100) agent.requestTimestamps = agent.requestTimestamps.slice(-100)
136
+
137
+ agent.lastActive = now
138
+
139
+ if (tx && amount !== undefined) {
140
+ agent.pendingOverride = { tx, amount, createdAt: Date.now() }
141
+ }
142
+
143
+ recalc(agent)
144
+ return agent
145
+ }
146
+
147
+ export function recordOverride(address: string): AgentProfile | undefined {
148
+ const agent = getAgent(address)
149
+ if (!agent || !agent.pendingOverride) return undefined
150
+
151
+ const OVERRIDE_TTL = 5 * 60 * 1000 // 5 minutes
152
+ if (Date.now() - (agent.pendingOverride.createdAt || 0) > OVERRIDE_TTL) {
153
+ agent.pendingOverride = undefined
154
+ return undefined // expired
155
+ }
156
+
157
+ agent.humanOverrides++
158
+ agent.pendingOverride = undefined
159
+
160
+ agent.totalApproved++
161
+ agent.consecutiveApprovals++
162
+ agent.consecutiveDenials = 0
163
+
164
+ recalc(agent)
165
+ const boost = getConfig().scoring?.overrideBoost ?? 3
166
+ agent.trustScore = Math.min(100, agent.trustScore + boost)
167
+ agent.tier = getSpendingLimits(agent.trustScore).tier.name
168
+ return agent
169
+ }
170
+
171
+ export function addVerifiedHuman(address: string, nullifierHash: string): AgentProfile {
172
+ const agent = getOrCreateAgent(address)
173
+ agent.worldIdVerified = true
174
+ agent.nullifierHash = nullifierHash
175
+ recalc(agent)
176
+ return agent
177
+ }
178
+
179
+ export function isVerifiedHuman(address: string): boolean {
180
+ return agents.get(address)?.worldIdVerified ?? false
181
+ }
182
+
183
+ export function setWebBotAuthVerified(address: string): AgentProfile {
184
+ const agent = getOrCreateAgent(address)
185
+ agent.webBotAuthVerified = true
186
+ recalc(agent)
187
+ return agent
188
+ }
189
+
190
+ export function setOWSWallet(address: string, walletId: string, apiKeyId: string): AgentProfile {
191
+ const agent = getOrCreateAgent(address)
192
+ agent.isOWSWallet = true
193
+ agent.walletId = walletId
194
+ agent.apiKeyId = apiKeyId
195
+ recalc(agent)
196
+ return agent
197
+ }
package/src/types.ts ADDED
@@ -0,0 +1,137 @@
1
+ // --- OWS Policy Types ---
2
+
3
+ export interface PolicyContext {
4
+ chain_id: string
5
+ wallet_id: string
6
+ api_key_id: string
7
+ transaction: {
8
+ to: string
9
+ value: string
10
+ raw_hex: string
11
+ data: string
12
+ }
13
+ spending: {
14
+ daily_total: string
15
+ date: string
16
+ }
17
+ timestamp: string
18
+ policy_config?: Record<string, unknown>
19
+ }
20
+
21
+ export interface PolicyResult {
22
+ allow: boolean
23
+ reason?: string
24
+ }
25
+
26
+ // --- Scoring Types ---
27
+
28
+ export interface TrustBreakdown {
29
+ identity: number
30
+ onChain: number
31
+ behavior: number
32
+ compliance: number
33
+ network: number
34
+ risk: number
35
+ total: number
36
+ }
37
+
38
+ export interface SpendingTier {
39
+ name: 'Sovereign' | 'Trusted' | 'Building' | 'Cautious' | 'Restricted' | 'Frozen'
40
+ color: string
41
+ dailyLimit: number
42
+ perTxLimit: number
43
+ minScore: number
44
+ }
45
+
46
+ // --- Agent Profile ---
47
+
48
+ export interface AgentProfile {
49
+ address: string
50
+ walletId?: string
51
+ apiKeyId?: string
52
+ chainId?: string
53
+ isOWSWallet: boolean
54
+ worldIdVerified: boolean
55
+ webBotAuthVerified: boolean
56
+ nullifierHash?: string
57
+ trustScore: number
58
+ breakdown: TrustBreakdown
59
+ tier: string
60
+ totalRequests: number
61
+ successfulRequests: number
62
+ failedRequests: number
63
+ dailySpent: number
64
+ dailyDate: string
65
+ consecutiveApprovals: number
66
+ consecutiveDenials: number
67
+ totalApproved: number
68
+ totalDenied: number
69
+ humanOverrides: number
70
+ counterparties: string[]
71
+ pendingOverride?: { tx: PolicyContext['transaction']; amount: number; createdAt: number }
72
+ lastOnChainRefresh?: number
73
+ requestTimestamps: number[]
74
+ lastActive: number
75
+ consecutiveCleanDays: number
76
+ cleanDayDate: string
77
+ createdAt: number
78
+ }
79
+
80
+ // --- WebSocket Event Types ---
81
+
82
+ export interface PolicyDecisionEvent {
83
+ type: 'POLICY_DECISION'
84
+ agent: string
85
+ amount: number
86
+ trustScore: number
87
+ tier: string
88
+ decision: 'APPROVE' | 'DENY' | 'OVERRIDE'
89
+ reason: string
90
+ dailyLimit: number
91
+ dailySpent: number
92
+ timestamp: number
93
+ }
94
+
95
+ export interface TrustChangeEvent {
96
+ type: 'TRUST_CHANGE'
97
+ agent: string
98
+ oldScore: number
99
+ newScore: number
100
+ oldTier: string
101
+ newTier: string
102
+ reason: string
103
+ timestamp: number
104
+ }
105
+
106
+ export interface BudgetWarningEvent {
107
+ type: 'BUDGET_WARNING'
108
+ agent: string
109
+ spent: number
110
+ limit: number
111
+ percentage: number
112
+ timestamp: number
113
+ }
114
+
115
+ export type TriageEvent = PolicyDecisionEvent | TrustChangeEvent | BudgetWarningEvent
116
+
117
+ // --- Spending Tiers ---
118
+
119
+ export let SPENDING_TIERS: SpendingTier[] = [
120
+ { name: 'Sovereign', color: '#fff8e1', dailyLimit: 1000, perTxLimit: 500, minScore: 80 },
121
+ { name: 'Trusted', color: '#ffd700', dailyLimit: 200, perTxLimit: 100, minScore: 60 },
122
+ { name: 'Building', color: '#4caf50', dailyLimit: 50, perTxLimit: 25, minScore: 40 },
123
+ { name: 'Cautious', color: '#00bcd4', dailyLimit: 10, perTxLimit: 5, minScore: 20 },
124
+ { name: 'Restricted', color: '#2196f3', dailyLimit: 2, perTxLimit: 1, minScore: 1 },
125
+ { name: 'Frozen', color: '#1a1a4e', dailyLimit: 0, perTxLimit: 0, minScore: 0 },
126
+ ]
127
+
128
+ export function updateSpendingTiers(tiers: SpendingTier[]): void {
129
+ SPENDING_TIERS = tiers
130
+ }
131
+
132
+ export function getTierForScore(score: number): SpendingTier {
133
+ for (const tier of SPENDING_TIERS) {
134
+ if (score >= tier.minScore) return tier
135
+ }
136
+ return SPENDING_TIERS[SPENDING_TIERS.length - 1]
137
+ }
@@ -0,0 +1,128 @@
1
+ import crypto from 'crypto'
2
+
3
+ export interface SignatureComponents {
4
+ signatureInput: string
5
+ signature: string
6
+ }
7
+
8
+ /**
9
+ * Parse RFC 9421 Signature-Input header.
10
+ * Format: sig1=("@method" "@authority" "@path"); keyid="agent-key"; alg="ed25519"; created=1234567890
11
+ */
12
+ export function parseSignatureInput(input: string): {
13
+ label: string
14
+ components: string[]
15
+ params: Record<string, string>
16
+ } | null {
17
+ try {
18
+ const match = input.match(/^(\w+)=\(([^)]*)\);\s*(.+)$/)
19
+ if (!match) return null
20
+ const [, label, componentStr, paramStr] = match
21
+ const components = componentStr.match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) ?? []
22
+ const params: Record<string, string> = {}
23
+ for (const part of paramStr.split(';')) {
24
+ const [k, v] = part.trim().split('=')
25
+ if (k && v) params[k.trim()] = v.replace(/"/g, '').trim()
26
+ }
27
+ return { label, components, params }
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Build the RFC 9421 signature base string from request components.
35
+ */
36
+ export function buildSignatureBase(
37
+ components: string[],
38
+ request: { method: string; url: string; headers: Record<string, string> },
39
+ signatureInputValue: string,
40
+ label: string
41
+ ): string {
42
+ const lines: string[] = []
43
+ const url = new URL(request.url, 'http://localhost')
44
+ for (const comp of components) {
45
+ if (comp === '@method') lines.push(`"@method": ${request.method.toUpperCase()}`)
46
+ else if (comp === '@authority') lines.push(`"@authority": ${url.host}`)
47
+ else if (comp === '@path') lines.push(`"@path": ${url.pathname}`)
48
+ else if (comp === '@query') lines.push(`"@query": ?${url.search.slice(1)}`)
49
+ else if (comp.startsWith('@')) lines.push(`"${comp}": `)
50
+ else lines.push(`"${comp}": ${request.headers[comp.toLowerCase()] ?? ''}`)
51
+ }
52
+ lines.push(`"@signature-params": ${signatureInputValue.split('=').slice(1).join('=')}`)
53
+ return lines.join('\n')
54
+ }
55
+
56
+ /**
57
+ * Verify an Ed25519 signature.
58
+ * Returns true if the signature is valid for the given public key and data.
59
+ */
60
+ export function verifyEd25519Signature(
61
+ publicKeyBase64: string,
62
+ signatureBase64: string,
63
+ data: string
64
+ ): boolean {
65
+ try {
66
+ const pubKeyBuffer = Buffer.from(publicKeyBase64, 'base64')
67
+ const sigBuffer = Buffer.from(signatureBase64, 'base64')
68
+ const key = crypto.createPublicKey({
69
+ key: Buffer.concat([
70
+ // Ed25519 DER prefix
71
+ Buffer.from('302a300506032b6570032100', 'hex'),
72
+ pubKeyBuffer
73
+ ]),
74
+ format: 'der',
75
+ type: 'spki'
76
+ })
77
+ return crypto.verify(null, Buffer.from(data), key, sigBuffer)
78
+ } catch {
79
+ return false
80
+ }
81
+ }
82
+
83
+ // Public key cache: agentKey -> base64 public key
84
+ const keyCache = new Map<string, string>()
85
+
86
+ export function registerAgentPublicKey(agentKey: string, publicKeyBase64: string): void {
87
+ keyCache.set(agentKey, publicKeyBase64)
88
+ }
89
+
90
+ export function getAgentPublicKey(agentKey: string): string | undefined {
91
+ return keyCache.get(agentKey)
92
+ }
93
+
94
+ /**
95
+ * Full Web Bot Auth verification.
96
+ * Checks Signature-Input + Signature headers against agent's registered public key.
97
+ */
98
+ export function verifyWebBotAuth(
99
+ headers: Record<string, string>,
100
+ request: { method: string; url: string; headers: Record<string, string> },
101
+ agentKey: string
102
+ ): boolean {
103
+ const signatureInput = headers['signature-input']
104
+ const signature = headers['signature']
105
+ if (!signatureInput || !signature) return false
106
+
107
+ const parsed = parseSignatureInput(signatureInput)
108
+ if (!parsed) return false
109
+
110
+ // Check algorithm is ed25519
111
+ if (parsed.params.alg && parsed.params.alg !== 'ed25519') return false
112
+
113
+ // Check expiry
114
+ if (parsed.params.expires && Number(parsed.params.expires) < Date.now() / 1000) return false
115
+
116
+ // Get public key
117
+ const pubKey = getAgentPublicKey(agentKey) || getAgentPublicKey(parsed.params.keyid)
118
+ if (!pubKey) return false
119
+
120
+ // Build signature base and verify
121
+ const base = buildSignatureBase(parsed.components, request, signatureInput, parsed.label)
122
+
123
+ // Extract signature value (format: sig1=:base64:)
124
+ const sigMatch = signature.match(new RegExp(`${parsed.label}=:([^:]+):`))
125
+ if (!sigMatch) return false
126
+
127
+ return verifyEd25519Signature(pubKey, sigMatch[1], base)
128
+ }
package/src/xmtp.ts ADDED
@@ -0,0 +1,188 @@
1
+ import { Client, IdentifierKind } from '@xmtp/node-sdk'
2
+ import type { Signer, Identifier, Dm } from '@xmtp/node-sdk'
3
+ import { privateKeyToAccount } from 'viem/accounts'
4
+ import { hexToBytes } from 'viem'
5
+ import { getConfig } from './config'
6
+
7
+ const HUMAN_ADDRESS = process.env.HUMAN_XMTP_ADDRESS || ''
8
+
9
+ // Module-level state
10
+ let xmtpClient: Client | null = null
11
+ let humanDm: Dm | null = null
12
+ let lastDeniedAgent = ''
13
+
14
+ function humanIdentifier(): Identifier {
15
+ return {
16
+ identifier: HUMAN_ADDRESS.toLowerCase(),
17
+ identifierKind: IdentifierKind.Ethereum,
18
+ }
19
+ }
20
+
21
+ // ----------------------------------------------------------------
22
+ // Incoming message listener — detects "override" replies
23
+ // ----------------------------------------------------------------
24
+ async function startMessageListener(): Promise<void> {
25
+ if (!xmtpClient) return
26
+ try {
27
+ const stream = await xmtpClient.conversations.streamAllMessages()
28
+ for await (const message of stream) {
29
+ try {
30
+ const text = typeof message.content === 'string' ? message.content.trim().toLowerCase() : ''
31
+ if (text.startsWith('override') && lastDeniedAgent) {
32
+ const port = process.env.PORT || '4021'
33
+ const agentToOverride = lastDeniedAgent
34
+ lastDeniedAgent = ''
35
+ const res = await fetch(`http://localhost:${port}/api/override/${agentToOverride}`, { method: 'POST' })
36
+ if (res.ok) {
37
+ console.log(`[XMTP] Override accepted for agent: ${agentToOverride}`)
38
+ }
39
+ }
40
+ } catch (err) {
41
+ console.error('[XMTP] Error processing message:', err)
42
+ }
43
+ }
44
+ } catch (err) {
45
+ console.error('[XMTP] Message stream error:', err)
46
+ }
47
+ }
48
+
49
+ // ----------------------------------------------------------------
50
+ // Send helper — finds or creates DM with human, caches it
51
+ // ----------------------------------------------------------------
52
+ async function sendToHuman(text: string): Promise<void> {
53
+ if (!xmtpClient) return
54
+ try {
55
+ if (!humanDm) {
56
+ humanDm = await xmtpClient.conversations.createDmWithIdentifier(humanIdentifier())
57
+ }
58
+ await humanDm.sendText(text)
59
+ } catch (err) {
60
+ console.error('[XMTP] Send error:', err)
61
+ humanDm = null // reset so next call retries
62
+ }
63
+ }
64
+
65
+ // ----------------------------------------------------------------
66
+ // Exported functions
67
+ // ----------------------------------------------------------------
68
+
69
+ export async function initXMTP(): Promise<void> {
70
+ if (!getConfig().xmtp?.enabled) {
71
+ console.log('[XMTP] Disabled by config')
72
+ return
73
+ }
74
+ const privateKey = process.env.XMTP_PRIVATE_KEY
75
+ if (!privateKey || !HUMAN_ADDRESS) {
76
+ console.log('[XMTP] Not configured — notifications disabled')
77
+ return
78
+ }
79
+
80
+ try {
81
+ const account = privateKeyToAccount(privateKey as `0x${string}`)
82
+
83
+ const signer: Signer = {
84
+ type: 'EOA',
85
+ getIdentifier: (): Identifier => ({
86
+ identifier: account.address.toLowerCase(),
87
+ identifierKind: IdentifierKind.Ethereum,
88
+ }),
89
+ signMessage: async (message: string): Promise<Uint8Array> => {
90
+ const sig = await account.signMessage({ message })
91
+ return hexToBytes(sig)
92
+ },
93
+ }
94
+
95
+ // `as any` required: ClientOptions is a union type that doesn't survive
96
+ // Omit<> in Client.create's signature — SDK types collapse the union.
97
+ xmtpClient = await Client.create(signer, { env: 'production' } as any)
98
+
99
+ // Warm up the DM channel
100
+ humanDm = await xmtpClient.conversations.createDmWithIdentifier(humanIdentifier())
101
+
102
+ // Start listening for override replies (fire and forget)
103
+ startMessageListener().catch((err) => {
104
+ console.error('[XMTP] Listener startup error:', err)
105
+ })
106
+
107
+ console.log('[XMTP] Initialized — notifications enabled')
108
+ } catch (err) {
109
+ console.error('[XMTP] Init failed:', err)
110
+ }
111
+ }
112
+
113
+ export async function notifyDenial(
114
+ agent: string,
115
+ amount: number,
116
+ trustScore: number,
117
+ tier: string,
118
+ dailyLimit: number,
119
+ dailySpent: number,
120
+ reason: string,
121
+ txTo: string,
122
+ ): Promise<void> {
123
+ if (!xmtpClient) return
124
+ lastDeniedAgent = agent
125
+ const msg = [
126
+ `⚠️ Transaction denied`,
127
+ `Agent: ${agent}`,
128
+ `Amount: $${amount.toFixed(2)} USDC → ${txTo}`,
129
+ `Trust score: ${trustScore} (${tier} tier)`,
130
+ `Daily limit: $${dailyLimit} | Spent today: $${dailySpent.toFixed(2)}`,
131
+ `Reason: ${reason}`,
132
+ ``,
133
+ `Reply "override" to approve this transaction.`,
134
+ ].join('\n')
135
+ await sendToHuman(msg)
136
+ }
137
+
138
+ export async function notifyTrustChange(
139
+ agent: string,
140
+ oldScore: number,
141
+ newScore: number,
142
+ oldTier: string,
143
+ newTier: string,
144
+ reason: string,
145
+ ): Promise<void> {
146
+ if (!xmtpClient) return
147
+ const msg = [
148
+ `📊 Trust update: ${agent}`,
149
+ `Score: ${oldScore} → ${newScore} | Tier: ${oldTier} → ${newTier}`,
150
+ `Reason: ${reason}`,
151
+ ].join('\n')
152
+ await sendToHuman(msg)
153
+ }
154
+
155
+ export async function notifyBudgetWarning(
156
+ agent: string,
157
+ spent: number,
158
+ limit: number,
159
+ ): Promise<void> {
160
+ if (!xmtpClient) return
161
+ const percentage = Math.round((spent / limit) * 100)
162
+ const remaining = limit - spent
163
+ const msg = [
164
+ `💰 Budget alert: ${agent}`,
165
+ `Daily spend: $${spent.toFixed(2)} / $${limit} (${percentage}%)`,
166
+ `Remaining: $${remaining.toFixed(2)}`,
167
+ ].join('\n')
168
+ await sendToHuman(msg)
169
+ }
170
+
171
+ export async function notifyAnomaly(
172
+ agent: string,
173
+ pattern: string,
174
+ action: string,
175
+ riskPenalty: number,
176
+ oldScore: number,
177
+ newScore: number,
178
+ ): Promise<void> {
179
+ if (!xmtpClient) return
180
+ const msg = [
181
+ `🚨 Anomaly: ${agent}`,
182
+ `Pattern: ${pattern}`,
183
+ `Action: ${action}`,
184
+ `Risk penalty: -${riskPenalty}`,
185
+ `Trust score: ${oldScore} → ${newScore}`,
186
+ ].join('\n')
187
+ await sendToHuman(msg)
188
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "warningThreshold": 0.8,
3
+ "scoreBands": [
4
+ { "name": "Sovereign", "min": 80, "dailyLimit": 1000, "perTxLimit": 500 },
5
+ { "name": "Trusted", "min": 60, "dailyLimit": 200, "perTxLimit": 100 },
6
+ { "name": "Building", "min": 40, "dailyLimit": 50, "perTxLimit": 25 },
7
+ { "name": "Cautious", "min": 20, "dailyLimit": 10, "perTxLimit": 5 },
8
+ { "name": "Restricted", "min": 1, "dailyLimit": 2, "perTxLimit": 1 },
9
+ { "name": "Frozen", "min": 0, "dailyLimit": 0, "perTxLimit": 0 }
10
+ ],
11
+ "xmtp": { "enabled": true },
12
+ "worldId": { "enabled": true },
13
+ "webBotAuth": { "enabled": true },
14
+ "networkScore": { "enabled": true },
15
+ "scoring": {
16
+ "overrideBoost": 3,
17
+ "maxFrequencyPenalty": 10,
18
+ "inactivityDecayRate": 0.5
19
+ },
20
+ "port": 4021,
21
+ "dashboardEnabled": true
22
+ }