clinch-core 0.6.2 → 0.7.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 +52 -4
- package/dist/index.d.ts +9 -0
- package/dist/index.js +115 -2
- package/package.json +1 -1
- package/src/index.ts +138 -3
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
The Agent Negotiation Protocol (ANP) governs how two autonomous software agents establish identity, declare capabilities, and reach a cryptographically verifiable agreement on price and terms.
|
|
12
12
|
|
|
13
13
|
### 1.1 Address Formatting & Routing
|
|
14
|
-
All destination routing in Clinch relies on
|
|
14
|
+
All destination routing in Clinch relies on structured, prefixed addresses:
|
|
15
15
|
|
|
16
16
|
$$\text{PROTOCOL\_MODE} \mathbin{.} \text{domain} \mathbin{.} \text{TLD}$$
|
|
17
17
|
|
|
@@ -36,9 +36,10 @@ The SDK maintains an isolated, turn-based state machine for each active session.
|
|
|
36
36
|
* `OFFLINE`: Client is completely disconnected.
|
|
37
37
|
* `CONNECTING`: Authenticating identity and performing local Proof-of-Work (PoW) verification.
|
|
38
38
|
* `IDLE`: Connected and authenticated. Ready to initialize new sessions.
|
|
39
|
+
* `RECONNECTING`: Socket connection lost; executing exponential backoff.
|
|
39
40
|
* `NEGOTIATING`: Active, turn-based bargaining sequence in progress.
|
|
40
41
|
* `CONVERGED`: Mathematical convergence reached; agreement co-signed.
|
|
41
|
-
* `STALEMATE`:
|
|
42
|
+
* `STALEMATE`: Negotiation terminated; max turns reached without convergence.
|
|
42
43
|
* `ERROR`: Internal network, compilation, or cryptographic validation failure.
|
|
43
44
|
|
|
44
45
|
### Developer Event Subscriptions
|
|
@@ -114,7 +115,7 @@ async function runAutonomousSession() {
|
|
|
114
115
|
runAutonomousSession();
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
### 4.2
|
|
118
|
+
### 4.2 Webhook & Horizontal Scale Pattern
|
|
118
119
|
For enterprise cloud environments where processes must remain stateless or scale horizontally across server pods, you must serialize and rehydrate active sessions dynamically. This ensures that when an asynchronous callback arrives, any pod can load the session state and use the matching ephemeral key to sign the next turn.
|
|
119
120
|
|
|
120
121
|
```javascript
|
|
@@ -167,11 +168,51 @@ webhookCore.on('callback_received', async (event) => {
|
|
|
167
168
|
});
|
|
168
169
|
```
|
|
169
170
|
|
|
171
|
+
### 4.3 Cascading Multi-Seller Squeeze and Races
|
|
172
|
+
To implement market discovery, the SDK supports cascading negotiations across multiple matching sellers. The Core orchestrates two distinct optimization strategies:
|
|
173
|
+
|
|
174
|
+
#### Sequential Squeeze (Default)
|
|
175
|
+
Useful for high-value items where time is secondary to price. The SDK negotiates sequentially with each seller, using the converged deal price of the previous seller as the strict, maximum budget ceiling for the next. This dynamically forces sellers to underbid one another.
|
|
176
|
+
|
|
177
|
+
#### Parallel Race
|
|
178
|
+
Necessary for real-time services like ride-hailing or logistics. The SDK handshakes and conducts negotiations with all selected sellers concurrently in parallel. Once all sessions finish, it selects the absolute lowest price converged under your budget.
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
// Triggers the cascading loop
|
|
182
|
+
const bestDeal = await core.negotiateCascade(
|
|
183
|
+
'domain_name', // Category to discover
|
|
184
|
+
{ intent: 'purchase', item: 'mybrand.io', max_budget: 150.00 },
|
|
185
|
+
3, // Max sellers to target
|
|
186
|
+
'sequential' // 'sequential' | 'parallel'
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (bestDeal) {
|
|
190
|
+
console.log(`🏆 Optimal deal secured with ${bestDeal.sellerId} at $${bestDeal.finalPrice}`);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 4.4 Blind Key Pass (Credential Injection)
|
|
195
|
+
For sessions with services that require authorization tokens or API keys to operate (such as platform execution nodes), you can register these credentials locally.
|
|
196
|
+
|
|
197
|
+
The Core library securely stores these secrets and silently injects them into the handshake transport layer during session initialization. This prevents the API keys from ever being exposed to the AI model's context window, safeguarding you against prompt injection attacks.
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
// Register the API credential locally bound to the domain namespace
|
|
201
|
+
core.registerSecret('apify.anp', 'apify_sec_key_xyz987', 'Apify Production Token');
|
|
202
|
+
|
|
203
|
+
// Handshaking with this address now automatically injects the credential silently
|
|
204
|
+
const sessionId = await core.negotiate('ANP/C.apify.anp', {
|
|
205
|
+
intent: 'purchase',
|
|
206
|
+
item: 'actor-scraper-run',
|
|
207
|
+
max_budget: 5.00
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
170
211
|
---
|
|
171
212
|
|
|
172
213
|
## 5. Integration Guide: Seller Nodes
|
|
173
214
|
|
|
174
|
-
Sellers use the `ClinchSeller` class to build compliant, automated endpoints.
|
|
215
|
+
Sellers use the `ClinchSeller` class to build compliant, automated endpoints.
|
|
175
216
|
|
|
176
217
|
### 5.1 Decoupled Routing Architecture
|
|
177
218
|
To prevent centralized token expiration failures, Clinch separates ownership from routing:
|
|
@@ -241,6 +282,13 @@ Initializes the cryptographic handshake with a seller. Returns `sessionId`.
|
|
|
241
282
|
#### `exportSessionState(sessionId)` / `importSessionState(serializedData)`
|
|
242
283
|
Serializes or de-serializes all in-flight state for a given session, including ephemeral Ed25519 keys, state metrics, and local sandbox parameters, into an exchangeable JSON string.
|
|
243
284
|
|
|
285
|
+
#### `registerSecret(domain, key, name?)` / `clearSecret(domain)`
|
|
286
|
+
Saves or clears an API secret locally. The key is never visible to the AI agent and is silently passed in handshake payloads to the designated domain.
|
|
287
|
+
|
|
288
|
+
#### `async negotiateCascade(category, constraints, maxSellers?, strategy?)`
|
|
289
|
+
Queries the discovery register and cascade-negotiates with matching nodes.
|
|
290
|
+
* `strategy`: `'sequential'` (Sequential Squeeze) or `'parallel'` (Concurrent Race). Default: `'sequential'`.
|
|
291
|
+
|
|
244
292
|
#### `buildAgentPrompt(sessionId, incomingMessage)`
|
|
245
293
|
Assembles a contextual, structure-compliant system prompt for external LLMs to ensure compliant JSON execution.
|
|
246
294
|
|
package/dist/index.d.ts
CHANGED
|
@@ -48,12 +48,15 @@ export declare class ClinchCore extends EventEmitter {
|
|
|
48
48
|
identityPubKey: string;
|
|
49
49
|
private activeSessions;
|
|
50
50
|
private ws;
|
|
51
|
+
private localSecrets;
|
|
51
52
|
private isSandboxMode;
|
|
52
53
|
private sandboxModelContext;
|
|
53
54
|
private sandboxMaxTurns;
|
|
54
55
|
get activeNegotiationId(): string | null;
|
|
55
56
|
constructor(config?: ClinchConfig);
|
|
56
57
|
private setStatus;
|
|
58
|
+
registerSecret(domain: string, key: string, name?: string): void;
|
|
59
|
+
clearSecret(domain: string): void;
|
|
57
60
|
initialize(cachedToken?: string): Promise<void>;
|
|
58
61
|
private getRegistryUrl;
|
|
59
62
|
private networkRequest;
|
|
@@ -69,6 +72,12 @@ export declare class ClinchCore extends EventEmitter {
|
|
|
69
72
|
negotiate(address: string, constraints: ConstraintVector): Promise<string>;
|
|
70
73
|
sendCounter(sessionId: string, price: number, reason: string): Promise<void>;
|
|
71
74
|
exitSession(sessionId: string): Promise<string>;
|
|
75
|
+
negotiateCascade(category: string, constraints: ConstraintVector, maxSellers?: number, strategy?: 'sequential' | 'parallel'): Promise<{
|
|
76
|
+
sessionId: string;
|
|
77
|
+
sellerId: string;
|
|
78
|
+
finalPrice: number;
|
|
79
|
+
} | null>;
|
|
80
|
+
private waitForSession;
|
|
72
81
|
sandbox(config?: SandboxConfig): Promise<void>;
|
|
73
82
|
private setupSandbox;
|
|
74
83
|
private handleAutomaticSandboxTurn;
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
47
47
|
const PROTOCOL_VERSION = "0.1.0";
|
|
48
48
|
const FIREBASE_CONFIG_URL = "https://clinchprotocol.web.app/network-config.json";
|
|
49
49
|
function toHex(arr) {
|
|
50
|
-
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
50
|
+
return Array.from(arr).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
51
51
|
}
|
|
52
52
|
function fromHex(hex) {
|
|
53
53
|
// Strips all spaces, newlines, and rogue quotes from .env strings
|
|
@@ -55,7 +55,7 @@ function fromHex(hex) {
|
|
|
55
55
|
const match = clean.match(/.{1,2}/g);
|
|
56
56
|
if (!match)
|
|
57
57
|
return new Uint8Array(0);
|
|
58
|
-
return new Uint8Array(match.map(byte => parseInt(byte, 16)));
|
|
58
|
+
return new Uint8Array(match.map((byte) => parseInt(byte, 16)));
|
|
59
59
|
}
|
|
60
60
|
// ============================================================================
|
|
61
61
|
// THE CLINCH CORE LIBRARY (Buyer)
|
|
@@ -71,6 +71,8 @@ class ClinchCore extends events_1.EventEmitter {
|
|
|
71
71
|
identityPubKey;
|
|
72
72
|
activeSessions = new Map();
|
|
73
73
|
ws = null;
|
|
74
|
+
// Blind Key Pass Local Secret Store
|
|
75
|
+
localSecrets = new Map();
|
|
74
76
|
isSandboxMode = false;
|
|
75
77
|
sandboxModelContext = null;
|
|
76
78
|
sandboxMaxTurns = 6;
|
|
@@ -99,6 +101,18 @@ class ClinchCore extends events_1.EventEmitter {
|
|
|
99
101
|
this.emit('log', `🟡 [State] ${this.status}`);
|
|
100
102
|
}
|
|
101
103
|
}
|
|
104
|
+
// --------------------------------------------------------------------------
|
|
105
|
+
// BLIND KEY PASS MANAGERS (Silent API Key Handshake)
|
|
106
|
+
// --------------------------------------------------------------------------
|
|
107
|
+
registerSecret(domain, key, name) {
|
|
108
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
109
|
+
this.localSecrets.set(normalizedDomain, { key, name });
|
|
110
|
+
this.emit('log', `[Security] Blind key registered for ${normalizedDomain} (${name || 'Unnamed'})`);
|
|
111
|
+
}
|
|
112
|
+
clearSecret(domain) {
|
|
113
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
114
|
+
this.localSecrets.delete(normalizedDomain);
|
|
115
|
+
}
|
|
102
116
|
async initialize(cachedToken) {
|
|
103
117
|
if (this.status === 'IDLE' || this.status === 'CONNECTING')
|
|
104
118
|
return;
|
|
@@ -311,6 +325,7 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
311
325
|
const parsed = this.parseAddress(address);
|
|
312
326
|
const ephemeralKeys = tweetnacl_1.default.sign.keyPair();
|
|
313
327
|
const ephemeralPubHex = toHex(ephemeralKeys.publicKey);
|
|
328
|
+
const blindSecret = this.localSecrets.get(parsed.domain);
|
|
314
329
|
const initPayload = {
|
|
315
330
|
clinch_version: PROTOCOL_VERSION,
|
|
316
331
|
mode: parsed.mode,
|
|
@@ -318,6 +333,10 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
318
333
|
session_pub_key: ephemeralPubHex,
|
|
319
334
|
timestamp_utc: new Date().toISOString()
|
|
320
335
|
};
|
|
336
|
+
if (blindSecret) {
|
|
337
|
+
initPayload.blind_auth_token = blindSecret.key;
|
|
338
|
+
this.emit('log', `[Security] Silently injected blind token for ${parsed.domain} at transport layer`);
|
|
339
|
+
}
|
|
321
340
|
const msgUint8 = new TextEncoder().encode(JSON.stringify(initPayload));
|
|
322
341
|
const signature = toHex(tweetnacl_1.default.sign.detached(msgUint8, ephemeralKeys.secretKey));
|
|
323
342
|
const response = await this.networkRequest(`/api/route/${parsed.domain}/handshake`, {
|
|
@@ -368,6 +387,100 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
368
387
|
session.exitTokenHash = res.token_hash;
|
|
369
388
|
return res.token_hash;
|
|
370
389
|
}
|
|
390
|
+
// --------------------------------------------------------------------------
|
|
391
|
+
// CASCADING ITERATIVE NEGOTIATION (Squeeze vs. Parallel Concurrency)
|
|
392
|
+
// --------------------------------------------------------------------------
|
|
393
|
+
async negotiateCascade(category, constraints, maxSellers = 3, strategy = 'sequential') {
|
|
394
|
+
this.emit('log', `[Cascade] Querying registry for matching sellers under "${category}"...`);
|
|
395
|
+
const discovery = await this.search(category);
|
|
396
|
+
const sellers = (discovery.results || []).slice(0, maxSellers);
|
|
397
|
+
if (sellers.length === 0) {
|
|
398
|
+
this.emit('log', `[Cascade] No matching sellers found for category: "${category}"`);
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
// ── STRATEGY 1: PARALLEL RACE (High Urgency / Ride Hailing) ──
|
|
402
|
+
if (strategy === 'parallel') {
|
|
403
|
+
this.emit('log', `[Cascade] ⚡ Running PARALLEL RACE simultaneously across ${sellers.length} seller nodes...`);
|
|
404
|
+
const sessionPromises = sellers.map(async (seller) => {
|
|
405
|
+
const targetAddress = `ANP/C.${seller.agent_id}`;
|
|
406
|
+
try {
|
|
407
|
+
const sessionId = await this.negotiate(targetAddress, constraints);
|
|
408
|
+
const result = await this.waitForSession(sessionId);
|
|
409
|
+
return { sellerId: seller.agent_id, sessionId, ...result };
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
this.emit('log', `[Cascade] ⚠️ Connection failed for parallel channel ${seller.agent_id}: ${e.message}`);
|
|
413
|
+
return { sellerId: seller.agent_id, sessionId: '', outcome: 'stalemate', price: Infinity };
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
const results = await Promise.all(sessionPromises);
|
|
417
|
+
const successfulDeals = results.filter((r) => r.outcome === 'deal' && r.price <= constraints.max_budget);
|
|
418
|
+
if (successfulDeals.length === 0) {
|
|
419
|
+
this.emit('log', `[Cascade] ✗ Parallel race completed. No successful deals reached.`);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
successfulDeals.sort((a, b) => a.price - b.price);
|
|
423
|
+
const winner = successfulDeals[0];
|
|
424
|
+
this.emit('log', `[Cascade] 🏆 Parallel race complete! Lowest offer: $${winner.price} from ${winner.sellerId}`);
|
|
425
|
+
return {
|
|
426
|
+
sessionId: winner.sessionId,
|
|
427
|
+
sellerId: winner.sellerId,
|
|
428
|
+
finalPrice: winner.price
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
// ── STRATEGY 2: SEQUENTIAL SQUEEZE (Low Urgency / Price Optimization) ──
|
|
432
|
+
this.emit('log', `[Cascade] ➔ Running SEQUENTIAL SQUEEZE across ${sellers.length} seller nodes...`);
|
|
433
|
+
let bestDeal = null;
|
|
434
|
+
let currentBudgetCeiling = constraints.max_budget;
|
|
435
|
+
for (const seller of sellers) {
|
|
436
|
+
const targetAddress = `ANP/C.${seller.agent_id}`;
|
|
437
|
+
this.emit('log', `\n[Cascade] Squeezing target: ${targetAddress} | Squeeze Ceiling: $${currentBudgetCeiling}`);
|
|
438
|
+
const sessionConstraints = {
|
|
439
|
+
...constraints,
|
|
440
|
+
max_budget: currentBudgetCeiling
|
|
441
|
+
};
|
|
442
|
+
try {
|
|
443
|
+
const sessionId = await this.negotiate(targetAddress, sessionConstraints);
|
|
444
|
+
const result = await this.waitForSession(sessionId);
|
|
445
|
+
if (result.outcome === 'deal' && result.price < currentBudgetCeiling) {
|
|
446
|
+
this.emit('log', `[Cascade] ✓ Better deal clinched at $${result.price} from ${seller.agent_id}!`);
|
|
447
|
+
bestDeal = {
|
|
448
|
+
sessionId,
|
|
449
|
+
sellerId: seller.agent_id,
|
|
450
|
+
finalPrice: result.price
|
|
451
|
+
};
|
|
452
|
+
currentBudgetCeiling = result.price;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
this.emit('log', `[Cascade] ✗ Seller ${seller.agent_id} failed to beat current best offer of $${currentBudgetCeiling}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
this.emit('log', `[Cascade] ⚠️ Dynamic session error with ${seller.agent_id}: ${e.message}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return bestDeal;
|
|
463
|
+
}
|
|
464
|
+
waitForSession(sessionId) {
|
|
465
|
+
return new Promise((resolve) => {
|
|
466
|
+
const onClosed = (event) => {
|
|
467
|
+
if (event.sessionId === sessionId) {
|
|
468
|
+
this.off('session_closed', onClosed);
|
|
469
|
+
this.off('status_changed', onStatus);
|
|
470
|
+
resolve({ outcome: 'deal', price: event.finalPrice });
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
const onStatus = (status) => {
|
|
474
|
+
if (status === 'STALEMATE') {
|
|
475
|
+
this.off('session_closed', onClosed);
|
|
476
|
+
this.off('status_changed', onStatus);
|
|
477
|
+
resolve({ outcome: 'stalemate', price: Infinity });
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
this.on('session_closed', onClosed);
|
|
481
|
+
this.on('status_changed', onStatus);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
371
484
|
async sandbox(config = {}) {
|
|
372
485
|
this.isSandboxMode = true;
|
|
373
486
|
this.sandboxMaxTurns = config.maxTurns || 6;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ const PROTOCOL_VERSION = "0.1.0";
|
|
|
10
10
|
const FIREBASE_CONFIG_URL = "https://clinchprotocol.web.app/network-config.json";
|
|
11
11
|
|
|
12
12
|
function toHex(arr: Uint8Array | number[]): string {
|
|
13
|
-
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
13
|
+
return Array.from(arr).map((b: number) => b.toString(16).padStart(2, '0')).join('');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function fromHex(hex: string): Uint8Array {
|
|
@@ -18,7 +18,7 @@ function fromHex(hex: string): Uint8Array {
|
|
|
18
18
|
const clean = hex.replace(/[^0-9a-fA-F]/g, '');
|
|
19
19
|
const match = clean.match(/.{1,2}/g);
|
|
20
20
|
if (!match) return new Uint8Array(0);
|
|
21
|
-
return new Uint8Array(match.map(byte => parseInt(byte, 16)));
|
|
21
|
+
return new Uint8Array(match.map((byte: string) => parseInt(byte, 16)));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// ============================================================================
|
|
@@ -97,6 +97,9 @@ export class ClinchCore extends EventEmitter {
|
|
|
97
97
|
private activeSessions = new Map<string, SessionState>();
|
|
98
98
|
private ws: WebSocket | null = null;
|
|
99
99
|
|
|
100
|
+
// Blind Key Pass Local Secret Store
|
|
101
|
+
private localSecrets = new Map<string, { key: string, name?: string }>();
|
|
102
|
+
|
|
100
103
|
private isSandboxMode = false;
|
|
101
104
|
private sandboxModelContext: any = null;
|
|
102
105
|
private sandboxMaxTurns = 6;
|
|
@@ -129,6 +132,20 @@ export class ClinchCore extends EventEmitter {
|
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
// --------------------------------------------------------------------------
|
|
136
|
+
// BLIND KEY PASS MANAGERS (Silent API Key Handshake)
|
|
137
|
+
// --------------------------------------------------------------------------
|
|
138
|
+
public registerSecret(domain: string, key: string, name?: string): void {
|
|
139
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
140
|
+
this.localSecrets.set(normalizedDomain, { key, name });
|
|
141
|
+
this.emit('log', `[Security] Blind key registered for ${normalizedDomain} (${name || 'Unnamed'})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public clearSecret(domain: string): void {
|
|
145
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
146
|
+
this.localSecrets.delete(normalizedDomain);
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
async initialize(cachedToken?: string): Promise<void> {
|
|
133
150
|
if (this.status === 'IDLE' || this.status === 'CONNECTING') return;
|
|
134
151
|
this.setStatus('CONNECTING');
|
|
@@ -356,7 +373,9 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
356
373
|
const ephemeralKeys = nacl.sign.keyPair();
|
|
357
374
|
const ephemeralPubHex = toHex(ephemeralKeys.publicKey);
|
|
358
375
|
|
|
359
|
-
const
|
|
376
|
+
const blindSecret = this.localSecrets.get(parsed.domain);
|
|
377
|
+
|
|
378
|
+
const initPayload: any = {
|
|
360
379
|
clinch_version: PROTOCOL_VERSION,
|
|
361
380
|
mode: parsed.mode,
|
|
362
381
|
constraints,
|
|
@@ -364,6 +383,11 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
364
383
|
timestamp_utc: new Date().toISOString()
|
|
365
384
|
};
|
|
366
385
|
|
|
386
|
+
if (blindSecret) {
|
|
387
|
+
initPayload.blind_auth_token = blindSecret.key;
|
|
388
|
+
this.emit('log', `[Security] Silently injected blind token for ${parsed.domain} at transport layer`);
|
|
389
|
+
}
|
|
390
|
+
|
|
367
391
|
const msgUint8 = new TextEncoder().encode(JSON.stringify(initPayload));
|
|
368
392
|
const signature = toHex(nacl.sign.detached(msgUint8, ephemeralKeys.secretKey));
|
|
369
393
|
|
|
@@ -426,6 +450,117 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
|
|
|
426
450
|
return res.token_hash;
|
|
427
451
|
}
|
|
428
452
|
|
|
453
|
+
// --------------------------------------------------------------------------
|
|
454
|
+
// CASCADING ITERATIVE NEGOTIATION (Squeeze vs. Parallel Concurrency)
|
|
455
|
+
// --------------------------------------------------------------------------
|
|
456
|
+
public async negotiateCascade(
|
|
457
|
+
category: string,
|
|
458
|
+
constraints: ConstraintVector,
|
|
459
|
+
maxSellers = 3,
|
|
460
|
+
strategy: 'sequential' | 'parallel' = 'sequential'
|
|
461
|
+
): Promise<{ sessionId: string, sellerId: string, finalPrice: number } | null> {
|
|
462
|
+
this.emit('log', `[Cascade] Querying registry for matching sellers under "${category}"...`);
|
|
463
|
+
const discovery = await this.search(category);
|
|
464
|
+
const sellers: any[] = (discovery.results || []).slice(0, maxSellers);
|
|
465
|
+
|
|
466
|
+
if (sellers.length === 0) {
|
|
467
|
+
this.emit('log', `[Cascade] No matching sellers found for category: "${category}"`);
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── STRATEGY 1: PARALLEL RACE (High Urgency / Ride Hailing) ──
|
|
472
|
+
if (strategy === 'parallel') {
|
|
473
|
+
this.emit('log', `[Cascade] ⚡ Running PARALLEL RACE simultaneously across ${sellers.length} seller nodes...`);
|
|
474
|
+
|
|
475
|
+
const sessionPromises = sellers.map(async (seller: any) => {
|
|
476
|
+
const targetAddress = `ANP/C.${seller.agent_id}`;
|
|
477
|
+
try {
|
|
478
|
+
const sessionId = await this.negotiate(targetAddress, constraints);
|
|
479
|
+
const result = await this.waitForSession(sessionId);
|
|
480
|
+
return { sellerId: seller.agent_id, sessionId, ...result };
|
|
481
|
+
} catch (e: any) {
|
|
482
|
+
this.emit('log', `[Cascade] ⚠️ Connection failed for parallel channel ${seller.agent_id}: ${e.message}`);
|
|
483
|
+
return { sellerId: seller.agent_id, sessionId: '', outcome: 'stalemate' as const, price: Infinity };
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const results = await Promise.all(sessionPromises);
|
|
488
|
+
const successfulDeals = results.filter((r: any) => r.outcome === 'deal' && r.price <= constraints.max_budget);
|
|
489
|
+
|
|
490
|
+
if (successfulDeals.length === 0) {
|
|
491
|
+
this.emit('log', `[Cascade] ✗ Parallel race completed. No successful deals reached.`);
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
successfulDeals.sort((a: any, b: any) => a.price - b.price);
|
|
496
|
+
const winner = successfulDeals[0];
|
|
497
|
+
|
|
498
|
+
this.emit('log', `[Cascade] 🏆 Parallel race complete! Lowest offer: $${winner.price} from ${winner.sellerId}`);
|
|
499
|
+
return {
|
|
500
|
+
sessionId: winner.sessionId,
|
|
501
|
+
sellerId: winner.sellerId,
|
|
502
|
+
finalPrice: winner.price
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── STRATEGY 2: SEQUENTIAL SQUEEZE (Low Urgency / Price Optimization) ──
|
|
507
|
+
this.emit('log', `[Cascade] ➔ Running SEQUENTIAL SQUEEZE across ${sellers.length} seller nodes...`);
|
|
508
|
+
let bestDeal: { sessionId: string, sellerId: string, finalPrice: number } | null = null;
|
|
509
|
+
let currentBudgetCeiling = constraints.max_budget;
|
|
510
|
+
|
|
511
|
+
for (const seller of sellers) {
|
|
512
|
+
const targetAddress = `ANP/C.${seller.agent_id}`;
|
|
513
|
+
this.emit('log', `\n[Cascade] Squeezing target: ${targetAddress} | Squeeze Ceiling: $${currentBudgetCeiling}`);
|
|
514
|
+
|
|
515
|
+
const sessionConstraints = {
|
|
516
|
+
...constraints,
|
|
517
|
+
max_budget: currentBudgetCeiling
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const sessionId = await this.negotiate(targetAddress, sessionConstraints);
|
|
522
|
+
const result = await this.waitForSession(sessionId);
|
|
523
|
+
|
|
524
|
+
if (result.outcome === 'deal' && result.price < currentBudgetCeiling) {
|
|
525
|
+
this.emit('log', `[Cascade] ✓ Better deal clinched at $${result.price} from ${seller.agent_id}!`);
|
|
526
|
+
bestDeal = {
|
|
527
|
+
sessionId,
|
|
528
|
+
sellerId: seller.agent_id,
|
|
529
|
+
finalPrice: result.price
|
|
530
|
+
};
|
|
531
|
+
currentBudgetCeiling = result.price;
|
|
532
|
+
} else {
|
|
533
|
+
this.emit('log', `[Cascade] ✗ Seller ${seller.agent_id} failed to beat current best offer of $${currentBudgetCeiling}`);
|
|
534
|
+
}
|
|
535
|
+
} catch (e: any) {
|
|
536
|
+
this.emit('log', `[Cascade] ⚠️ Dynamic session error with ${seller.agent_id}: ${e.message}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return bestDeal;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private waitForSession(sessionId: string): Promise<{ outcome: 'deal' | 'stalemate', price: number }> {
|
|
544
|
+
return new Promise((resolve) => {
|
|
545
|
+
const onClosed = (event: any) => {
|
|
546
|
+
if (event.sessionId === sessionId) {
|
|
547
|
+
this.off('session_closed', onClosed);
|
|
548
|
+
this.off('status_changed', onStatus);
|
|
549
|
+
resolve({ outcome: 'deal', price: event.finalPrice });
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const onStatus = (status: CoreStatus) => {
|
|
553
|
+
if (status === 'STALEMATE') {
|
|
554
|
+
this.off('session_closed', onClosed);
|
|
555
|
+
this.off('status_changed', onStatus);
|
|
556
|
+
resolve({ outcome: 'stalemate', price: Infinity });
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
this.on('session_closed', onClosed);
|
|
560
|
+
this.on('status_changed', onStatus);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
429
564
|
async sandbox(config: SandboxConfig = {}): Promise<void> {
|
|
430
565
|
this.isSandboxMode = true;
|
|
431
566
|
this.sandboxMaxTurns = config.maxTurns || 6;
|